From cfa91a216a4f9f0bdd8548013dd5bc91950ca392 Mon Sep 17 00:00:00 2001 From: meh Date: Sun, 2 Aug 2026 10:30:43 +0700 Subject: [PATCH 1/2] feat: integrate prompt authoring and project controls --- Cargo.lock | 43 +++- Cargo.toml | 11 + apps/gui/Cargo.toml | 10 +- apps/gui/frontend/bun.lock | 7 +- apps/gui/frontend/package.json | 3 +- apps/gui/frontend/src/api/client.ts | 11 +- apps/gui/frontend/src/api/index.ts | 4 +- apps/gui/frontend/src/api/mock.ts | 20 +- apps/gui/frontend/src/api/tauri.ts | 4 +- .../frontend/src/features/home/HomeTab.tsx | 13 +- .../src/features/project/Composer.tsx | 72 +++++- .../src/features/project/MessageBody.test.tsx | 8 + .../src/features/project/MessageBody.tsx | 15 +- .../src/features/project/ProjectPanel.tsx | 67 ++++- .../src/features/project/ProjectTab.tsx | 19 +- .../src/features/settings/SettingsTab.tsx | 119 ++------- .../src/features/shell/CloseConfirm.tsx | 6 +- .../src/features/shell/useAppShell.ts | 17 +- .../frontend/src/features/tabs/TabStrip.tsx | 52 +++- apps/gui/frontend/src/lib/labels.test.ts | 20 ++ apps/gui/frontend/src/lib/labels.ts | 10 +- .../gui/frontend/src/lib/promptEditor.test.ts | 48 ++++ apps/gui/frontend/src/lib/promptEditor.ts | 95 +++++++ apps/gui/frontend/src/lib/stats.test.ts | 34 +++ apps/gui/frontend/src/lib/stats.ts | 35 ++- apps/gui/frontend/src/stores/prefs.ts | 2 + apps/gui/frontend/src/stores/workspace.tsx | 19 +- apps/gui/frontend/src/types/index.ts | 13 +- apps/gui/src/directives.rs | 234 ++++++++++-------- apps/gui/src/experimental.rs | 115 +-------- apps/gui/src/main.rs | 25 +- apps/gui/src/projects.rs | 121 ++++++++- apps/gui/src/settings.rs | 76 +++++- apps/gui/src/tasks.rs | 23 +- docs/ps-capability.yaml | 2 + docs/token-optimizations.md | 7 +- 36 files changed, 920 insertions(+), 460 deletions(-) create mode 100644 apps/gui/frontend/src/lib/labels.test.ts create mode 100644 apps/gui/frontend/src/lib/promptEditor.test.ts create mode 100644 apps/gui/frontend/src/lib/promptEditor.ts diff --git a/Cargo.lock b/Cargo.lock index aac60d9..f1340f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,9 +23,9 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "agent-abstraction" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b7ac55a3ddbf3470033f034c2e6ea14d15a107bc4fc205c8ea380b97f250106" +checksum = "7e63f12e036c673c2189b86fe6176e10d75505de7a570444c9d77e6ae706ccae" dependencies = [ "libc", "serde", @@ -38,7 +38,7 @@ dependencies = [ [[package]] name = "agent-experimental" version = "0.1.0" -source = "git+https://gitlab.com/anonymous-28282828282/agent-experimental.git?rev=df144f229d8ccd510deb17a866c9724337294e42#df144f229d8ccd510deb17a866c9724337294e42" +source = "git+https://gitlab.com/anonymous-28282828282/agent-experimental.git?rev=4d7b738c5bf481798313751de5cc507469c9d2c2#4d7b738c5bf481798313751de5cc507469c9d2c2" dependencies = [ "anyhow", "reqwest", @@ -46,6 +46,7 @@ dependencies = [ "serde", "serde_json", "tempfile", + "tokio", ] [[package]] @@ -182,8 +183,8 @@ dependencies = [ "derive_more 2.1.1", "eyre", "futures", + "promptsyntax", "rkyv 0.8.17", - "security-framework", "serde", "serde_json", "tauri", @@ -1054,7 +1055,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1901,7 +1902,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2822,6 +2823,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "promptsyntax" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65d44b1448a1cdc9c8398f9089ef1af49db3a564dcfd9f503ab15017a8f8c1d" +dependencies = [ + "serde", + "unicode-ident", + "unicode-normalization", +] + [[package]] name = "psc-nanoid" version = "3.1.1" @@ -3213,7 +3225,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3269,7 +3281,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -4232,10 +4244,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -4687,6 +4699,15 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" version = "1.13.3" @@ -5021,7 +5042,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index df99a3c..3f0159a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,17 @@ az-core = { path = "crates/core" } # Heavy deps are declared per-crate on purpose: only apps/gui pulls in the # Tauri stack, so agent/proxy builds never trigger a webview toolchain build. +[profile.dev] +# GUI diagnostics come from logs and backtraces. Generating full symbol tables +# for WebKit and Tauri dependencies dominated clean local check time. +debug = 0 +incremental = true + +[profile.test] +# Tests need assertions and line-bearing panic output, not debugger symbols. +debug = 0 +incremental = true + [profile.release] strip = true # Release runners retain `target` through rust-cache. Incremental artifacts make diff --git a/apps/gui/Cargo.toml b/apps/gui/Cargo.toml index eee7b6d..548c12d 100644 --- a/apps/gui/Cargo.toml +++ b/apps/gui/Cargo.toml @@ -7,7 +7,7 @@ publish.workspace = true [features] default = [] -experimental = ["dep:agent-experimental", "dep:security-framework"] +experimental = ["dep:agent-experimental"] [build-dependencies] tauri-build = { version = "2", features = [] } @@ -15,8 +15,9 @@ tauri-build = { version = "2", features = [] } [dependencies] # Carries the store forward when a column changes; see crates/wt-migrate. wt-migrate = { path = "../../crates/wt-migrate" } -agent-abstraction = "0.4.5" -agent-experimental = { git = "https://gitlab.com/anonymous-28282828282/agent-experimental.git", rev = "df144f229d8ccd510deb17a866c9724337294e42", default-features = false, optional = true } +agent-abstraction = "0.4.6" +agent-experimental = { git = "https://gitlab.com/anonymous-28282828282/agent-experimental.git", rev = "4d7b738c5bf481798313751de5cc507469c9d2c2", default-features = false, optional = true } +promptsyntax = "0.1.0" az-core.workspace = true worktable = "0.9" # The `worktable!` macro emits code that names these by bare path rather than @@ -36,9 +37,6 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" tauri-plugin-dialog = "2" -[target.'cfg(target_os = "macos")'.dependencies] -security-framework = { version = "3.7", optional = true } - [dev-dependencies] tokio = { version = "1", features = ["macros", "rt"] } diff --git a/apps/gui/frontend/bun.lock b/apps/gui/frontend/bun.lock index d4bae07..cc22cdf 100644 --- a/apps/gui/frontend/bun.lock +++ b/apps/gui/frontend/bun.lock @@ -10,6 +10,7 @@ "@tauri-apps/api": "^2.1.1", "clsx": "^2.1.1", "popmotion": "^11.0.5", + "promptsyntax": "^0.1.0", "solid-js": "^1.9.5", "tailwind-merge": "^3.6.0", }, @@ -507,7 +508,7 @@ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - "commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], + "commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], @@ -859,6 +860,8 @@ "pretty-hrtime": ["pretty-hrtime@1.0.3", "", {}, "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A=="], + "promptsyntax": ["promptsyntax@0.1.0", "", {}, "sha512-96orvq3qU7OspMNyOyOh0H3JzCai0rrpdAUoT1PYBhjkeg2KPqNNKo3f4uZqrtvhvJok2gDO3yWtp7a5w8HMoQ=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], @@ -1097,7 +1100,7 @@ "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + "svgo/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], "webpack/schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="], diff --git a/apps/gui/frontend/package.json b/apps/gui/frontend/package.json index 2d132cc..0440887 100644 --- a/apps/gui/frontend/package.json +++ b/apps/gui/frontend/package.json @@ -1,7 +1,7 @@ { "name": "az-gui-frontend", "version": "0.1.0", - "description": "AgencyZero \u2014 Tauri GUI frontend", + "description": "AgencyZero — Tauri GUI frontend", "type": "module", "private": true, "scripts": { @@ -23,6 +23,7 @@ "@tauri-apps/api": "^2.1.1", "clsx": "^2.1.1", "popmotion": "^11.0.5", + "promptsyntax": "^0.1.0", "solid-js": "^1.9.5", "tailwind-merge": "^3.6.0" }, diff --git a/apps/gui/frontend/src/api/client.ts b/apps/gui/frontend/src/api/client.ts index bcf42b3..305e5a7 100644 --- a/apps/gui/frontend/src/api/client.ts +++ b/apps/gui/frontend/src/api/client.ts @@ -5,7 +5,6 @@ import type { AgentStatus, AvailableUpdate, BuildInfo, - ClaudeTokenStatus, ClaudeUsage, CostSummary, CreatedProject, @@ -81,6 +80,8 @@ export interface AgencyZeroApi { setItemStatus(id: string, status: ProjectStatus): Promise; /** Rewrite one item's title, for the panel's inline edit. */ updateItem(id: string, title: string): Promise; + /** Associate the item with a validated GitHub issue URL. */ + setItemIssue(id: string, url: string): Promise; reorderItems(projectId: string, ids: string[]): Promise; // — Pull requests ———————————————————————————————————————————— @@ -109,13 +110,7 @@ export interface AgencyZeroApi { // — Settings ———————————————————————————————————————————————— getSettings(): Promise; setSettings(patch: DeepPartial): Promise; - /** Experimental profile only. Reports presence without returning the token. */ - claudeTokenStatus(): Promise; - /** Experimental profile only. Saves the token directly into macOS Keychain. */ - setClaudeToken(token: string): Promise; - /** Experimental profile only. Removes the token from macOS Keychain. */ - removeClaudeToken(): Promise; - /** Experimental profile only. Fetches current Claude subscription usage. */ + /** Experimental profile only. Fetches usage through Claude Code's managed login. */ claudeUsage(): Promise; /** Probes the installed CLIs. `recheck` forces a fresh probe. */ listAgentStatus(recheck: boolean): Promise; diff --git a/apps/gui/frontend/src/api/index.ts b/apps/gui/frontend/src/api/index.ts index 2d16774..44aecf6 100644 --- a/apps/gui/frontend/src/api/index.ts +++ b/apps/gui/frontend/src/api/index.ts @@ -31,6 +31,7 @@ const COMMAND_FOR: Partial> = { deleteItem: "delete_item", setItemStatus: "set_item_status", updateItem: "update_item", + setItemIssue: "set_item_issue", reorderItems: "reorder_items", listPullRequests: "list_pull_requests", dismissPullRequest: "dismiss_pull_request", @@ -40,9 +41,6 @@ const COMMAND_FOR: Partial> = { resolveModeration: "resolve_moderation", getSettings: "get_settings", setSettings: "set_settings", - claudeTokenStatus: "claude_token_status", - setClaudeToken: "set_claude_token", - removeClaudeToken: "remove_claude_token", claudeUsage: "claude_usage", listAgentStatus: "list_agent_status", listModels: "list_models", diff --git a/apps/gui/frontend/src/api/mock.ts b/apps/gui/frontend/src/api/mock.ts index 7a31964..56d9f8c 100644 --- a/apps/gui/frontend/src/api/mock.ts +++ b/apps/gui/frontend/src/api/mock.ts @@ -83,7 +83,6 @@ export function createMockApi(): AgencyZeroApi { const agentStatus = clone(fixtures.AGENT_STATUS); const models = clone(fixtures.MODEL_CATALOGUE); let settings = clone(fixtures.SETTINGS); - let claudeTokenConfigured = false; const pullRequests = clone(fixtures.PULL_REQUESTS); /* * What a project's agent kept across a compaction, per project. @@ -328,6 +327,13 @@ export function createMockApi(): AgencyZeroApi { return settle(item); }, + async setItemIssue(id, url) { + const item = findItem(id); + item.reference = `issue:${url}`; + emit("item:updated", item); + return settle(item); + }, + async reorderItems(projectId, ids) { ids.forEach((id, order) => { const item = items.find((candidate) => candidate.id === id); @@ -392,18 +398,6 @@ export function createMockApi(): AgencyZeroApi { return settle(settings); }, - claudeTokenStatus: () => settle({ configured: claudeTokenConfigured }), - - setClaudeToken: (_token) => { - claudeTokenConfigured = true; - return settle({ configured: true }); - }, - - removeClaudeToken: () => { - claudeTokenConfigured = false; - return settle({ configured: false }); - }, - claudeUsage: () => settle({ fiveHour: { diff --git a/apps/gui/frontend/src/api/tauri.ts b/apps/gui/frontend/src/api/tauri.ts index 5796613..6741c5f 100644 --- a/apps/gui/frontend/src/api/tauri.ts +++ b/apps/gui/frontend/src/api/tauri.ts @@ -57,6 +57,7 @@ export function createTauriApi(): AgencyZeroApi { deleteItem: (id) => call("delete_item", { id }), setItemStatus: (id, status) => call("set_item_status", { id, status }), updateItem: (id, title) => call("update_item", { id, title }), + setItemIssue: (id, url) => call("set_item_issue", { id, url }), reorderItems: (projectId, ids) => call("reorder_items", { projectId, ids }), listPullRequests: (projectId) => call("list_pull_requests", { projectId }), @@ -69,9 +70,6 @@ export function createTauriApi(): AgencyZeroApi { getSettings: () => call("get_settings"), setSettings: (patch) => call("set_settings", { patch }), - claudeTokenStatus: () => call("claude_token_status"), - setClaudeToken: (token) => call("set_claude_token", { token }), - removeClaudeToken: () => call("remove_claude_token"), claudeUsage: () => call("claude_usage"), listAgentStatus: (recheck) => call("list_agent_status", { recheck }), listModels: (discover) => call("list_models", { discover }), diff --git a/apps/gui/frontend/src/features/home/HomeTab.tsx b/apps/gui/frontend/src/features/home/HomeTab.tsx index f0c4955..0471ce3 100644 --- a/apps/gui/frontend/src/features/home/HomeTab.tsx +++ b/apps/gui/frontend/src/features/home/HomeTab.tsx @@ -510,7 +510,7 @@ function TaskManagerStatus(): JSX.Element { {(task) => (
- + {task.title} {statusSuffix(task.status)} @@ -539,7 +539,7 @@ function GroupItemRow(props: { item: ProjectItem; onFold: () => void; onOpen: () => void; - /** Cycles pending → active → finished → pending, from the marker. */ + /** Cycles through every owner-visible status, from the marker. */ onAdvance: () => void; }): JSX.Element { const { actions } = useWorkspace(); @@ -598,14 +598,9 @@ function GroupItemRow(props: { }} aria-label={`Change the status of ${props.item.title}`} title={`${props.item.status} — click to change`} - class="shrink-0 rounded-md p-0.5 transition-colors hover:bg-primary/12" + class="flex size-7 shrink-0 items-center justify-center rounded-full transition-colors hover:bg-primary/12 focus-visible:bg-primary/12" > - } - > - - + + { expect(container.querySelector("[data-ps-directive]")).toBeNull(); expect(container.textContent).toContain(directive); }); + + it("leaves undeclared Prompt Syntax namespaces inert", () => { + const directive = ""; + const { container } = render(() => ); + + expect(container.querySelector("[data-ps-directive]")).toBeNull(); + expect(container.textContent).toContain(directive); + }); }); /* diff --git a/apps/gui/frontend/src/features/project/MessageBody.tsx b/apps/gui/frontend/src/features/project/MessageBody.tsx index 7f34175..b65613f 100644 --- a/apps/gui/frontend/src/features/project/MessageBody.tsx +++ b/apps/gui/frontend/src/features/project/MessageBody.tsx @@ -1,3 +1,4 @@ +import { PromptSyntaxParser } from "promptsyntax"; import { createSignal, For, type JSX, Show } from "solid-js"; import { Icon } from "~/components/Icon"; import { describeError, log } from "~/lib/log"; @@ -72,13 +73,23 @@ type Block = | { kind: "directive"; text: string } | { kind: "prose"; text: string }; +const promptSyntax = new PromptSyntaxParser({ authoringNamespaces: ["agency"] }); + /** The same explicit authoring-line boundary Rust promotes. */ export function isPromptSyntaxDirectiveLine(line: string): boolean { if (line.startsWith(" ") || line.startsWith("\t")) return false; const trimmed = line.trim(); if (trimmed.startsWith(">")) return false; - const afterTag = trimmed.slice(3); - return trimmed.startsWith(""); + const parsed = promptSyntax.parse(trimmed); + if (parsed.segments.length !== 1) return false; + const [segment] = parsed.segments; + return ( + segment?.type === "directive" && + segment.span.start === 0 && + segment.span.end === trimmed.length && + (segment.directive.kind === "authoring_segment" || + segment.directive.kind === "invalid_authoring_segment") + ); } /** diff --git a/apps/gui/frontend/src/features/project/ProjectPanel.tsx b/apps/gui/frontend/src/features/project/ProjectPanel.tsx index 0656bac..17703e3 100644 --- a/apps/gui/frontend/src/features/project/ProjectPanel.tsx +++ b/apps/gui/frontend/src/features/project/ProjectPanel.tsx @@ -7,7 +7,7 @@ import { ItemMarker } from "~/components/StatusDot"; import { clockTime, elapsed, taskMeta } from "~/lib/format"; import { nextStatus, STATUS_LABELS, statusSuffix } from "~/lib/labels"; import { describeError, log } from "~/lib/log"; -import { prefs, togglePanelSection } from "~/stores/prefs"; +import { prefs, setPrefs, togglePanelSection } from "~/stores/prefs"; import { useNow, useWorkspace } from "~/stores/workspace"; import type { Project, ProjectItem } from "~/types"; @@ -506,7 +506,14 @@ function ItemList(props: { projectId: string; items: ProjectItem[] }): JSX.Eleme * sections keep the column, fully expanded on demand — a harvest that lands * a dozen items should be readable without a 300px porthole. */ - const [tall, setTall] = createSignal(false); + const tall = () => prefs.expandedItemProjects.includes(props.projectId); + const toggleTall = (): void => { + setPrefs("expandedItemProjects", (projects) => + projects.includes(props.projectId) + ? projects.filter((project) => project !== props.projectId) + : [...projects, props.projectId], + ); + }; /** * The pull request a shipped row names, when this project has one by that * number. The item stores the number alone, deliberately: it is the match @@ -515,6 +522,9 @@ function ItemList(props: { projectId: string; items: ProjectItem[] }): JSX.Eleme */ const prUrl = (reference: string) => (state.pullRequests[props.projectId] ?? []).find((pr) => String(pr.number) === reference)?.url; + const issueUrl = (reference: string) => + reference.startsWith("issue:") ? reference.slice("issue:".length) : null; + const issueNumber = (url: string) => url.split("/").at(-1) ?? "?"; /** * Narrows the list as you type. Substring, case-insensitive — the same rule * `wt-tools search-items` uses, so what you find here and what a query finds @@ -558,10 +568,9 @@ function ItemList(props: { projectId: string; items: ProjectItem[] }): JSX.Eleme * The ladder lives in `lib/labels` now, because Home had its own and the * same click on the same row did different things on the two screens. * - * `shipped` is in the cycle because bouncing a fix that did not work is the - * move this list most needs to make easy: one click takes it back to `new`, - * where the next attempt begins. `finished` wraps rather than sticking, so a - * row that turns out not to be done can be reopened. + * Every stored state is in the cycle, including the two owner-only end + * states. Agent directives cannot set those states, but this button belongs + * to the owner and must be able to correct every row manually. */ function advance(item: ProjectItem): void { void actions.setItemStatus(item.id, nextStatus(item.status)); @@ -681,7 +690,7 @@ function ItemList(props: { projectId: string; items: ProjectItem[] }): JSX.Eleme onClick={() => advance(item)} aria-label={`Change the status of ${item.title}`} title={`${statusSuffix(item.status)} — click for ${STATUS_LABELS[nextStatus(item.status)]}`} - class="ml-2.5 shrink-0 cursor-pointer rounded-md p-0.5 transition-colors hover:bg-primary/12" + class="ml-1.5 flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-full transition-colors hover:bg-primary/12 focus-visible:bg-primary/12" > @@ -721,15 +730,31 @@ function ItemList(props: { projectId: string; items: ProjectItem[] }): JSX.Eleme this state exists to prevent. */} - {(number) => ( - /* + {(reference) => ( + (PR #{number()})}> + (PR #{reference()})}> + {(url) => ( + + )} + + } + > {(url) => ( )} @@ -771,6 +796,24 @@ function ItemList(props: { projectId: string; items: ProjectItem[] }): JSX.Eleme {/* Revealed on hover: the controls all the time would be louder than the titles they act on. */} + - - - -
+ Claude Code @@ -925,7 +853,7 @@ function ExperimentalSettings(): JSX.Element { diff --git a/apps/gui/frontend/src/features/shell/useAppShell.ts b/apps/gui/frontend/src/features/shell/useAppShell.ts index 39ab9b2..701e9a8 100644 --- a/apps/gui/frontend/src/features/shell/useAppShell.ts +++ b/apps/gui/frontend/src/features/shell/useAppShell.ts @@ -25,6 +25,14 @@ export function useAppShell(): { const cancelClose = () => setIsClosing(false); + const hasLiveWork = () => + Object.keys(state.runStatus).length > 0 || + Object.values(state.streaming).some((text) => text.length > 0) || + Object.values(state.running).some((tasks) => tasks.length > 0) || + Object.values(state.messages).some((messages) => + messages.some((message) => message.moderation?.needsApproval), + ); + const confirmClose = () => { setIsClosing(false); // destroy(), not close(): close() re-enters the same close-requested @@ -32,6 +40,11 @@ export function useAppShell(): { void getCurrentWindow().destroy(); }; + const requestClose = () => { + if (hasLiveWork()) setIsClosing(true); + else confirmClose(); + }; + onMount(() => { if (!isTauri()) return; @@ -50,7 +63,7 @@ export function useAppShell(): { track(listen("menu:close-tab", () => actions.closeTab(state.activeKey))); track(listen("menu:prev-tab", () => actions.cycleTab(-1))); track(listen("menu:next-tab", () => actions.cycleTab(1))); - track(listen("menu:quit", () => setIsClosing(true))); + track(listen("menu:quit", requestClose)); // Same drain-then-exec as the Settings button; the menu is just nearer. track(listen("menu:restart", () => void actions.relaunchApp())); @@ -58,7 +71,7 @@ export function useAppShell(): { track( getCurrentWindow().onCloseRequested((event) => { event.preventDefault(); - setIsClosing(true); + requestClose(); }), ); diff --git a/apps/gui/frontend/src/features/tabs/TabStrip.tsx b/apps/gui/frontend/src/features/tabs/TabStrip.tsx index eea4473..867c64b 100644 --- a/apps/gui/frontend/src/features/tabs/TabStrip.tsx +++ b/apps/gui/frontend/src/features/tabs/TabStrip.tsx @@ -3,7 +3,8 @@ import { Icon, type IconProps } from "~/components/Icon"; import { StatusDot } from "~/components/StatusDot"; import { createTabReorder } from "~/features/tabs/reorder"; import { useTabShortcuts } from "~/features/tabs/shortcuts"; -import { useWorkspace } from "~/stores/workspace"; +import { countdown } from "~/lib/format"; +import { useNow, useWorkspace } from "~/stores/workspace"; import type { Tab } from "~/types"; const PILL = "flex h-8 shrink-0 items-center rounded-full transition-colors"; @@ -26,7 +27,8 @@ const TAB_ICON: Record = { * working in a different one. */ export function TabStrip(): JSX.Element { - const { state, actions } = useWorkspace(); + const { state, actions, isLive } = useWorkspace(); + const now = useNow(); useTabShortcuts(); let strip!: HTMLDivElement; @@ -64,6 +66,30 @@ export function TabStrip(): JSX.Element { onCleanup(() => observer.disconnect()); }); + onMount(() => { + const refresh = () => { + void actions.refreshQuota(); + if (isLive("claudeUsage")) void actions.refreshClaudeUsage().catch(() => undefined); + }; + refresh(); + const interval = window.setInterval(refresh, 60_000); + onCleanup(() => window.clearInterval(interval)); + }); + + const codexWeek = () => { + const windows = state.quota?.agents.find((agent) => agent.agent === "codex")?.windows ?? []; + return windows.reduce<(typeof windows)[number] | null>( + (longest, window) => + (window.windowMinutes ?? 0) > (longest?.windowMinutes ?? 0) ? window : longest, + null, + ); + }; + + const fractionLabel = (fraction: number | null): string => { + if (fraction === null || !Number.isFinite(fraction)) return "not reported"; + return `${Math.round(Math.min(1, Math.max(0, fraction)) * 100)}%`; + }; + /** * Keeps the active tab visible. * @@ -132,9 +158,27 @@ export function TabStrip(): JSX.Element { nudge(1)} /> - {/* Account usage moved to the project panel, where it has room to say - everything on its own lines instead of a strip of pills. */}
+ + {(window) => ( + + Codex 7d {fractionLabel(window().usedFraction)} + + )} + + + {(window) => ( + + Claude 7d {Math.min(100, Math.max(0, window().utilization)).toFixed(0)}% + + )} +