Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .agents/skills/macro-private-maintainer/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ description: Master orchestration skill for maintaining, auditing, developing, a
4. **修改优先级原则**:配置 > 环境变量 > Adapter 替换 > 依赖注入 > 反代 Proxy > 小范围 Patch > 修改 Domain。
5. **凭据安全红线**:严禁在 Git 追踪的文件中硬编码真实服务器 IP、私钥、OAuth Secret 或 API Key。
6. **UI 设计系统一致性**:前端二开必须复用 Macro 官方 `@ui`、Kobalte primitives、Theme 语义 Token、既有字号与动效语言;禁止建立平行组件库、私有颜色体系、任意字号或无障碍不受控的自定义交互。
7. **上游/二开归因先行**:处理 CI、lint、typecheck、构建失败或 warning 前,先用 `git diff upstream/main -- <path>`、`git show upstream/main:<path>`、现有 workflow/just 脚本确认问题来源。结论必须区分:`上游已有`、`二开新增`、`二开触发上游隐患`。
8. **上游非阻断问题不主动改**:若 lint / type / test 输出来自 `upstream/main` 已存在的问题,且不影响当前构建、CI 门禁、生产运行或本次二开目标,只记录来源与风险,不为“清爽”而修改上游代码。避免把私有 fork 变成无关风格修复分支,增加后续 upstream merge 成本。
9. **二开代码必须贴合上游规范**:凡是本 fork 新增或本次触碰的二开代码,必须按上游现有目录边界、类型模型、query/service-client 分层、UI 组件规范、格式化与 lint 规则实现。若二开触发 warning/error,优先通过对齐上游模式修复;不要靠禁用规则、扩大类型、粗暴 cast、复制业务逻辑或改原始上游脚本来绕过。
10. **上游原生运维入口优先**:遇到生产/本地环境状态漂移、服务初始化顺序、外部系统配置缺失、IaC 未落地、数据库/FusionAuth/LocalStack/OpenSearch/Redis/Kafka 等运行时状态不一致时,先查仓库原生脚本、Just recipes、Pulumi/Terraform/IaC 栈、Docker Compose、迁移与 README,再判断是否为“脚本未执行 / import 未完成 / reconcile 未覆盖”。禁止先写新的旁路补丁、手工 curl 脚本或业务代码兜底来掩盖漂移。
11. **优先贴近 upstream 处理方式**:凡是 Macro 上游已有部署、初始化、导入、同步、回填、修复、seed、doctor、drift check、reconcile 等机制,优先复用或补齐调用路径;只有确认上游没有覆盖当前私有化场景时,才新增最小私有化封装,并明确标注原因与边界。

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ just stack down
- Do not treat local DEV success as production or CI parity.
- Use CodeGraph for symbol/call relationship analysis when `.codegraph/` exists.
- Use Cargo/Nix/Compose graphs for build dependency analysis instead of guessing from filenames.
- For runtime state drift such as missing FusionAuth IdPs, stale LocalStack resources, missing queues, stale Docker volumes, missing seeded roles, or service config that exists in code but not in the running environment, inspect upstream-native repo mechanisms first: `just` recipes, `xtask_local`, Pulumi stacks, Docker Compose, migrations, seed tools, README runbooks, and existing doctor/status commands. Prefer restoring the intended upstream/IaC reconcile path over adding one-off curl patches or business-code fallbacks.
- For CI parity checks, distinguish upstream-existing warnings from fork-introduced failures before editing. If a warning exists on `upstream/main` and does not fail the current gate, report it without changing upstream code. Fix fork-introduced errors/warnings by following the upstream file's existing patterns and boundaries.

## References

Expand Down
2 changes: 2 additions & 0 deletions .fork/customizations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ customizations:
- crates/macro_db_client/**
- crates/macro_db_migrator/**
- self-host/init/**
- apps/web/src/features/auth/EmailForm.tsx
- apps/web/src/features/auth/Login.tsx
tests:
- conventions
- auth-rust
Expand Down
3 changes: 0 additions & 3 deletions .github/workflows/validate-service-cache.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
name: Validate service cache planner

on:
push:
branches:
- build/service-cache-20260906
pull_request:
branches:
- main
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/components/app/app-sidebar/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ const DEFAULT_TRY_VISIBILITY: TryItemVisibility = {

const markdownDocumentsQuery = buildDocumentTypeQuery(['doc-markdown']);

const SIDEBAR_LINKS = [
const SIDEBAR_LINKS: SidebarItem[] = [
{
id: 'inbox',
label: 'Inbox',
Expand Down Expand Up @@ -234,7 +234,7 @@ const SIDEBAR_LINKS = [
hotkey: 'c',
hotkeyToken: TOKENS.sidebar.goTo.channels,
},
] satisfies SidebarItem[];
];

export type SidebarState = 'hidden' | 'expanded' | 'slim';

Expand Down Expand Up @@ -1373,9 +1373,9 @@ export const AppSidebar = (props: AppSidebarProps) => {
.map((id) => findLink(id))
.filter((link): link is SidebarItem => link !== undefined)
.map((link) => ({
id: link.id as SidebarSectionLinkId,
label: link.label,
checked: sectionVisibility()[link.id as SidebarSectionLinkId] ?? true,
onToggle: () => toggleSection(link.id as SidebarSectionLinkId),
}));

const tryItems = createMemo(() => {
Expand Down
15 changes: 4 additions & 11 deletions apps/web/src/components/app/sidebar-next/footer-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@ import { runCreateAction } from '@app/features/command/Launcher';
import { globalSplitManager } from '@app/signal/splitLayout';
import { CALENDAR_BLOCK_ID } from '@block-calendar/types';
import { SidebarSettingsWidget } from '@components/app/app-sidebar/sidebar';
import {
enableChatV3Agents,
isFeatureEnabled,
} from '@core/constant/featureFlags';
import {
type SettingsTab,
useSettingsState,
Expand Down Expand Up @@ -68,13 +64,10 @@ export const FooterActions = (props: {
// The two creatables both bind `a` and are mutually exclusive on the
// agents flag, so pick the one that is actually registered.
// `shouldInsert` is what `createBlock` turns into `preferNewSplit`.
runCreateAction(
isFeatureEnabled(enableChatV3Agents) ? 'agent' : 'chat',
{
shouldInsert: true,
source: 'sidebar',
}
)
runCreateAction('agent', {
shouldInsert: true,
source: 'sidebar',
})
}
>
<SparkleIcon />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { getViewPreset } from '@app/features/next-soup/sidebar/soup-filter-prese
import { NonMemberChannelPreview } from '@app/features/next-soup/soup-view/non-member-channel-preview';
import { SoupView } from '@app/features/next-soup/soup-view/soup-view';
import { useRecentViewFlag } from '@app/features/next-soup/use-recent-view-flag';
import { ReminderEditorSplit } from '@app/features/reminders/ReminderEditorSplit';
import { SettingsPanelComponentWrapper } from '@app/features/settings/Settings';
import { useAnalytics } from '@app/lib/analytics/analytics-context';
import { useFeatureFlag, usePosthog } from '@app/lib/analytics/posthog';
Expand Down Expand Up @@ -190,7 +191,12 @@ export function resolveComponent(
}
}
const fallback = REGISTRY.get('inbox');
if (fallback) return fallback;
if (fallback) {
return {
element: () => fallback.factory(params ?? {}),
initialMeta: fallback.initialMeta,
};
}
throw new Error(`Component '${name}' not registered`);
}
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ function SplitBackButton() {
square
size="sm"
class="p-1 rounded-lg touch:active:bg-transparent"
label={() => t('Go Back')}
label={t('Go Back')}
hotkey={TOKENS.split.go.back}
disabled={!context.handle.canGoBack()}
onClick={() => {
Expand All @@ -121,7 +121,7 @@ function SplitForwardButton() {
square
size="sm"
class="p-1 rounded-lg touch:active:bg-transparent"
label={() => t('Go Forward')}
label={t('Go Forward')}
hotkey={TOKENS.split.go.forward}
disabled={!context.handle.canGoForward()}
onClick={context.handle.goForward}
Expand Down Expand Up @@ -278,7 +278,7 @@ function SoupNavigationButtons() {
<div class="flex items-center gap-0.5">
<Button
class="p-1 rounded-lg"
label={() => t('Previous item')}
label={t('Previous item')}
hotkey={TOKENS.entity.step.start}
disabled={!canNavigateUp()}
onClick={() => navigate(-1)}
Expand All @@ -287,7 +287,7 @@ function SoupNavigationButtons() {
</Button>
<Button
class="p-1 rounded-lg"
label={() => t('Next item')}
label={t('Next item')}
hotkey={TOKENS.entity.step.end}
disabled={!canNavigateDown()}
onClick={() => navigate(1)}
Expand Down
5 changes: 3 additions & 2 deletions apps/web/src/components/app/split-layout/layoutManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,7 @@ export type SplitHandle<TMeta extends ComponentMeta = ComponentMeta> = {
* without a new entry). A no-op unless the split currently shows a block of
* `type`.
*/
adoptContentId: (options: { type: BlockName; nextId: string }) => void;
adoptContentId: (options: { type: SplitContentType; nextId: string }) => void;
removeFromHistory: (predicate: (content: SplitContent) => boolean) => void;
toggleSpotlight: (force?: boolean) => void;
setDisplayName: (name: string) => void;
Expand Down Expand Up @@ -1030,7 +1030,8 @@ export function createSplitLayout(
* cause is `replace`, so the URL sync swaps the path in place instead of
* adding a back step to a placeholder the user can never return to.
*/
function adoptContentId(id: SplitId, type: BlockName, nextId: string) {
function adoptContentId(id: SplitId, type: SplitContentType, nextId: string) {
if (type === 'component') return;
const i = splitIndexById(id);
if (i < 0) return;

Expand Down
12 changes: 10 additions & 2 deletions apps/web/src/features/auth/EmailForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { SERVER_HOSTS } from '@core/constant/servers';
import { platformFetch } from '@core/util/platformFetch';
import { authServiceClient } from '@service-auth/client';
import { passwordLogin } from '@queries/auth/login';
import { action, useSubmission } from '@solidjs/router';
import { Stage } from './Shared';

Expand All @@ -13,6 +13,14 @@ const REDIRECT_URI = `${protocol}://${window.location.host}/app`;
async function isPasswordLogin(email?: string | null) {
if (!email) return false;

// FORK-CUSTOM: AUTH-SELFHOST-001 - Allow self-host admin email to trigger password login flow
const adminEmail = (
window as unknown as { __MACRO_ENV__?: { ADMIN_EMAIL?: string } }
).__MACRO_ENV__?.ADMIN_EMAIL;
if (adminEmail && email.toLowerCase() === adminEmail.toLowerCase()) {
return true;
}

const encodedEmail = new TextEncoder().encode(email.toLowerCase());
const hashedBuffer = await crypto.subtle.digest('SHA-256', encodedEmail);
const hashedEmail = Array.from(new Uint8Array(hashedBuffer))
Expand All @@ -34,7 +42,7 @@ export const sendEmailCode = action(async (formData: FormData) => {
const password = formData.get('password');
if (!password || typeof password !== 'string') return 'isPasswordLogin';

const maybeTokens = await authServiceClient.passwordLogin({
const maybeTokens = await passwordLogin({
password,
email,
});
Expand Down
27 changes: 14 additions & 13 deletions apps/web/src/features/auth/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ import LogoIcon from '@icon/macro-logo.svg';
import ArrowLeft from '@phosphor/arrow-left.svg';
import ArrowRight from '@phosphor/arrow-right.svg';
import { useUserInfo } from '@queries/auth';
import { passwordlessCallback, sessionLogin } from '@queries/auth/login';
import {
invalidateAllAfterLogin,
useUserInfoQuery,
} from '@queries/auth/user-info';
import { authServiceClient } from '@service-auth/client';
import {
action,
useAction,
Expand Down Expand Up @@ -132,7 +132,7 @@ function LoginPicker(props: {

<Show when={showApple}>
<Button
variant="base"
variant="outline"
class="bg-surface"
onClick={() => startSsoLogin('Apple')}
>
Expand All @@ -141,7 +141,7 @@ function LoginPicker(props: {
</Button>
</Show>

<Button variant="base" class="bg-surface" onClick={continueWithEmail}>
<Button variant="outline" class="bg-surface" onClick={continueWithEmail}>
Continue with email
</Button>
</div>
Expand Down Expand Up @@ -229,12 +229,12 @@ function EmailFormNew(props: {
});

createEffect(() => {
if (sentEmailCode(submission.result)) {
props.setStage(Stage.Verify);
if (submission.result === 'LoggedIn') {
props.setStage(Stage.Done);
} else if (submission.result === 'isPasswordLogin') {
setIsPasswordLogin(true);
} else if (submission.result === 'LoggedIn') {
props.setStage(Stage.Done);
} else if (sentEmailCode(submission.result)) {
props.setStage(Stage.Verify);
}
});

Expand Down Expand Up @@ -267,7 +267,7 @@ function EmailFormNew(props: {
Continue
<ArrowRight class="size-4" />
</Button>
<Button variant="base" class="bg-surface" onClick={props.onBack}>
<Button variant="outline" class="bg-surface" onClick={props.onBack}>
<ArrowLeft class="size-4" />
Back to sign in
</Button>
Expand All @@ -281,7 +281,7 @@ const verifyCode = action(async (formData: FormData) => {
const email = formData.get('email');
if (typeof email !== 'string') throw new Error('Invalid email');

const result = await authServiceClient.passwordlessCallback({ code, email });
const result = await passwordlessCallback({ code, email });
if (result.isErr()) {
if (result.error.some((err) => err.code === 'UNAUTHORIZED')) {
throw new Error('Invalid code.');
Expand Down Expand Up @@ -442,7 +442,7 @@ function VerifyFormNew(props: {
Verify
<ArrowRight class="size-4" />
</Button>
<Button variant="base" class="bg-surface" onClick={props.onBack}>
<Button variant="outline" class="bg-surface" onClick={props.onBack}>
<ArrowLeft class="size-4" />
Change email
</Button>
Expand Down Expand Up @@ -492,9 +492,10 @@ export function Login(props: { signupMode?: boolean }) {
? rawToken[rawToken.length - 1]
: rawToken;
if (session_code && typeof session_code === 'string') {
unsetTokenPromise();
authServiceClient.sessionLogin({ session_code }).then(async (res) => {
void (async () => {
const res = await sessionLogin({ session_code });
if (res.isOk()) {
unsetTokenPromise();
await invalidateAllAfterLogin();
await initEmailLink().match(
() => {},
Expand All @@ -505,7 +506,7 @@ export function Login(props: { signupMode?: boolean }) {
}
);
}
});
})();
}
});

Expand Down
10 changes: 10 additions & 0 deletions apps/web/src/features/block-automation/component/Automation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { useBlockId } from '@core/block';
import { EntityIcon } from '@core/component/EntityIcon';
import { toast } from '@core/component/Toast/Toast';
import { blockNameToDefaultFile } from '@core/constant/allBlocks';
import { getAppCapabilities } from '@core/constant/featureFlags';
import { whenSettled } from '@core/util/whenSettled';
import { formatDateAndTime } from '@entity';
import CopyIcon from '@phosphor/copy.svg';
Expand Down Expand Up @@ -125,6 +126,15 @@ function HistoryList(props: { records: HistoryRecord[]; isPending: boolean }) {
}

export function Automation() {
const capabilities = getAppCapabilities();
if (!capabilities.scheduledActions) {
return (
<div class="flex size-full items-center justify-center px-4 text-center text-xs text-ink-muted">
Scheduled actions are unavailable in this profile.
</div>
);
}

const scheduleId = useBlockId();
const panel = useSplitPanelOrThrow();
const { replaceOrInsertSplit } = useSplitLayout();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export function MessageCard(props: MessageCardProps) {
<div class="@container/message macro-message-width macro-message-padding w-full">
<div
class={cn(
'relative p-4 rounded-lg bg-message border border-edge-muted outline-none',
'relative p-4 rounded-lg overflow-hidden bg-message border border-edge-muted outline-none',
props.isSelected
? 'z-1 light-mode:shadow-lg light-mode:shadow-drop-shadow dark-mode:ring-1 dark-mode:ring-accent/40'
: props.allowHover && 'hover:overlay-hover'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function SentMessageIndicator(
<Show when={props.isSent}>
<div
class={cn(
'absolute -inset-y-px -left-px w-1 rounded-l-lg transition-colors duration-700 ease-[cubic-bezier(0.16,1,0.3,1)] motion-reduce:transition-none bg-accent/70'
'absolute inset-y-0 left-0 w-0.5 transition-colors duration-700 ease-[cubic-bezier(0.16,1,0.3,1)] motion-reduce:transition-none bg-accent/70'
)}
aria-hidden="true"
/>
Expand Down
7 changes: 6 additions & 1 deletion apps/web/src/features/command/Launcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,8 @@ export function runCreateAction(
const source = options.source ?? 'create_menu';

switch (blockName) {
case 'agent':
return runCreateAction('chat', options);
case 'md': {
const span = startDocumentSpan('doc.create');
span.setAttr('doc.type', 'md');
Expand Down Expand Up @@ -670,7 +672,10 @@ export function useCreateMenuBlocks(
*/
export function useCreatableEnabled(): (name: CreatableName) => boolean {
const blocks = useCreateMenuBlocks();
return (name) => blocks().some((block) => block.blockName === name);
return (name) =>
blocks().some((block) =>
name === 'agent' ? block.blockName === 'chat' : block.blockName === name
);
}

export const [createMenuOpen, setCreateMenuOpen] = createControlledOpenSignal(
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/features/command/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { Component } from 'solid-js';
* than in `BlockAliasRegistry`, where it would leak into `fileTypeToBlockName`,
* split content types and `NonDocumentBlockTypes`.
*/
export type CreatableName = BlockName | BlockAlias | 'reminder';
export type CreatableName = BlockName | BlockAlias | 'reminder' | 'agent';

export type CreatableBlock = Omit<HotkeyRegistrationOptions, 'scopeId'> & {
label: string;
Expand Down
Loading
Loading