Uh oh!
There was an error while loading. Please reload this page.
feat(composer): workspace picker, git branch switching, and composer defaults persistence - #511
Conversation
78cb43b to
93af488Compare
Astro-Han
left a comment
There was a problem hiding this comment.
结论
- 合并判断:需修后再合并。
- 主要原因:当前 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引入并使用createFolderStore、runtime.setFolder、folder-change、session.folderId;apps/desktop/src/preload/preload.ts和apps/desktop/src/global.d.ts也暴露了folders:*/sessions:setFolder。但本 PR 没有提供对应的createFolderStore、SessionFolder、SessionManager.setFolder等实现。 - 验证:
node ../../worktree-bootstrap.mjs失败在@maka/desktop build:main,报@maka/storage没有导出createFolderStore,SessionManager没有setFolder,SessionSummary没有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不设置selectedProjectRoot;app:listGitBranches和app:checkoutGitBranch都从currentProjectRoot()取目录。 - 处理:砍掉双状态。把手动选择、recent 选择、reload 恢复统一成 main-side
selectProjectRoot/setProjectRootIPC;成功时由 main 更新selectedProjectRoot并返回projectPath/projectGit。resolveProjectGitInfo要么只做纯查询且不用于选择,要么删除。 - 验证:构造两个 git repo A/B,先让 main 当前为 B,再从 recent 列表选择 A,打开分支菜单和切分支;断言 git 命令 cwd 是 A,且 UI projectPath、branch、新建 session cwd 全部一致。
- 证据:renderer 的
P1:
projectPath持久化不能可靠跨 reload,启动后refreshAppInfo()会用 main 默认路径覆盖 localStorage 种出来的appInfo,新任务可能跑到错误目录。- 证据:
app-shell.tsx用loadComposerDefaults().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.ts的checkoutBranch()直接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.ts和composer-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,避免用户在某个会话切到高权限模式后,后续新任务默默继承。
likun666661
left a comment
There was a problem hiding this comment.
LGTM,but can you resolve some conflicts?
…switching, and composer defaults persistence
93af488 to
abda4bbCompareResolve 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-Han
left a comment
There was a problem hiding this comment.
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.
Astro-Han
left a comment
There was a problem hiding this comment.
Current head db98f721 still needs changes.
Verified locally:
npm run -w @maka/desktop buildpasses.npm run typecheckfails:src/renderer/app-shell-session-settings-actions.ts(58,30):permissionModeis not inPartial<ComposerDefaults>.src/renderer/app-shell-session-settings-actions.ts(81,30): same issue.
npm run -w @maka/desktop testfails 2 tests:composer-new-chat-model-picker-contract.test.jsstill expects the unsafe persisted permission-mode default.composer.cssstill has a raw hover background (oklch(from var(--foreground) l c h / 0.06)) instead of the state token.
git-branch.test.jspasses 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:58apps/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.jsonand renderercomposer-defaults.projectPath. Main should own the current project path; renderer can keep only recent paths and UI defaults. project-context-badge.test.tsstill 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.listLocalBranchesstill parsesgit branch --listhuman output.git for-each-ref --format=%(refname:short) refs/headsplusresolveProjectGitInfo(projectRoot).branchwould remove the starred-line / detached-HEAD parser surface.
Astro-Han
left a comment
There was a problem hiding this comment.
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.jsonand renderercomposer-defaults.projectPath. Main should own the current project; renderer can keep recent paths and model UI defaults. project-context-badge.test.tsandcomposer-new-chat-model-picker-contract.test.tsstill 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.listLocalBranchesstill parsesgit branch --listhuman output.git for-each-ref --format=%(refname:short) refs/headsplusresolveProjectGitInfo(projectRoot).branchwould 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
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-Han
left a comment
There was a problem hiding this comment.
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.
#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.
…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.
… 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.
Composer Workspace & Git Branch Features
Enhances the composer with workspace picker improvements, git branch management, and persistent defaults.
Changes
cwdfield onSessionSummaryfor per-session working directory trackingcomposer-defaults.tsmoduleHistoryicon for recent workspaces@Astro-Han 请审核