') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); feat(composer): workspace picker, git branch switching, and composer defaults persistence by sunheyi6 · Pull Request #511 · apache/maka · GitHub
Skip to content

feat(composer): workspace picker, git branch switching, and composer defaults persistence - #511

Merged
Astro-Han merged 6 commits into
apache:mainfrom
sunheyi6:feature/composer-workspace
Jul 5, 2026
Merged

feat(composer): workspace picker, git branch switching, and composer defaults persistence#511
Astro-Han merged 6 commits into
apache:mainfrom
sunheyi6:feature/composer-workspace

Conversation

@sunheyi6

Copy link
Copy Markdown
Contributor

Composer Workspace & Git Branch Features

Enhances the composer with workspace picker improvements, git branch management, and persistent defaults.

Changes

  • Workspace picker with recent project directories and branch indicator
  • Git branch listing and checkout from the composer UI
  • Persisted composer defaults to localStorage for reload resilience
  • cwd field on SessionSummary for per-session working directory tracking
  • New Git branch IPC handlers in main process
  • Newcomposer-defaults.ts module
  • NewHistory icon for recent workspaces

@Astro-Han 请审核

@sunheyi6
sunheyi6force-pushed the feature/composer-workspace branch from 78cb43b to 93af488CompareJuly 4, 2026 12:13

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

结论

  • 合并判断:需修后再合并。
  • 主要原因:当前 PR build 直接不过;同时 workspace/project path 有 renderer localStorage、renderer state、main selectedProjectRoot 三套状态,recent workspace 和 reload 恢复不会同步 main,分支列表/切分支可能在用户没看到的旧仓库执行。
  • 还需要的验证:修复后请补行为测试,覆盖 recent workspace、reload 恢复、分支 list/checkout cwd、新建 session cwd 这几条路径必须指向同一个 projectPath。

P0

  • 未发现 P0。

P1 阻塞项

  • P1:PR 混入了 folder/session grouping 代码,但没有带上对应 core/storage/runtime 实现,导致 desktop main build 失败。

    • 证据:apps/desktop/src/main/main.ts 引入并使用 createFolderStoreruntime.setFolderfolder-changesession.folderIdapps/desktop/src/preload/preload.tsapps/desktop/src/global.d.ts 也暴露了 folders:* / sessions:setFolder。但本 PR 没有提供对应的 createFolderStoreSessionFolderSessionManager.setFolder 等实现。
    • 验证:node ../../worktree-bootstrap.mjs 失败在 @maka/desktop build:main,报 @maka/storage 没有导出 createFolderStoreSessionManager 没有 setFolderSessionSummary 没有 folderId
    • 处理:先把 folder 相关改动从 #511 删除;这个 PR 只保留 composer workspace picker、git branch、composer defaults 相关变更。
  • P1:选择 recent workspace 后,UI 显示 A 仓库,但分支列表/切分支仍可能在旧仓库执行,用户可能误切真实仓库分支。

    • 证据:renderer 的 selectRecentProjectDirectory(path) 只调用 app.resolveProjectGitInfo(path)setAppInfo;main 的 app:resolveProjectGitInfo 不设置 selectedProjectRootapp:listGitBranchesapp:checkoutGitBranch 都从 currentProjectRoot() 取目录。
    • 处理:砍掉双状态。把手动选择、recent 选择、reload 恢复统一成 main-side selectProjectRoot / setProjectRoot IPC;成功时由 main 更新 selectedProjectRoot 并返回 projectPath/projectGitresolveProjectGitInfo 要么只做纯查询且不用于选择,要么删除。
    • 验证:构造两个 git repo A/B,先让 main 当前为 B,再从 recent 列表选择 A,打开分支菜单和切分支;断言 git 命令 cwd 是 A,且 UI projectPath、branch、新建 session cwd 全部一致。
  • P1:projectPath 持久化不能可靠跨 reload,启动后 refreshAppInfo() 会用 main 默认路径覆盖 localStorage 种出来的 appInfo,新任务可能跑到错误目录。

    • 证据:app-shell.tsxloadComposerDefaults().projectPath 初始化 appInfo;bootstrap effect 下一帧调用 refreshAppInfo()refreshAppInfo() 直接用 window.maka.app.info() 覆盖 appInfo;main 的 app:info 只读 selectedProjectRoot 或 fallback。
    • 处理:reload 恢复也走同一个 main-side select/set project root。前端不要单独把 localStorage 的 projectPath 当事实源;localStorage 最多保存“上次选择的路径/最近列表”,真正 current project 必须由 main 确认后返回。
    • 验证:localStorage 里有 repo A,main 默认是 repo B,mount 后等待 refreshAppInfo()appInfo.projectPath、新建 session cwd、分支列表 cwd 都仍是 A。
  • P1:Composer 菜单里直接执行 git checkout <branch>,没有 dirty worktree guard/确认,容易改动用户工作区状态。

    • 证据:apps/desktop/src/main/git-branch.tscheckoutBranch() 直接 git checkout branch;renderer 选择菜单项后直接调用 window.maka.app.checkoutGitBranch(branch)
    • 处理:checkout 前先检查 git status --porcelain。dirty 时阻止并提示,或要求用户明确确认;如果 V1 不想扩大交互,也可以先只展示当前分支和分支列表,不做切换。
    • 验证:补 dirty repo 测试,确认未确认时不会调用 checkout。

P2 非阻塞建议

  • P2:显式选择的 recent 路径不存在时会静默 fallback 到 process.cwd(),这会把“路径失效”变成“选错目录”。这是非阻塞建议,但建议这次一起收掉。

    • 证据:resolveProjectRoot([projectPath]) 在候选路径不可用时返回 resolve(process.cwd())app:resolveProjectGitInfo 对显式 projectPath 也走这条路径。
    • 处理:拆出显式路径解析,例如 resolveExplicitProjectRoot(path)。用户明确选择的路径不存在/不可读时返回 ok:false reason:not-found/read-failed,不允许 fallback;fallback 只保留给 app 启动默认探测。
    • 验证:recentProjectPaths 里放一个已删除目录,点击后应 toast 报错并保持当前工作区不变,不应改成 process.cwd()
  • P2:测试没有覆盖真实用户主路径,挡不住上面的 wrong cwd / reload 覆盖回归。这是非阻塞建议,但建议作为本 PR 的回归保护补上。

    • 证据:project-context-badge.test.tscomposer-new-chat-model-picker-contract.test.ts 主要靠源码正则检查 wiring;git-branch.test.ts 只测 git helper,没有测 renderer 选择 project 后 main IPC 使用哪个 cwd。
    • 处理:补最小行为测试或 IPC contract test:选择 recent/default project 后,branch list/checkout 的 cwd 必须等于当前 composer projectPath;选择模型/权限/工作区后,localStorage、UI、sessions.create 参数保持一致。

P3 非阻塞建议

  • P3:git-branch.ts 解析 git branch --list 的人类输出,代码和测试都偏绕。这只是非阻塞简化建议。

    • 处理:可以用 git for-each-ref --format=%(refname:short) refs/heads 列本地分支,current 继续复用 resolveProjectGitInfo(projectRoot).branch;这样可以删除 starred-line / detached parsing 相关分支和脆弱测试。
  • P3:composer-defaults 长期保存绝对 project path、recent paths、model、permissionMode,但现在没有清空入口。这也是非阻塞建议。

    • 处理:至少提供 reset/clear defaults;尤其是 permission mode,避免用户在某个会话切到高权限模式后,后续新任务默默继承。

@likun666661likun666661 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM,but can you resolve some conflicts?

@sunheyi6
sunheyi6force-pushed the feature/composer-workspace branch from 93af488 to abda4bbCompareJuly 4, 2026 22:40
Resolve merge conflicts and address review feedback (PR apache#511):
- Conflict: import path for resolveProjectGitInfo/resolveProjectRoot
changed from './project-context.js' to '@maka/runtime' in main;
keep the new path and add git-branch.js import.
- Fix: make registerIpc() async and await loadLastProjectPath()
before registering IPC handlers, eliminating a reload race that
could cause currentProjectRoot() to fall back to process.cwd().
- Fix: validate path existence in app:selectProjectRoot IPC handler
so stale recent-workspace entries (deleted/moved directories)
return 'not-found' instead of silently falling back to process.cwd().
- Cleanup: remove duplicate cwd field in SessionSummary interface
and its two call sites (headerToSummary, toSummary).
- Update git-branch.ts import to point to @maka/runtime.

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes. The four original P1s are fixed (folder code gone, unified main-side selectProjectRoot, reload restores from last-project-path.json, dirty guard with git status --porcelain), but this PR introduces three new blocking regressions. One is a real safety regression, not a stale test.

P1 — real regression: permissionMode is now persisted and restored (safety).app-shell.tsx changed pendingNewChatPermissionMode from useState<PermissionMode | null>(null) to seed from persistedComposerDefaults?.permissionMode, and composer-defaults.ts now stores permissionMode in localStorage. The contract in session-status-presentation.test.ts is deliberate: the no-session permission pick is renderer-only, starts at null, is sent once on session creation, then cleared — and main.ts (Settings → 通用 default) is the single authority. Persisting it means a user who switches to a higher permission mode (e.g. auto-edit) in one session will have that mode silently inherited by new sessions after a restart, with no visible signal. This is exactly the footgun the null-initial design avoided.

Revert pendingNewChatPermissionMode to useState<PermissionMode | null>(null) and drop permissionMode from composer-defaults. Project-path persistence is fine and should stay; permission mode should not cross sessions.

P1 — project-context-badge.test.ts regression.composer.tsx's workspacePicker props structure changed, so the source-regex contract no longer matches (it expects disabled={props.workspacePicker.pending === true}; the field shape is now different). This test passes on main and fails on this branch. Either update the regex to the new shape or, preferably, convert it to a behavior test (the earlier P2 note asked for less source-regex reliance).

P1 — CSS leading-token governance.composer.css uses bare numbers / px / normal for line-height, which violates the --leading-* whitelist (PR-LEADING-CONVERGE-0 contract). This also passes on main. Use the --leading-* tokens (or em / literals) instead of raw values.

P2 — non-blocking: dirty guard has no test.checkoutBranch's git status --porcelain dirty check is the safety fix for the original P1, but git-branch.test.ts has 10 cases and none cover the dirty-worktree refusal. Add a case that fakes a non-empty git status output and asserts reason: 'dirty' with no git checkout call.

What landed well: the folder/session-grouping code is fully removed; selectProjectRoot validates the path with stat (no silent process.cwd() fallback on a stale recent entry — also closes the earlier P2); listGitBranches / checkoutGitBranch both resolve cwd through currentProjectRoot(); branch names are injection-guarded and HEAD is re-read after checkout.

- P1: Remove permissionMode from composer-defaults persistence.
pendingNewChatPermissionMode now starts from null (never seeds from
localStorage) to avoid silently inheriting a high-privilege mode
across restarts.
- P1: Update project-context-badge.test.ts regex to match the current
workspacePicker render (wp alias instead of props.workspacePicker).
- P1: Replace bare line-height: 1.25 with var(--leading-tight) in
composer.css to comply with PR-LEADING-CONVERGE-0 governance.
- P2: Add dirty-worktree guard test for checkoutBranch: fakes
non-empty git status --porcelain and asserts reason: 'dirty' with
no checkout call.
@sunheyi6
sunheyi6 requested a review from Astro-HanJuly 5, 2026 07:43

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current head db98f721 still needs changes.

Verified locally:

  • npm run -w @maka/desktop build passes.
  • npm run typecheck fails:
    • src/renderer/app-shell-session-settings-actions.ts(58,30): permissionMode is not in Partial<ComposerDefaults>.
    • src/renderer/app-shell-session-settings-actions.ts(81,30): same issue.
  • npm run -w @maka/desktop test fails 2 tests:
    • composer-new-chat-model-picker-contract.test.js still expects the unsafe persisted permission-mode default.
    • composer.css still has a raw hover background (oklch(from var(--foreground) l c h / 0.06)) instead of the state token.
  • git-branch.test.js passes 11/11, but it still has no status-error / timeout coverage for the checkout dirty guard.

P1: permission-mode cleanup is incomplete and the branch does not typecheck

ComposerDefaults no longer includes permissionMode, which is the right direction for the earlier security issue, but two callers still write it:

  • apps/desktop/src/renderer/app-shell-session-settings-actions.ts:58
  • apps/desktop/src/renderer/app-shell-session-settings-actions.ts:81

That leaves the branch untypecheckable. The related contract test is also stale: it still asserts that pendingNewChatPermissionMode is seeded from persistedComposerDefaults?.permissionMode, which is the behavior we do not want for the no-session permission picker. Remove the saveComposerDefaults({ permissionMode: mode }) writes and update the contract to lock the renderer-only/null initial state instead.

P1: IPC registration can race the first renderer IPC calls

registerIpc() is now async and awaits readFile(last-project-path.json) before registering any handlers. Startup still does:

voidregisterIpc();app.whenReady().then(async()=>{constbackgroundStartup=runBackgroundStartup();awaitmainWindowController.createWindow();awaitbackgroundStartup;});

If that file read is delayed, the first window can mount before app:info and the other handlers in registerIpc() exist. This can fail the cold-start path with a missing IPC handler. Either await an ipcReady promise before the first createWindow(), or keep handler registration synchronous and make currentProjectRoot() await a persisted-path promise internally.

P1: checkoutBranch still fails open when git status --porcelain errors

The dirty guard only reads stdout:

const{stdout: statusOutput}=awaitrunGit(...);if(statusOutput.trim())returndirty;

If git status --porcelain fails, times out, or cannot run but returns empty stdout, the code still proceeds to git checkout. Status failures must fail closed: read { stdout, stderr, error }, return timeout / missing-git / failed when error is set, and only run checkout after a successful clean status. Add targeted tests for status error/timeout not invoking checkout; the current dirty-stdout test is not enough.

P1: desktop tests still fail on the composer hover token

apps/desktop/src/renderer/styles/composer.css has:

.maka-composer-branch-picker:hover {
background:oklch(from var(--foreground) l c h /0.06);
}

The state-token contract rejects raw hover backgrounds; the neighboring workspace picker already uses var(--state-hover-bg). Use the state token here too so the desktop suite stays green.

P2: restored last-project-path is not validated

Startup restores parsed.projectPath directly into selectedProjectRoot, and currentProjectRoot() returns it without stat/resolve validation. Only explicit selectProjectRoot validates the path. If the persisted directory was deleted or moved, app.info, new-session cwd, branch list, and checkout can all operate on a stale path. Load the persisted path through the same validation path as explicit selection; clear it or fall back when invalid.

P2: workspace-instructions IPC still uses process.cwd()

workspaceInstructions:getState, workspaceInstructions:openFile, and workspaceInstructions:createFile still resolve files under process.cwd(). After the user selects project A from a launcher cwd B, project-specific instructions still read/write B. These handlers should use await currentProjectRoot() unless the UI is intentionally showing app-launch-directory instructions.

P3 notes

  • There are still two current-project persistence paths: main last-project-path.json and renderer composer-defaults.projectPath. Main should own the current project path; renderer can keep only recent paths and UI defaults.
  • project-context-badge.test.ts still relies heavily on source-regex wiring checks. A behavior-level test would better cover the badge, picker, selection result, and IPC parameters, though this repo currently has limited renderer mounting support.
  • listLocalBranches still parses git branch --list human output. git for-each-ref --format=%(refname:short) refs/heads plus resolveProjectGitInfo(projectRoot).branch would remove the starred-line / detached-HEAD parser surface.

@sunheyi6
sunheyi6 requested a review from Astro-HanJuly 5, 2026 09:42

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current head cf0c5be6 fixes the earlier IPC-registration race and the git status --porcelain fail-open path. workspaceInstructions:* now uses currentProjectRoot(), and the persisted last-project-path load no longer blindly installs a deleted path. Those parts look resolved.

This still needs changes because session creation can still fall back to the app launch directory instead of the selected project.

P1: new sessions still default to process.cwd()

main.ts still has:

ipcMain.handle('sessions:create',async(_event,input?: Partial<CreateSessionInput>)=>{constcwd=input?.cwd??process.cwd();

Quick Chat has the same problem:

returnruntime.createSession({cwd: process.cwd(),

The renderer only passes cwd when its projectPath prop is truthy:

...(projectPath ? {cwd: projectPath} : {}),

That leaves a real cold-start / stale-state path: if appInfo has not refreshed yet, localStorage is empty, localStorage has an old project, or any renderer caller omits cwd, the session is created in the Electron launch directory. The session header then persists that cwd, and the system prompt / workspace-instruction context follows it. Main already owns the current project through currentProjectRoot(), so session creation should use that as the default, not process.cwd().

Minimum fix: in main, use input?.cwd ?? await currentProjectRoot() for normal sessions:create, and use await currentProjectRoot() for Quick Chat creation as well. The cleaner follow-up is to stop having the renderer decide new-session cwd at all: remove the cwd forwarding from normal renderer sessions.create and let main be the single authority for the current project.

P1: focus-ring CSS contract still fails

Full desktop test still has one failure from composer.css:

.maka-composer-branch-picker:focus-visible {
box-shadow:0003pxoklch(from var(--focus-ring) l c h /0.14);
}

Use the tokenized width:

box-shadow:000var(--focus-ring-width) oklch(from var(--focus-ring) l c h /0.14);

P2: app:resolveProjectGitInfo still falls back on bad explicit paths

app:resolveProjectGitInfo still does:

constresolved=typeofprojectPath==='string'
? awaitresolveProjectRoot([projectPath])
: awaitcurrentProjectRoot();

For an explicit user-supplied path, this should not silently fall back to process.cwd() when the path is deleted or invalid. If this IPC remains, stat / validate the explicit path first and return a typed failure (not-found / invalid-path) instead of returning an unrelated project root.

P3 notes

  • Current project path is still persisted in two places: main last-project-path.json and renderer composer-defaults.projectPath. Main should own the current project; renderer can keep recent paths and model UI defaults.
  • project-context-badge.test.ts and composer-new-chat-model-picker-contract.test.ts still rely heavily on source-regex wiring checks. A behavior test for workspace selection / first-frame send / recent selection would be stronger, though I realize the repo has limited renderer-mount support today.
  • listLocalBranches still parses git branch --list human output. git for-each-ref --format=%(refname:short) refs/heads plus resolveProjectGitInfo(projectRoot).branch would remove the starred-line / detached parser surface.

Local verification context from the merged current-main + PR check:

  • desktop build: pass
  • typecheck: pass
  • focused git branch / permission mode / project badge tests: pass
  • full desktop test: one failure, the focus-ring token above

@sunheyi6
sunheyi6 requested a review from Astro-HanJuly 5, 2026 11:33
Bot-created sessions used process.cwd(), so a bot message arriving while
the user had project A selected could start the session in the app launch
directory instead. Rename the BotIncomingMainServiceDeps.cwd() sync string
to getCurrentProjectRoot(): Promise<string>, await it at the createSession
call site, and wire main.ts to the current project root resolver.
The resolver is defined inside registerIpc(), so a module-level provider is
reassigned from registerIpc once the resolver exists; the launch directory
remains the safe fallback until then. Unifying project-root resolution
across main is tracked as a follow-up.

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up fix pushed (b9a43e50): bot-incoming sessions now use the current project root instead of process.cwd(). Renamed BotIncomingMainServiceDeps.cwd() to getCurrentProjectRoot(): Promise<string> and await it at the createSession call site. Behavior test asserts the createSession cwd follows the injected resolver rather than process.cwd(). typecheck, build, and full desktop suite (2056/0) green. The resolver lives inside registerIpc(), so a module-level provider is reassigned from registerIpc once the resolver exists (launch dir stays the safe fallback); unifying project-root resolution across main is noted as a follow-up.

@Astro-Han
Astro-Han merged commit a513887 into apache:mainJul 5, 2026
Astro-Han added a commit that referenced this pull request Jul 5, 2026
#520 PR9 review P3-1: the Table primitive had a single consumer
(usage-settings-page SimpleStatsTable) and no second HTML <table> consumer
in sight — the model "table" is a div-based list (.modelTable), not an HTML
table, so it will never migrate onto Table. Per Occam's razor, lift the
primitive only when a second real consumer appears.
- Delete packages/ui/src/primitives/table.tsx and its barrel export.
- SimpleStatsTable in usage-settings-page now renders a native HTML
<table>/<thead>/<tbody>/<tr>/<th>/<td> with the same Tailwind classes the
primitive applied (w-full border-collapse rounded-[var(--radius-surface)]
border border-border text-caption + cell border-b/px/py/align/
tabular-nums). Styles stay inline so the stats surface is self-contained
until a second HTML <table> consumer justifies lifting it back.
- Rename card-table-converge-contract → card-converge-contract: drop the
Table data-slot / table-sites assertions, keep Card. Note the usage stats
table a11y semantics (aria-label + scope) now live in
settings-usage-contract, whose assertions are updated to match the native
<table>/<th scope> shape.
- settings-form-a11y and tabular-nums are untouched: they assert on the
.modelTable CSS class (a div surface), not on the Table primitive.
Verification: 2049/2049 contract pass (one fewer test — the deleted
"table sites import Table" case), typecheck clean. Screenshot pixel diff vs
main: settings-general light/dark 1280 AE=0; settings-data 1280 shows ~4500
px (RMSE 0.001) in the `data` section, which is main-branch composer/sidebar
drift (#510/#511), not this change — the usage stats table lives in the
`usage` section and its styles are identical to the retired primitive.
Astro-Han added a commit that referenced this pull request Jul 5, 2026
…PR9) (#554)
* feat(ui): add Card/Table primitives, retire settings card/table classes (#520 PR9)
Card (packages/ui/src/primitives/card.tsx) — thin surface container
(data-slot="card" + radius-surface), following maka's ChoiceCard
philosophy: each call site keeps its own layout/visual CSS. settingsRows,
settingsMetricCard, and maka-error-card now route through Card; their CSS
drops the border-radius line (Card owns it), layout is byte-identical.
Table (packages/ui/src/primitives/table.tsx) — shadcn-style family
(Table/TableHeader/TableBody/TableRow/TableHead/TableCell) with data-slot.
settingsStatsTable retires entirely; the table chrome (border + radius +
caption font-size) and the cell chrome (tabular-nums + hairline row
separators + caption-tone color + semibold head) move into the primitive.
maka-error-card stays on Card (not Alert): it is a large crash surface
with shadow-modal + stack <pre>, not a small inline callout.
card-table-converge-contract locks the migration. Updated four existing
contracts whose selectors pinned the retired classes/structure:
radius-converge (drop .settingsRows tier — Card owns it now),
tabular-nums (drop .settingsStatsTable th/td — Table owns it now),
settings-usage (regex now matches the Table family + scoped heads/cells),
web-search-boundary (regex now matches <SettingsRows className=…>).
Token values verified equivalent: --font-weight-semibold=600 (font-semibold),
--space-1/2, --radius-surface=8px. Screenshot manifest passes (32/32, 0 fail).
* feat(ui): converge badge surfaces onto pill Badge + squared Chip
#520 PR9 commit 2: collapse the four coexisting badge surfaces onto two
canonical primitives, split by UI role.
- pill Badge (primitives/badge.tsx): emphasis markers. Retire the
PrimitiveBadge alias + the legacy ui.tsx Badge (raw emerald/amber
variants). health/permission center, artifact-pane, plan-reminder, and
permission-dialog route through <Badge>.
- squared Chip (primitives/chip.tsx): dense status rows. Retire the
.settingsBadge + .settingsConnectionBadge CSS chips. settings
connection status / default / category markers route through <Chip>;
variants mirror StatusTone, so settings callers pass the tone directly
instead of going through statusBadgeVariant (which stays for the
health/permission pill Badge sites).
- contracts: badge-converge (pill track), chip-converge (squared track,
locks radius-control not pill), settings-form-a11y (lock point moved
from the CSS class to the Chip primitive), radius-converge (drop the
stale ui.tsx badgeVariants entry).
- visual zero-change: Chip cva reproduces the retired CSS oklch alphas
(success/12, info/14, warning/18, destructive/15) and the neutral
foreground-5 base. screenshot pixel diff vs main: settings-bots
light/dark 1280 AE=0.
* fix(ui): address PR9 review — Chip size=sm for settingsBadge, token contract, comment fix#520 PR9 review fixes:
- P2: the three .settingsBadge migration sites (provider-connection-detail
x2, provider-add-form x1) now use Chip size="sm" to reproduce the retired
.settingsBadge geometry (18px / font-normal / 0-6px padding). Without
size="sm" they defaulted to the larger status-row size (20px / semibold /
2-8px padding) and drifted. The neutral background did not drift:
bg-secondary aliases --color-secondary = var(--foreground-5), so only
height/weight/padding moved.
- P3-2: chip-converge-contract now locks user-visible tokens (neutral
bg-secondary + foreground-secondary text, sm/default size geometry,
status-tone alphas /12 /14 /18 /15) so a cva class change that keeps the
import is still caught. Dropped the "no old span" migration-narrative
loop; kept import + role-split + token assertions.
- P3-3: bot.css / connection.css retire comments now say Chip
variant="neutral" (not Badge variant="secondary"), matching the actual
migration and not steering future migrants back to the pill Badge.
Verification: 2050/2050 contract pass, typecheck clean, screenshot
settings-bots light/dark 1280 + light 990 all AE=0 (pixel-identical to
main after the size=sm fix; the 990 variant's earlier 13380 px diff is gone).
* refactor(ui): retire premature Table primitive per review (#520 PR9 P3)
#520 PR9 review P3-1: the Table primitive had a single consumer
(usage-settings-page SimpleStatsTable) and no second HTML <table> consumer
in sight — the model "table" is a div-based list (.modelTable), not an HTML
table, so it will never migrate onto Table. Per Occam's razor, lift the
primitive only when a second real consumer appears.
- Delete packages/ui/src/primitives/table.tsx and its barrel export.
- SimpleStatsTable in usage-settings-page now renders a native HTML
<table>/<thead>/<tbody>/<tr>/<th>/<td> with the same Tailwind classes the
primitive applied (w-full border-collapse rounded-[var(--radius-surface)]
border border-border text-caption + cell border-b/px/py/align/
tabular-nums). Styles stay inline so the stats surface is self-contained
until a second HTML <table> consumer justifies lifting it back.
- Rename card-table-converge-contract → card-converge-contract: drop the
Table data-slot / table-sites assertions, keep Card. Note the usage stats
table a11y semantics (aria-label + scope) now live in
settings-usage-contract, whose assertions are updated to match the native
<table>/<th scope> shape.
- settings-form-a11y and tabular-nums are untouched: they assert on the
.modelTable CSS class (a div surface), not on the Table primitive.
Verification: 2049/2049 contract pass (one fewer test — the deleted
"table sites import Table" case), typecheck clean. Screenshot pixel diff vs
main: settings-general light/dark 1280 AE=0; settings-data 1280 shows ~4500
px (RMSE 0.001) in the `data` section, which is main-branch composer/sidebar
drift (#510/#511), not this change — the usage stats table lives in the
`usage` section and its styles are identical to the retired primitive.
* docs(css): fix .settingsStatsTable retire comment to point at local SimpleStatsTable
Per PR9 review P3: the retire comment still referenced the deleted
packages/ui/src/primitives/table.tsx, which would mislead a future maintainer
into restoring a public Table primitive. Now points at the local native
SimpleStatsTable in usage-settings-page.tsx, matching the actual state.
Astro-Han added a commit that referenced this pull request Jul 6, 2026
… Input (#520 item 22) (#555)
* feat(ui): canonical Input/Textarea onto Base UI, retire native ui.tsx Input (#22 PR10)
#22 PR10: collapse the dual Input tracks onto one canonical
primitives/input.tsx + primitives/textarea.tsx, retiring the native ui.tsx
Input/Textarea.
- primitives/input.tsx now wraps Base UI's Input primitive with maka's
inputClasses styling ported as the default chrome. Stays a single <input>
(no span wrapper) so caller CSS targeting `> input` / `input:focus-visible`
still matches. unstyled gives the bare form (bareFieldClasses +
data-maka-field-chrome="none") for Field/InputGroup embedding.
- primitives/textarea.tsx parallel shape: single <textarea> + inputClasses +
textarea sizing. Base UI ships no Textarea, so this is a native textarea.
Drops the Base UI Field.Control + span wrapper (caller doesn't use Field).
- ui.tsx Input, Textarea, inputClasses, bareFieldClasses retired. inputClasses +
bareFieldClasses moved into primitives/input.tsx (exported) so number-field
keeps its standalone NumberFieldInput chrome.
- InputGroup: InputGroupInput/InputGroupTextarea already pass unstyled (inner
field bare, InputGroup owns the chrome — no double chrome). InputGroup CSS
updated to key off data-slot="input"/"textarea" (the single element) instead
of the retired span data-slot="input-control"/"textarea-control"; dropped the
span-only `:contents` / `:before:hidden` rules.
- index.ts re-exports primitives/input + primitives/textarea as the canonical
Input/Textarea; the 44 usages across 9 settings files keep importing from
@maka/ui unchanged.
- Contracts: new input-canonical-contract (single element, inputClasses token
lock, ui.tsx retired, bare-field shape); field-chrome.test rewritten for the
unified shape (drops nativeInput/h-8.5/field-sizing-content assertions that
pinned the old primitives/input vs ui.tsx divergence); radius-converge
inputClasses tier repointed at primitives/input.tsx.
Verification: typecheck clean; @maka/ui test 43/43; @maka/desktop test
2069/2069. Screenshot pixel diff vs main: settings-general and settings-bots
light/dark 1280 AE=0 (pixel-identical, both contain Input). settings-data
364 px (edge); settings-appearance/daily-review ~9-10K px in a 12px right-edge
column (window edge, not input/textarea); settings-memory ~18K px in a large
content block (main-branch composer/sidebar commits #510/#511, not this PR).
* fix(ui): PR10 review — InputGroup force-bare, search reset, behavior contract
- P2-1: InputGroupInput/Textarea spread props before unstyled and
data-maka-field-chrome so a caller's unstyled={false} cannot re-enable
the inner chrome (would double the border / focus ring). The InputGroup
owns the chrome; the inner control stays bare.
- P2-2: port type="search" WebKit cancel/decoration/results reset into
primitives/input. Main's primitives/input always applied this for
InputGroupInput (search-modal path); the unified Input keeps the
contract for both the @maka/ui Input and InputGroupInput paths.
size/nativeInput/type=file variants audited — no callers in repo
(@maka/ui Input was ui.tsx without these; InputGroupInput never
passed them); removed without a compat layer.
- P3-1: rewrite input-canonical-contract as behavior tests only — no
source regex. Styled/unstyled single element + chrome, InputGroup
force-bare even with unstyled={false}, type="search" webkit reset,
barrel import smoke.
- P3-2: #22 PR10 -> #520 item 22 in comments (the PR流水号 narrative
pointed at the wrong tracker).
* fix(ui): drop redundant data-maka-field-chrome on InputGroup adapters
The canonical Input/Textarea already force data-maka-field-chrome="none"
when unstyled (the {...props} spread precedes the attribute in both
primitives, so a caller cannot override it). The adapter passing it
explicitly was a no-op. Drop the redundant attribute; keep unstyled.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@sunheyi6@likun666661@Astro-Han