diff --git a/README.md b/README.md index 18700a8..4ad0321 100644 --- a/README.md +++ b/README.md @@ -105,8 +105,8 @@ coding-agent configuration into a new or existing Sandpi Environment. See - Same-Turn Codex steering while tools are running, with native message ordering - Sandbox/Codex process self-recovery with a bounded visible continuation - Live native context-window and Sandbox CPU/memory usage in the composer -- Codex tools, Skills, MCP configuration, approvals and supported slash-command - surfaces +- Codex tools, Skills, MCP configuration, approvals and browser-native Session + and Workspace controls - Persistent multi-Environment and multi-Session Web UI with compact, progressively paginated Session lists that keep every running Session visible, and completion state distinct from archival that automatically diff --git a/README.zh-CN.md b/README.zh-CN.md index 63d84ce..eabfe16 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -97,7 +97,7 @@ Environment。如果多个 Session 本来就应该共享文件、工具和执行 - 支持在工具运行期间向同一个 Codex Turn 追加消息,并保留原生消息顺序 - Sandbox/Codex 进程自恢复,以及有上限的可见 continuation - 在输入框实时显示原生上下文窗口和 Sandbox CPU/内存使用率 -- Codex tools、Skills、MCP 配置、审批和已支持的 slash-command 界面 +- Codex tools、Skills、MCP 配置、审批,以及浏览器原生的 Session 和 Workspace 操作 - 持久化多 Environment、多 Session Web UI;Session 列表保持紧凑并支持渐进分页, 默认显示所有运行中的 Session,同时提供独立于归档语义、并会在新 Turn 开始时 自动恢复为未完成的完成状态 diff --git a/docs/architecture/native-session-authority.md b/docs/architecture/native-session-authority.md index b2a0a7e..69d5dea 100644 --- a/docs/architecture/native-session-authority.md +++ b/docs/architecture/native-session-authority.md @@ -495,128 +495,21 @@ Thread; the Activity tab is added only when a selected Session supplies its harness-owned renderer. Environment settings remain a sidebar management action and do not replace these Workspace operations. -## Codex slash command boundary - -Slash completion is part of the Codex harness adapter, not the shared -conversation dispatcher. The New Session and active Session composers use one -Codex registry and parser, support pointer plus Up/Down/Tab/Enter/Escape -interaction, and reject unknown commands locally instead of sending them to the -model as user text. Each registry entry also names a stable browser intent. -Dispatch switches on that intent rather than command text. The Codex TUI is the -behavioral reference for command meaning, while app-server remains the data and -mutation authority; redundant aliases and terminal-only presentation commands -are not copied into the browser. - -The model-visible input boundary is strict. Sandpi submits only text the user -can see in a composer, validated native image inputs, or the verbatim Codex TUI -`/init` prompt after the user explicitly invokes that command. It never supplies -Sandpi-authored `baseInstructions` or `developerInstructions`, calls -`thread/inject_items`, or replays a user mutation after runtime repair. The only -Sandpi-authored model input is the visible, versioned, single-attempt recovery -Turn described above. Custom review instructions remain user input and go only -through Codex's native `review/start` contract. - -Commands preserve Sandpi product ownership where a browser-native surface -already exists: - -- `/new [name]` and `/clear [name]` navigate to the Environment's New Session - composer, preserve the optional name until creation, and pass Codex the - matching `startup` or `clear` native Session start source. They remain - unavailable during an active Turn, matching Codex TUI command availability. -- `/fork` calls native `thread/fork`, creates a child product Session and - selects it. -- `/rename [name]` updates only the product Session title; native Thread - metadata remains harness-owned. Bare `/rename` opens the same product - operation in a dialog. `/archive` - updates product Session metadata. -- User-managed completion is also product Session metadata. It only changes - attention treatment in Session navigation; it neither archives the Session - nor changes the harness-native runtime status. A later authoritative native - `turn/started` transition atomically clears completion, while input steered - into the already-active Turn does not. -- `/mention`, `/diff`, `/skills`, `/mcp [verbose]` and `/permissions` open the - corresponding composer, Inspector or Environment settings surface. MCP - verbose requests full native status and renders server tools, resources and - resource templates. -- `/ide` opens the Workspace Inspector. `/agent` opens the dedicated native - Agent Threads picker described below. `/logout` opens the Codex account - connection so the user retains the existing confirmation and reconnect flow. -- `/copy` copies the latest assistant message. -- `/compact` calls `thread/compact/start`; `/review` calls inline - `review/start` with either the native `uncommittedChanges` target or a custom - target. Their ordinary native Turn/item notifications remain authoritative. - Inline review can also expose Codex's private one-shot reviewer Turn beside - the completed review wrapper in `thread/read`. Sandpi derives that exact - adjacent relationship from the native review markers and matching output: - the wrapper remains the Session control Turn and owns the visible result, - while the private delegate is omitted from conversation and interruption - state. The raw native snapshot is not rewritten or persisted separately. -- `/goal` reads, creates and edits native `thread/goal/*` state; `pause`, - `resume` and `clear` map to native status updates or goal removal. -- `/personality` offers the same Friendly and Pragmatic choices as Codex TUI, - reads the live model capability, writes native config, rereads the effective - layered value, and applies that value with - `thread/settings/update` on a loaded Thread. `/usage - daily|weekly|cumulative` projects `account/usage/read` token activity and is - deliberately separate from Sandpi/Sandbox0 billing usage. -- `/memories` writes `features.memories` and native memory policy. Its feature - switch enables or disables both memory use and generation together; after - enabling, either policy can still be adjusted independently. Sandpi rereads - the effective layered values, updates the selected Thread's - `thread/memoryMode/set` eligibility, and exposes - `memory/reset`. `/hooks` reads `hooks/list` and only upserts user-controlled - enablement or the reviewed current hash under `hooks.state`. -- `/ps` lists `thread/backgroundTerminals/list`; `/stop` cleans every native - background terminal, while the process dialog can terminate one process. -- Fast is a - first-class composer switch that sends the service-tier id returned by the - selected model's live `model/list` entry and is absent when Codex reports no - Fast tier for that model. -- `/plan` sends the selected live model and effort through Codex's native Plan - collaboration-mode settings. `/init` submits the same visible user prompt - vendored from Codex TUI's `prompt_for_init_command.md`; Sandpi does not add - hidden instructions around it. - -### Native Agent Threads - -Session Activity is a parent-Thread execution and audit feed; it is not the -Codex Agent picker. Sandpi initializes app-server with -`capabilities.experimentalApi`, then `/agent` uses -`thread/list(ancestorThreadId)` to page the persisted spawn tree at any depth. -The Agent picker contains the main Agent thread and its spawned subagent -threads. This preserves completed descendants across Sandpi, app-server and -Sandbox restarts. Selecting a row calls `thread/read(includeTurns: true)` and -projects that native child transcript with the same Codex message, tool and -Turn renderers used by the main conversation. A child that has not materialized -history yet falls back to metadata-only display. - -The server re-reads the ancestor tree before accepting a child Thread id, so a -caller cannot use the endpoint to inspect an unrelated Thread in the same -Environment. Child metadata and transcripts are never copied into PostgreSQL -or converted into product Sessions. The browser records the open picker as -`agents=1` and a selected child as `agent={threadId}` so refresh restores the -same GUI state. Closing the picker removes both parameters. - -Codex app-server does not expose the TUI slash catalog as a runtime capability. -Sandpi therefore keeps an explicit, reviewed command registry instead of -pretending to discover it dynamically. Future command maintenance should -compare the pinned Codex TUI source with that registry, map browser-relevant -commands to an existing or new intent, and deliberately exclude TUI-only -commands. Native protocol types should continue moving toward output generated -from the Sandbox0-pinned Codex version (`@openai/codex` 0.144.1 at the time of -this document) rather than adding parallel Sandpi protocol models. Browser API -projections may normalize validated native values, but must not invent harness -behavior. - -`/resume` is intentionally absent because Sandpi's sidebar and URL own product -Session selection. `/model` and `/fast` are absent because the composer owns -those controls, and `/status` is absent because the browser already presents -the relevant state. TUI terminal styling, local-login and debug commands are -likewise omitted rather than emulated or forwarded. Commands for Apps, plugins, -experimental flags, feedback and permanent deletion stay absent until Sandpi -has a faithful product surface and lifecycle contract for them. A native -mutation that cannot safely overlap a Turn is hidden and rejected while the -current Turn is active. +## Codex composer boundary + +Sandpi does not implement a browser slash-command grammar or mirror the Codex +TUI command catalog. Text entered in either Codex composer is submitted as +visible user input, including text whose first character is `/`. Product +operations such as creating, forking, renaming or archiving a Session; opening +Workspace and Environment settings; copying responses; and selecting models, +reasoning effort or Fast mode remain explicit browser controls. + +This boundary keeps Sandpi coupled to the app-server capabilities it actually +uses instead of the independently evolving Codex TUI command surface. Native +review and compaction events that already exist in durable Thread history remain +readable, but Sandpi does not expose command-only mutations for starting them. +Environment memory configuration remains an explicit API because the Sandpi CLI +uses it independently of the browser composer. Uploaded composer files use the Sandbox0 File API and live under `/workspace/.sandpi/uploads/{upload-id}/{safe-name}`. Sandpi validates a diff --git a/e2e/smoke.spec.ts b/e2e/smoke.spec.ts index 24e900c..5821e73 100644 --- a/e2e/smoke.spec.ts +++ b/e2e/smoke.spec.ts @@ -21,7 +21,6 @@ import type { CodexFileUpdateChange, CodexNativeSnapshot, CodexSession, - CodexThread, CodexThreadItem, CodexTurn, } from "../src/harnesses/codex/types"; @@ -3768,27 +3767,28 @@ test("interrupts a persisted running Session before its native snapshot arrives" await expect.poll(() => interruptBody).toEqual({}); }); -test("deduplicates native models and sends Plan plus composer Fast mode", async ({ +test("deduplicates native models and submits leading slash text verbatim", async ({ page, - request, }) => { - const workspace = await activeWorkspace(request); - test.skip( - !workspace || - workspace.environment.status !== "ready" || - workspace.environment.codingAgent.status !== "connected", - "A ready Environment with Codex connected is required for this check.", + const bootstrap = getMockBootstrap(); + useEnglishUi(bootstrap); + const environment = bootstrap.environments.find( + (candidate) => + candidate.status === "ready" && + candidate.codingAgent.status === "connected", ); - if ( - !workspace || - workspace.environment.status !== "ready" || - workspace.environment.codingAgent.status !== "connected" - ) { - return; - } - const { environment } = workspace; + expect(environment).toBeTruthy(); + if (!environment) return; + bootstrap.selectedEnvironmentId = environment.id; + bootstrap.selectedSessionId = ""; let createBody: Record | undefined; + await page.route( + (url) => url.pathname === "/api/v1/bootstrap", + async (route) => { + await route.fulfill({ json: { data: bootstrap } }); + }, + ); await page.route( `**/api/v1/environments/${encodeURIComponent(environment.id)}/harnesses/codex/models`, async (route) => { @@ -3856,285 +3856,26 @@ test("deduplicates native models and sends Plan plus composer Fast mode", async const composer = page.locator( 'textarea[name="new-session-instruction"]', ); - await composer.fill("/plan"); - await composer.press("Enter"); - await expect(page.locator(".codex-composer-mode")).toBeVisible(); - await expect(composer).toHaveValue(""); + await composer.fill("/plan Design the persistence change"); + await expect(page.locator(".codex-slash-command-menu")).toHaveCount(0); + await expect(page.locator(".codex-composer-mode")).toHaveCount(0); const fastToggle = page.getByTestId("codex-fast-toggle"); await expect(fastToggle).toHaveAttribute("aria-pressed", "false"); await fastToggle.click(); await expect(fastToggle).toHaveAttribute("aria-pressed", "true"); - await expect(page.locator(".codex-composer-mode")).toHaveCount(1); - await composer.fill("Design the persistence change"); await page.locator(".composer-shell .send-button").click(); await expect .poll(() => createBody) .toMatchObject({ environmentId: environment.id, - prompt: "Design the persistence change", + prompt: "/plan Design the persistence change", modelId: "e2e-plan-model", reasoningEffort: "high", - collaborationMode: "plan", serviceTier: "e2e-native-priority", }); -}); - -test("maps Codex slash commands to Sandpi new and fork Session flows", async ({ - page, - request, -}) => { - const workspace = await activeWorkspace(request); - test.skip(!workspace, "An active Session is required for this check."); - if (!workspace) return; - const { environment, session } = workspace; - const eventPath = `/api/v1/sessions/${session.id}/events`; - const nativeThreadId = "thread-e2e-slash-commands"; - const forkedSessionId = "session-e2e-slash-fork"; - const now = Date.now() / 1_000; - const snapshot: CodexNativeSnapshot = { - protocol: "codex-app-server", - nativeSessionId: nativeThreadId, - historyRevision: 1, - modelId: "e2e-slash-model", - reasoningEffort: "high", - sessionStatus: "waiting", - forkableTurnIds: [], - activity: { - source: "codex-rollout", - availability: "available", - error: null, - records: [], - }, - thread: { - id: nativeThreadId, - createdAt: now, - updatedAt: now, - status: { type: "idle" }, - turns: [], - }, - }; - let forkBody: unknown; - const browserErrors: string[] = []; - page.on("console", (message) => { - if (message.type() === "error") browserErrors.push(message.text()); - }); - page.on("pageerror", (error) => browserErrors.push(error.message)); - - await installDarkUiPreferences(page); - await installControlledEventSource(page); - await page.route("**/api/v1/**/models", async (route) => { - await route.fulfill({ - json: { - data: { - data: [ - { - id: snapshot.modelId, - displayName: "E2E slash model", - isDefault: true, - defaultReasoningEffort: "high", - supportedReasoningEfforts: [ - { - reasoningEffort: "high", - description: "Deep reasoning", - }, - ], - }, - ], - }, - meta: { availability: "available", source: "codex" }, - }, - }); - }); - await page.route( - `**/api/v1/sessions/${encodeURIComponent(session.id)}/fork`, - async (route) => { - forkBody = route.request().postDataJSON(); - await route.fulfill({ - status: 201, - json: { - data: { - ...session, - id: forkedSessionId, - title: "Forked from slash command", - status: "waiting", - archived: false, - createdAt: now, - updatedAt: now, - }, - }, - }); - }, - ); - const agentThread: CodexThread = { - id: "thread-e2e-subagent", - parentThreadId: nativeThreadId, - agentNickname: "Scout", - agentRole: "explorer", - preview: "Inspect the native Agent flow", - createdAt: now + 1, - updatedAt: now + 2, - status: { type: "idle" }, - turns: [ - { - id: "turn-e2e-subagent", - itemsView: "full", - status: "completed", - error: null, - startedAt: now + 1, - completedAt: now + 2, - durationMs: 1_000, - items: [ - { - type: "userMessage", - id: "item-e2e-subagent-prompt", - clientId: null, - content: [ - { - type: "text", - text: "Inspect the native Agent flow", - text_elements: [], - }, - ], - }, - { - type: "agentMessage", - id: "item-e2e-subagent-answer", - text: "Sub-agent result from its own native Thread.", - phase: "final_answer", - memoryCitation: null, - }, - ], - }, - ], - }; - await page.route( - `**/api/v1/sessions/${encodeURIComponent(session.id)}/agents**`, - async (route) => { - const path = new URL(route.request().url()).pathname; - if (path.endsWith("/agents")) { - await route.fulfill({ - json: { - data: { - root: snapshot.thread, - descendants: [{ ...agentThread, turns: [] }], - }, - }, - }); - return; - } - const requestedThreadId = decodeURIComponent(path.split("/").at(-1)!); - await route.fulfill({ - json: { - data: - requestedThreadId === agentThread.id - ? agentThread - : snapshot.thread, - }, - }); - }, - ); - - const sourceUrl = - `/?environment=${encodeURIComponent(environment.id)}` + - `&session=${encodeURIComponent(session.id)}`; - await page.goto(sourceUrl); - await emitControlledEvent(page, eventPath, "snapshot", snapshot); - await expect(page.getByText("Loading conversation…")).toBeHidden(); - - const composer = page.locator('textarea[name="message"]'); - await composer.fill("/"); - const menu = page.getByRole("listbox", { - name: /Codex slash commands|Codex 斜杠命令/, - }); - await expect(menu).toBeVisible(); - await expect( - menu.locator(".codex-slash-command-name", { hasText: "/new" }), - ).toHaveCount(1); - await expect( - menu.locator(".codex-slash-command-name", { hasText: "/fork" }), - ).toHaveCount(1); - await expect(menu).not.toContainText("/resume"); - await expect(menu).not.toContainText("/side"); - await expect(menu).not.toContainText("/btw"); - await expect(menu).not.toContainText("/fast"); - await expect(menu).not.toContainText("/status"); - - await composer.fill("/agent"); - await composer.press("Enter"); - const agentDialog = page.getByRole("dialog", { name: "Agent Threads" }); - await expect(agentDialog).toBeVisible(); - await expect - .poll(() => new URL(page.url()).searchParams.get("agents")) - .toBe("1"); - await expect( - page.getByRole("complementary", { name: "Inspector" }), - ).toBeHidden(); - await agentDialog.getByRole("button", { name: /Scout/ }).click(); - await expect - .poll(() => new URL(page.url()).searchParams.get("agent")) - .toBe(agentThread.id); - await expect( - agentDialog.getByText("Sub-agent result from its own native Thread."), - ).toBeVisible(); - await expect(page.locator("html")).toHaveAttribute( - "data-resolved-theme", - "dark", - ); - const parentAgentMessage = agentDialog.locator( - 'article[data-role="user"] .markdown-content', - ); - await expect(parentAgentMessage).toHaveCSS( - "background-color", - "rgb(32, 56, 42)", - ); - await expect(parentAgentMessage).toHaveCSS( - "color", - "rgb(240, 239, 233)", - ); - await page.reload(); - await emitControlledEvent(page, eventPath, "snapshot", snapshot); - await expect(agentDialog).toBeVisible(); - await expect( - agentDialog.getByText("Sub-agent result from its own native Thread."), - ).toBeVisible(); - await agentDialog - .getByRole("button", { name: "Close Agent Threads" }) - .click(); - await expect - .poll(() => new URL(page.url()).searchParams.get("agents")) - .toBeNull(); - expect(new URL(page.url()).searchParams.get("agent")).toBeNull(); - - await composer.fill("/subagents"); - await composer.press("Enter"); - await expect(agentDialog).toBeHidden(); - await expect( - page - .getByRole("region", { name: "Codex conversation" }) - .getByRole("alert"), - ).toContainText("Unknown command /subagents"); - - await composer.fill("/new"); - await composer.press("Enter"); - await expect - .poll(() => new URL(page.url()).searchParams.get("session")) - .toBeNull(); - await expect( - page.locator('textarea[name="new-session-instruction"]'), - ).toBeVisible(); - - await page.goto(sourceUrl); - await emitControlledEvent(page, eventPath, "snapshot", snapshot); - await expect(page.getByText("Loading conversation…")).toBeHidden(); - const resumedComposer = page.locator('textarea[name="message"]'); - await resumedComposer.fill("/fork"); - await resumedComposer.press("Enter"); - await expect.poll(() => forkBody).toEqual({}); - await expect - .poll(() => new URL(page.url()).searchParams.get("session")) - .toBe(forkedSessionId); - expect(browserErrors).toEqual([]); + expect(createBody).not.toHaveProperty("collaborationMode"); + expect(createBody).not.toHaveProperty("sessionStartSource"); }); test("keeps an optimistic prompt ahead of Activity and folds it for the streamed answer", async ({ diff --git a/openapi.yaml b/openapi.yaml index 0f4c8b6..53aa9c3 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -156,58 +156,6 @@ components: required: - type additionalProperties: false - CodexAgentThreads: - type: object - properties: - root: - $ref: "#/components/schemas/CodexThread" - descendants: - type: array - items: - $ref: "#/components/schemas/CodexThread" - required: - - root - - descendants - additionalProperties: false - CodexBackgroundTerminals: - type: object - properties: - terminals: - type: array - items: - type: object - properties: - itemId: - type: string - processId: - type: string - command: - type: string - cwd: - type: string - osPid: - nullable: true - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - cpuPercent: - nullable: true - type: number - rssKb: - nullable: true - type: number - required: - - itemId - - processId - - command - - cwd - - osPid - - cpuPercent - - rssKb - additionalProperties: false - required: - - terminals - additionalProperties: false CodexComposerUpload: type: object properties: @@ -280,94 +228,6 @@ components: - createdAt - updatedAt additionalProperties: false - CodexHooksInventory: - type: object - properties: - cwd: - type: string - hooks: - type: array - items: - type: object - properties: - key: - type: string - eventName: - type: string - handlerType: - type: string - isManaged: - type: boolean - matcher: - nullable: true - type: string - command: - nullable: true - type: string - timeoutSec: - type: number - statusMessage: - nullable: true - type: string - sourcePath: - type: string - source: - type: string - pluginId: - nullable: true - type: string - displayOrder: - type: number - enabled: - type: boolean - currentHash: - type: string - trustStatus: - type: string - enum: - - trusted - - untrusted - - modified - - managed - required: - - key - - eventName - - handlerType - - isManaged - - matcher - - command - - timeoutSec - - statusMessage - - sourcePath - - source - - pluginId - - displayOrder - - enabled - - currentHash - - trustStatus - additionalProperties: false - warnings: - type: array - items: - type: string - errors: - type: array - items: - type: object - properties: - path: - type: string - message: - type: string - required: - - message - additionalProperties: false - required: - - cwd - - hooks - - warnings - - errors - additionalProperties: false CodexMcpInventory: type: object properties: @@ -542,21 +402,6 @@ components: - activity - forkableTurnIds additionalProperties: {} - CodexPersonalitySettings: - type: object - properties: - personality: - type: string - enum: - - friendly - - pragmatic - - none - supported: - type: boolean - required: - - personality - - supported - additionalProperties: false CodexRateLimitResetResult: type: object properties: @@ -570,34 +415,6 @@ components: required: - outcome additionalProperties: false - CodexSessionGoal: - type: object - properties: - goal: - nullable: true - type: object - properties: - objective: - type: string - status: - type: string - tokenBudget: - nullable: true - type: number - tokensUsed: - type: number - timeUsedSeconds: - type: number - required: - - objective - - status - - tokenBudget - - tokensUsed - - timeUsedSeconds - additionalProperties: false - required: - - goal - additionalProperties: false CodexSkillsInventory: type: object properties: @@ -808,51 +625,6 @@ components: - status - turns additionalProperties: {} - CodexTokenUsage: - type: object - properties: - summary: - type: object - properties: - lifetimeTokens: - nullable: true - type: number - peakDailyTokens: - nullable: true - type: number - longestRunningTurnSec: - nullable: true - type: number - currentStreakDays: - nullable: true - type: number - longestStreakDays: - nullable: true - type: number - required: - - lifetimeTokens - - peakDailyTokens - - longestRunningTurnSec - - currentStreakDays - - longestStreakDays - additionalProperties: false - dailyUsageBuckets: - type: array - items: - type: object - properties: - startDate: - type: string - tokens: - type: number - required: - - startDate - - tokens - additionalProperties: false - required: - - summary - - dailyUsageBuckets - additionalProperties: false CodingSession: type: object properties: @@ -5234,90 +5006,6 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" - /api/v1/environments/{environmentId}/harnesses/codex/hooks: - get: - operationId: listEnvironmentCodexHooks - summary: List Environment Codex hooks - tags: - - Codex - parameters: - - schema: - type: string - minLength: 1 - in: path - name: environmentId - required: true - responses: - "200": - description: Successful response. - content: - application/json: - schema: - type: object - properties: - data: - $ref: "#/components/schemas/CodexHooksInventory" - required: - - data - additionalProperties: false - default: - description: Sandpi error. - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - put: - operationId: updateEnvironmentCodexHook - summary: Update one Environment Codex hook - tags: - - Codex - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - key: - type: string - minLength: 1 - maxLength: 8192 - enabled: - type: boolean - trustedHash: - type: string - minLength: 1 - maxLength: 512 - required: - - key - description: At least one of enabled or trustedHash is required. - description: At least one of enabled or trustedHash is required. - parameters: - - schema: - type: string - minLength: 1 - in: path - name: environmentId - required: true - responses: - "200": - description: Successful response. - content: - application/json: - schema: - type: object - properties: - data: - $ref: "#/components/schemas/CodexHooksInventory" - required: - - data - additionalProperties: false - default: - description: Sandpi error. - content: - application/json: - schema: - $ref: "#/components/schemas/Error" /api/v1/environments/{environmentId}/harnesses/codex/mcp-servers: get: operationId: listEnvironmentCodexMcpServers @@ -5840,10 +5528,10 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" - /api/v1/environments/{environmentId}/harnesses/codex/personality: + /api/v1/environments/{environmentId}/harnesses/codex/rate-limits: get: - operationId: getEnvironmentCodexPersonality - summary: Get the Environment Codex personality + operationId: getCodexRateLimits + summary: Get Codex account rate limits tags: - Codex parameters: @@ -5862,7 +5550,7 @@ paths: type: object properties: data: - $ref: "#/components/schemas/CodexPersonalitySettings" + $ref: "#/components/schemas/CodexAccountRateLimits" required: - data additionalProperties: false @@ -5872,9 +5560,10 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + /api/v1/environments/{environmentId}/harnesses/codex/rate-limits/reset: put: - operationId: setEnvironmentCodexPersonality - summary: Set the Environment Codex personality + operationId: resetCodexRateLimit + summary: Consume one Codex rate-limit reset credit tags: - Codex requestBody: @@ -5884,13 +5573,13 @@ paths: schema: type: object properties: - personality: + idempotencyKey: type: string - enum: - - friendly - - pragmatic + minLength: 16 + maxLength: 128 required: - - personality + - idempotencyKey + additionalProperties: false parameters: - schema: type: string @@ -5907,7 +5596,7 @@ paths: type: object properties: data: - $ref: "#/components/schemas/CodexPersonalitySettings" + $ref: "#/components/schemas/CodexRateLimitResetResult" required: - data additionalProperties: false @@ -5917,13 +5606,20 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" - /api/v1/environments/{environmentId}/harnesses/codex/rate-limits: + /api/v1/environments/{environmentId}/harnesses/codex/skills: get: - operationId: getCodexRateLimits - summary: Get Codex account rate limits + operationId: listEnvironmentCodexSkills + summary: List Environment Codex skills tags: - Codex parameters: + - schema: + type: string + enum: + - "1" + in: query + name: force + required: false - schema: type: string minLength: 1 @@ -5939,7 +5635,7 @@ paths: type: object properties: data: - $ref: "#/components/schemas/CodexAccountRateLimits" + $ref: "#/components/schemas/CodexSkillsInventory" required: - data additionalProperties: false @@ -5949,10 +5645,10 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" - /api/v1/environments/{environmentId}/harnesses/codex/rate-limits/reset: + /api/v1/environments/{environmentId}/harnesses/codex/skills/config: put: - operationId: resetCodexRateLimit - summary: Consume one Codex rate-limit reset credit + operationId: setEnvironmentCodexSkillEnabled + summary: Enable or disable one Environment Codex skill tags: - Codex requestBody: @@ -5962,100 +5658,15 @@ paths: schema: type: object properties: - idempotencyKey: + path: type: string - minLength: 16 - maxLength: 128 + minLength: 1 + maxLength: 4096 + enabled: + type: boolean required: - - idempotencyKey - additionalProperties: false - parameters: - - schema: - type: string - minLength: 1 - in: path - name: environmentId - required: true - responses: - "200": - description: Successful response. - content: - application/json: - schema: - type: object - properties: - data: - $ref: "#/components/schemas/CodexRateLimitResetResult" - required: - - data - additionalProperties: false - default: - description: Sandpi error. - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - /api/v1/environments/{environmentId}/harnesses/codex/skills: - get: - operationId: listEnvironmentCodexSkills - summary: List Environment Codex skills - tags: - - Codex - parameters: - - schema: - type: string - enum: - - "1" - in: query - name: force - required: false - - schema: - type: string - minLength: 1 - in: path - name: environmentId - required: true - responses: - "200": - description: Successful response. - content: - application/json: - schema: - type: object - properties: - data: - $ref: "#/components/schemas/CodexSkillsInventory" - required: - - data - additionalProperties: false - default: - description: Sandpi error. - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - /api/v1/environments/{environmentId}/harnesses/codex/skills/config: - put: - operationId: setEnvironmentCodexSkillEnabled - summary: Enable or disable one Environment Codex skill - tags: - - Codex - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - path: - type: string - minLength: 1 - maxLength: 4096 - enabled: - type: boolean - required: - - path - - enabled + - path + - enabled parameters: - schema: type: string @@ -6201,38 +5812,6 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" - /api/v1/environments/{environmentId}/harnesses/codex/token-usage: - get: - operationId: getCodexTokenUsage - summary: Get Codex account token usage - tags: - - Codex - parameters: - - schema: - type: string - minLength: 1 - in: path - name: environmentId - required: true - responses: - "200": - description: Successful response. - content: - application/json: - schema: - type: object - properties: - data: - $ref: "#/components/schemas/CodexTokenUsage" - required: - - data - additionalProperties: false - default: - description: Sandpi error. - content: - application/json: - schema: - $ref: "#/components/schemas/Error" /api/v1/environments/{environmentId}/harnesses/codex/uploads: post: operationId: uploadCodexComposerFile @@ -8180,11 +7759,6 @@ paths: type: string minLength: 1 maxLength: 100 - sessionStartSource: - type: string - enum: - - startup - - clear images: default: [] maxItems: 6 @@ -8283,10 +7857,10 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" - /api/v1/sessions/{sessionId}/agents: + /api/v1/sessions/{sessionId}/events: get: - operationId: listSessionAgentThreads - summary: List the native Codex agent-thread tree + operationId: streamSessionEvents + summary: Stream the native Session snapshot and live events tags: - Sessions parameters: @@ -8296,32 +7870,55 @@ paths: in: path name: sessionId required: true + x-sandpi-sse: + events: + snapshot: + schema: + $ref: "#/components/schemas/CodexNativeSnapshot" + activity: + schema: + type: object + notification: + schema: + $ref: "#/components/schemas/NativeHarnessEvent" + invalidation: + schema: + type: object + stream-error: + schema: + type: object x-sandpi-native-schema: codex-app-server responses: "200": description: Successful response. content: - application/json: + text/event-stream: schema: - type: object - properties: - data: - $ref: "#/components/schemas/CodexAgentThreads" - required: - - data - additionalProperties: false + type: string default: description: Sandpi error. content: application/json: schema: $ref: "#/components/schemas/Error" - /api/v1/sessions/{sessionId}/agents/{nativeThreadId}: - get: - operationId: getSessionAgentThread - summary: Read one native Codex agent Thread + /api/v1/sessions/{sessionId}/fork: + post: + operationId: forkSession + summary: Fork a Session from its current native branch tags: - Sessions + requestBody: + required: false + content: + application/json: + schema: + default: {} + type: object + properties: + title: + type: string + minLength: 1 + maxLength: 200 parameters: - schema: type: string @@ -8329,23 +7926,17 @@ paths: in: path name: sessionId required: true - - schema: - type: string - minLength: 1 - in: path - name: nativeThreadId - required: true - x-sandpi-native-schema: codex-app-server + x-sandpi-optional-request-body: true responses: - "200": - description: Successful response. + "201": + description: Created. content: application/json: schema: type: object properties: data: - $ref: "#/components/schemas/CodexThread" + $ref: "#/components/schemas/CodingSession" required: - data additionalProperties: false @@ -8355,12 +7946,33 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" - /api/v1/sessions/{sessionId}/background-terminals: - get: - operationId: listSessionBackgroundTerminals - summary: List Codex background terminals + /api/v1/sessions/{sessionId}/metadata: + put: + operationId: updateSessionMetadata + summary: Update product Session metadata tags: - Sessions + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + title: + type: string + minLength: 1 + maxLength: 200 + pinned: + type: boolean + completed: + type: boolean + archived: + type: boolean + unread: + type: boolean + description: At least one metadata field is required. + description: At least one metadata field is required. parameters: - schema: type: string @@ -8377,7 +7989,7 @@ paths: type: object properties: data: - $ref: "#/components/schemas/CodexBackgroundTerminals" + $ref: "#/components/schemas/CodingSession" required: - data additionalProperties: false @@ -8387,9 +7999,10 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" - delete: - operationId: cleanSessionBackgroundTerminals - summary: Clean completed Codex background terminals + /api/v1/sessions/{sessionId}/models: + get: + operationId: listSessionCodexModels + summary: List native Codex model capabilities for a Session tags: - Sessions parameters: @@ -8399,6 +8012,7 @@ paths: in: path name: sessionId required: true + x-sandpi-native-schema: codex-app-server responses: "200": description: Successful response. @@ -8410,484 +8024,29 @@ paths: data: type: object properties: - cleaned: - type: boolean + data: + type: array + items: {} required: - - cleaned + - data additionalProperties: false - required: - - data - additionalProperties: false - default: - description: Sandpi error. - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - /api/v1/sessions/{sessionId}/background-terminals/{processId}: - delete: - operationId: terminateSessionBackgroundTerminal - summary: Terminate one Codex background terminal - tags: - - Sessions - parameters: - - schema: - type: string - minLength: 1 - in: path - name: sessionId - required: true - - schema: - type: string - minLength: 1 - in: path - name: processId - required: true - responses: - "200": - description: Successful response. - content: - application/json: - schema: - type: object - properties: - data: + meta: type: object properties: - terminated: - type: boolean + availability: + type: string + enum: + - available + - runtime-unavailable + source: + type: string + enum: + - codex + message: + type: string required: - - terminated - additionalProperties: false - required: - - data - additionalProperties: false - default: - description: Sandpi error. - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - /api/v1/sessions/{sessionId}/compact: - post: - operationId: compactSession - summary: Start native Session compaction - tags: - - Sessions - parameters: - - schema: - type: string - minLength: 1 - in: path - name: sessionId - required: true - responses: - "202": - description: Accepted. - content: - application/json: - schema: - type: object - properties: - data: - type: object - properties: - accepted: - type: boolean - required: - - accepted - additionalProperties: false - required: - - data - additionalProperties: false - default: - description: Sandpi error. - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - /api/v1/sessions/{sessionId}/events: - get: - operationId: streamSessionEvents - summary: Stream the native Session snapshot and live events - tags: - - Sessions - parameters: - - schema: - type: string - minLength: 1 - in: path - name: sessionId - required: true - x-sandpi-sse: - events: - snapshot: - schema: - $ref: "#/components/schemas/CodexNativeSnapshot" - activity: - schema: - type: object - notification: - schema: - $ref: "#/components/schemas/NativeHarnessEvent" - invalidation: - schema: - type: object - stream-error: - schema: - type: object - x-sandpi-native-schema: codex-app-server - responses: - "200": - description: Successful response. - content: - text/event-stream: - schema: - type: string - default: - description: Sandpi error. - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - /api/v1/sessions/{sessionId}/fork: - post: - operationId: forkSession - summary: Fork a Session from its current native branch - tags: - - Sessions - requestBody: - required: false - content: - application/json: - schema: - default: {} - type: object - properties: - title: - type: string - minLength: 1 - maxLength: 200 - parameters: - - schema: - type: string - minLength: 1 - in: path - name: sessionId - required: true - x-sandpi-optional-request-body: true - responses: - "201": - description: Created. - content: - application/json: - schema: - type: object - properties: - data: - $ref: "#/components/schemas/CodingSession" - required: - - data - additionalProperties: false - default: - description: Sandpi error. - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - /api/v1/sessions/{sessionId}/goal: - get: - operationId: getSessionGoal - summary: Get the native Session goal - tags: - - Sessions - parameters: - - schema: - type: string - minLength: 1 - in: path - name: sessionId - required: true - responses: - "200": - description: Successful response. - content: - application/json: - schema: - type: object - properties: - data: - $ref: "#/components/schemas/CodexSessionGoal" - required: - - data - additionalProperties: false - default: - description: Sandpi error. - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - put: - operationId: updateSessionGoal - summary: Update the native Session goal - tags: - - Sessions - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - objective: - type: string - minLength: 1 - maxLength: 10000 - status: - type: string - enum: - - active - - paused - description: At least one of objective or status is required. - description: At least one of objective or status is required. - parameters: - - schema: - type: string - minLength: 1 - in: path - name: sessionId - required: true - responses: - "200": - description: Successful response. - content: - application/json: - schema: - type: object - properties: - data: - $ref: "#/components/schemas/CodexSessionGoal" - required: - - data - additionalProperties: false - default: - description: Sandpi error. - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - delete: - operationId: clearSessionGoal - summary: Clear the native Session goal - tags: - - Sessions - parameters: - - schema: - type: string - minLength: 1 - in: path - name: sessionId - required: true - responses: - "200": - description: Successful response. - content: - application/json: - schema: - type: object - properties: - data: - $ref: "#/components/schemas/CodexSessionGoal" - required: - - data - additionalProperties: false - default: - description: Sandpi error. - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - /api/v1/sessions/{sessionId}/memories: - get: - operationId: getSessionCodexMemories - summary: Get Session Codex memory settings - tags: - - Sessions - parameters: - - schema: - type: string - minLength: 1 - in: path - name: sessionId - required: true - responses: - "200": - description: Successful response. - content: - application/json: - schema: - type: object - properties: - data: - $ref: "#/components/schemas/CodexMemoriesSettings" - required: - - data - additionalProperties: false - default: - description: Sandpi error. - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - put: - operationId: setSessionCodexMemories - summary: Set Session Codex memory settings - tags: - - Sessions - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - featureEnabled: - type: boolean - useMemories: - type: boolean - generateMemories: - type: boolean - required: - - featureEnabled - - useMemories - - generateMemories - parameters: - - schema: - type: string - minLength: 1 - in: path - name: sessionId - required: true - responses: - "200": - description: Successful response. - content: - application/json: - schema: - type: object - properties: - data: - $ref: "#/components/schemas/CodexMemoriesSettings" - required: - - data - additionalProperties: false - default: - description: Sandpi error. - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - /api/v1/sessions/{sessionId}/metadata: - put: - operationId: updateSessionMetadata - summary: Update product Session metadata - tags: - - Sessions - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - title: - type: string - minLength: 1 - maxLength: 200 - pinned: - type: boolean - completed: - type: boolean - archived: - type: boolean - unread: - type: boolean - description: At least one metadata field is required. - description: At least one metadata field is required. - parameters: - - schema: - type: string - minLength: 1 - in: path - name: sessionId - required: true - responses: - "200": - description: Successful response. - content: - application/json: - schema: - type: object - properties: - data: - $ref: "#/components/schemas/CodingSession" - required: - - data - additionalProperties: false - default: - description: Sandpi error. - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - /api/v1/sessions/{sessionId}/models: - get: - operationId: listSessionCodexModels - summary: List native Codex model capabilities for a Session - tags: - - Sessions - parameters: - - schema: - type: string - minLength: 1 - in: path - name: sessionId - required: true - x-sandpi-native-schema: codex-app-server - responses: - "200": - description: Successful response. - content: - application/json: - schema: - type: object - properties: - data: - type: object - properties: - data: - type: array - items: {} - required: - - data - additionalProperties: false - meta: - type: object - properties: - availability: - type: string - enum: - - available - - runtime-unavailable - source: - type: string - enum: - - codex - message: - type: string - required: - - availability - - source + - availability + - source additionalProperties: false required: - data @@ -8899,134 +8058,6 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" - /api/v1/sessions/{sessionId}/personality: - get: - operationId: getSessionCodexPersonality - summary: Get the Session Codex personality - tags: - - Sessions - parameters: - - schema: - type: string - minLength: 1 - in: path - name: sessionId - required: true - responses: - "200": - description: Successful response. - content: - application/json: - schema: - type: object - properties: - data: - $ref: "#/components/schemas/CodexPersonalitySettings" - required: - - data - additionalProperties: false - default: - description: Sandpi error. - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - put: - operationId: setSessionCodexPersonality - summary: Set the Session Codex personality - tags: - - Sessions - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - personality: - type: string - enum: - - friendly - - pragmatic - required: - - personality - parameters: - - schema: - type: string - minLength: 1 - in: path - name: sessionId - required: true - responses: - "200": - description: Successful response. - content: - application/json: - schema: - type: object - properties: - data: - $ref: "#/components/schemas/CodexPersonalitySettings" - required: - - data - additionalProperties: false - default: - description: Sandpi error. - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - /api/v1/sessions/{sessionId}/review: - post: - operationId: startSessionReview - summary: Start an inline native code review - tags: - - Sessions - requestBody: - required: false - content: - application/json: - schema: - default: {} - type: object - properties: - instructions: - type: string - minLength: 1 - maxLength: 100000 - parameters: - - schema: - type: string - minLength: 1 - in: path - name: sessionId - required: true - x-sandpi-optional-request-body: true - responses: - "202": - description: Accepted. - content: - application/json: - schema: - type: object - properties: - data: - type: object - properties: - nativeTurnId: - type: string - required: - - nativeTurnId - additionalProperties: false - required: - - data - additionalProperties: false - default: - description: Sandpi error. - content: - application/json: - schema: - $ref: "#/components/schemas/Error" /api/v1/sessions/{sessionId}/turns: post: operationId: startSessionTurn diff --git a/src/app/globals.css b/src/app/globals.css index 94ea547..7e22eb9 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -3037,71 +3037,7 @@ code.codex-compact-activity-subject { 0 2px 8px rgb(30 30 24 / 8%); } -.codex-slash-command-menu { - position: absolute; - z-index: 30; - right: 0; - bottom: calc(100% + 8px); - left: 0; - display: grid; - max-height: min(360px, 48dvh); - overflow-y: auto; - padding: 5px; - border: 1px solid var(--line); - border-radius: 12px; - background: var(--panel-strong); - box-shadow: - 0 18px 48px rgb(30 30 24 / 18%), - 0 2px 9px rgb(30 30 24 / 10%); -} - -.codex-slash-command-menu > button { - display: grid; - grid-template-columns: minmax(130px, 0.4fr) minmax(0, 1fr); - align-items: baseline; - gap: 12px; - min-height: 36px; - padding: 8px 9px; - border: 0; - border-radius: 7px; - background: transparent; - color: var(--ink); - text-align: left; - cursor: pointer; -} - -.codex-slash-command-menu > button.is-active, -.codex-slash-command-menu > button:hover { - background: var(--selected); -} - -.codex-slash-command-name { - overflow: hidden; - font-family: var(--font-mono); - font-size: 11px; - font-weight: 650; - text-overflow: ellipsis; - white-space: nowrap; -} - -.codex-slash-command-name i { - margin-left: 5px; - color: var(--ink-faint); - font-style: normal; - font-weight: 450; -} - -.codex-slash-command-description { - overflow: hidden; - color: var(--ink-soft); - font-size: 10.5px; - line-height: 1.35; - text-overflow: ellipsis; - white-space: nowrap; -} - -.codex-composer-notice, -.codex-composer-mode { +.codex-composer-notice { display: flex; min-height: 28px; align-items: center; @@ -3120,38 +3056,16 @@ code.codex-compact-activity-subject { color: var(--red); } -.codex-composer-notice strong, -.codex-composer-mode strong { +.codex-composer-notice strong { color: var(--ink); font-weight: 650; } -.codex-composer-notice > span, -.codex-composer-mode > span { +.codex-composer-notice > span { min-width: 0; flex: 1; } -.codex-composer-mode > button { - display: inline-flex; - width: 22px; - height: 22px; - flex: 0 0 auto; - align-items: center; - justify-content: center; - padding: 0; - border: 0; - border-radius: 5px; - background: transparent; - color: var(--ink-faint); - cursor: pointer; -} - -.codex-composer-mode > button:hover { - background: var(--hover); - color: var(--ink); -} - .composer-shell:focus-within { border-color: var(--ink-faint); box-shadow: @@ -3159,13 +3073,6 @@ code.codex-compact-activity-subject { 0 2px 9px rgb(30 30 24 / 10%); } -@media (max-width: 640px) { - .codex-slash-command-menu > button { - grid-template-columns: minmax(112px, 0.46fr) minmax(0, 1fr); - gap: 8px; - } -} - .native-context-reset-notice { display: grid; grid-template-columns: auto minmax(0, 1fr); diff --git a/src/components/conversation.tsx b/src/components/conversation.tsx index 67a5150..7277255 100644 --- a/src/components/conversation.tsx +++ b/src/components/conversation.tsx @@ -28,10 +28,7 @@ interface ConversationProps { onInspectorTabChange: (tab: InspectorTab) => void; onInspectorWidthRatioChange: (ratio: number, persist: boolean) => void; onToggleTerminal: () => void; - onNewSession: (options?: { - title?: string; - source?: "startup" | "clear"; - }) => void; + onNewSession: () => void; onOpenEnvironmentSettings: ( tab: EnvironmentSettingsTab, options?: EnvironmentSettingsOpenOptions, @@ -49,7 +46,7 @@ interface ConversationProps { /** * Thin harness dispatcher. It selects a complete harness-owned conversation experience and - * intentionally exposes no shared message, tool, approval, slash-command or composer contract. + * intentionally exposes no shared message, tool, approval or composer contract. */ export function Conversation(props: ConversationProps) { if (isCodexSession(props.session)) { diff --git a/src/components/new-session-workspace.tsx b/src/components/new-session-workspace.tsx index d9829b0..9009695 100644 --- a/src/components/new-session-workspace.tsx +++ b/src/components/new-session-workspace.tsx @@ -1,10 +1,6 @@ "use client"; import { CodexNewSessionWorkspace } from "@/harnesses/codex/new-session-workspace"; -import type { - EnvironmentSettingsOpenOptions, - EnvironmentSettingsTab, -} from "@/components/environment-settings"; import type { CodexSession } from "@/harnesses/codex/types"; import type { OperationLanguage, SendShortcut } from "@/lib/operation-ui"; import type { CodingSession, Environment } from "@/lib/types"; @@ -16,13 +12,7 @@ interface NewSessionWorkspaceProps { canManageEnvironment: boolean; onEnvironmentChange: (environment: Environment) => void; onCreated: (session: CodingSession) => void; - initialTitle?: string; - sessionStartSource?: "startup" | "clear"; onOpenAgentHarnessSettings: () => void; - onOpenEnvironmentSettings: ( - tab: EnvironmentSettingsTab, - options?: EnvironmentSettingsOpenOptions, - ) => void; onToggleSidebar: () => void; inspectorOpen: boolean; onToggleInspector: () => void; diff --git a/src/components/sandpi-app.tsx b/src/components/sandpi-app.tsx index 123873a..f6c2b44 100644 --- a/src/components/sandpi-app.tsx +++ b/src/components/sandpi-app.tsx @@ -107,10 +107,6 @@ export function SandpiApp({ initialData }: SandpiAppProps) { initialTab: EnvironmentSettingsTab; mcpVerbose?: boolean; } | null>(null); - const [newSessionPreset, setNewSessionPreset] = useState<{ - title?: string; - source?: "startup" | "clear"; - } | null>(null); const [newEnvironmentOpen, setNewEnvironmentOpen] = useState(false); const [inspectorOpen, setInspectorOpen] = useState(false); const [terminalOpen, setTerminalOpen] = useState(false); @@ -220,7 +216,6 @@ export function SandpiApp({ initialData }: SandpiAppProps) { ); if (environmentChanged) setTerminalOpen(false); if (selectionChanged) { - setNewSessionPreset(null); const environment = next.environments.find( (candidate) => candidate.id === next.selectedEnvironmentId, ); @@ -576,17 +571,13 @@ export function SandpiApp({ initialData }: SandpiAppProps) { setSelectedSessionId(""); replaceWorkspaceUrl(environmentId); } - setNewSessionPreset(null); setSidebarOpen(false); }, [environments, hydrateSession, sessions], ); const handleNewSession = useCallback( - ( - environmentId: string, - options?: { title?: string; source?: "startup" | "clear" }, - ) => { + (environmentId: string) => { if ( !environments.some( (environment) => environment.id === environmentId, @@ -596,7 +587,6 @@ export function SandpiApp({ initialData }: SandpiAppProps) { } setSelectedEnvironmentId(environmentId); setSelectedSessionId(""); - setNewSessionPreset(options ?? null); replaceWorkspaceUrl(environmentId); setSidebarOpen(false); }, @@ -745,7 +735,6 @@ export function SandpiApp({ initialData }: SandpiAppProps) { ]); setSelectedEnvironmentId(session.environmentId); setSelectedSessionId(session.id); - setNewSessionPreset(null); replaceWorkspaceUrl(session.environmentId, session.id); setTerminalOpen(false); }, @@ -1123,9 +1112,7 @@ export function SandpiApp({ initialData }: SandpiAppProps) { onInspectorTabChange={handleInspectorTabChange} onInspectorWidthRatioChange={handleInspectorWidthRatioChange} onToggleTerminal={() => setTerminalOpen((open) => !open)} - onNewSession={(options) => - handleNewSession(selectedEnvironment.id, options) - } + onNewSession={() => handleNewSession(selectedEnvironment.id)} onOpenEnvironmentSettings={(tab, options) => openEnvironmentSettings(selectedEnvironment.id, tab, options) } @@ -1148,14 +1135,9 @@ export function SandpiApp({ initialData }: SandpiAppProps) { canManageEnvironment={canManageEnvironment(selectedEnvironment)} onEnvironmentChange={handleEnvironmentChange} onCreated={handleSessionCreated} - initialTitle={newSessionPreset?.title} - sessionStartSource={newSessionPreset?.source} onOpenAgentHarnessSettings={() => openEnvironmentSettings(selectedEnvironment.id, "credentials") } - onOpenEnvironmentSettings={(tab, options) => - openEnvironmentSettings(selectedEnvironment.id, tab, options) - } onToggleSidebar={handleToggleNavigation} inspectorOpen={showInspector} onToggleInspector={() => handleInspectorOpenChange(!showInspector)} diff --git a/src/harnesses/codex/agent-threads-dialog.module.css b/src/harnesses/codex/agent-threads-dialog.module.css deleted file mode 100644 index 4e226ba..0000000 --- a/src/harnesses/codex/agent-threads-dialog.module.css +++ /dev/null @@ -1,571 +0,0 @@ -.backdrop { - position: fixed; - z-index: 95; - inset: 0; - display: flex; - align-items: center; - justify-content: center; - padding: 20px; - background: rgb(24 24 21 / 38%); - backdrop-filter: blur(2px); -} - -.dialog { - --agent-dialog-chrome: var(--panel-strong); - --agent-dialog-sidebar: var(--sidebar); - --agent-dialog-content: var(--panel-strong); - --agent-dialog-user-surface: var(--green-soft); - --agent-dialog-user-border: color-mix( - in srgb, - var(--green) 32%, - var(--line) - ); - - display: grid; - width: min(1120px, 100%); - height: min(760px, calc(100dvh - 40px)); - grid-template-rows: auto minmax(0, 1fr) auto; - overflow: hidden; - border: 1px solid var(--line); - border-radius: 14px; - outline: 0; - background: var(--agent-dialog-content); - box-shadow: 0 26px 80px rgb(24 24 20 / 26%); -} - -.header { - display: flex; - min-height: 62px; - align-items: center; - gap: 11px; - padding: 10px 12px 10px 17px; - border-bottom: 1px solid var(--line); - background: var(--agent-dialog-chrome); -} - -.headerIcon { - display: inline-flex; - width: 34px; - height: 34px; - flex: 0 0 auto; - align-items: center; - justify-content: center; - border: 1px solid var(--line); - border-radius: 9px; - background: var(--sidebar); - color: var(--ink-soft); -} - -.headerCopy { - display: flex; - min-width: 0; - flex: 1; - flex-direction: column; - gap: 3px; -} - -.headerCopy strong { - color: var(--ink); - font-size: 13px; - font-weight: 680; -} - -.headerCopy small { - overflow: hidden; - color: var(--ink-faint); - font-size: 10px; - text-overflow: ellipsis; - white-space: nowrap; -} - -.header > button { - display: inline-flex; - width: 32px; - height: 32px; - align-items: center; - justify-content: center; - padding: 0; - border: 0; - border-radius: 7px; - background: transparent; - color: var(--ink-faint); - cursor: pointer; -} - -.header > button:hover { - background: var(--hover); - color: var(--ink); -} - -.body { - display: grid; - min-height: 0; - grid-template-columns: minmax(230px, 29%) minmax(0, 1fr); -} - -.threadList { - min-height: 0; - overflow-y: auto; - border-right: 1px solid var(--line); - background: var(--agent-dialog-sidebar); - scrollbar-width: thin; -} - -.listHeading { - position: sticky; - z-index: 1; - top: 0; - display: flex; - min-height: 39px; - align-items: center; - justify-content: space-between; - padding: 0 13px; - border-bottom: 1px solid var(--line-soft); - background: var(--agent-dialog-sidebar); - color: var(--ink-faint); - font-size: 9px; - font-weight: 700; - letter-spacing: 0.055em; - text-transform: uppercase; -} - -.listHeading small { - font-size: 9px; -} - -.threadList > button { - display: grid; - width: calc(100% - 10px); - min-height: 57px; - grid-template-columns: 12px minmax(0, 1fr) auto; - align-items: center; - gap: 8px; - margin: 5px; - padding: 8px 9px; - border: 0; - border-radius: 8px; - background: transparent; - color: var(--ink-soft); - text-align: left; - cursor: pointer; -} - -.threadList > button:hover { - background: var(--hover); -} - -.threadList > button.selected { - background: var(--selected); - color: var(--ink); - box-shadow: inset 0 0 0 1px - color-mix(in srgb, var(--ink-faint) 12%, transparent); -} - -.status { - width: 7px; - height: 7px; - border-radius: 50%; - background: var(--ink-faint); -} - -.status[data-status="active"] { - background: var(--green); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--green) 14%, transparent); -} - -.status[data-status="idle"] { - background: var(--blue); -} - -.status[data-status="systemError"] { - background: var(--red); -} - -.threadCopy { - display: flex; - min-width: 0; - flex-direction: column; - gap: 4px; -} - -.threadCopy strong, -.threadCopy small { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.threadCopy strong { - color: var(--ink); - font-size: 11px; - font-weight: 650; -} - -.threadCopy small { - color: var(--ink-faint); - font-size: 9px; -} - -.threadState { - color: var(--ink-faint); - font-size: 8px; - white-space: nowrap; -} - -.loading, -.error { - display: flex; - align-items: center; - gap: 7px; - margin: 10px; - padding: 10px; - color: var(--ink-faint); - font-size: 10px; -} - -.loading svg, -.detailLoading svg { - animation: spin 0.9s linear infinite; -} - -.error { - border: 1px solid color-mix(in srgb, var(--red) 22%, var(--line)); - border-radius: 7px; - background: var(--red-soft); - color: var(--red); - line-height: 1.45; -} - -.detail { - display: grid; - min-width: 0; - min-height: 0; - grid-template-rows: auto minmax(0, 1fr); - background: var(--agent-dialog-content); -} - -.detailHeader { - display: flex; - min-height: 58px; - align-items: center; - justify-content: space-between; - gap: 16px; - padding: 9px 18px; - border-bottom: 1px solid var(--line-soft); - background: var(--agent-dialog-chrome); -} - -.detailHeader > span:first-child { - display: flex; - min-width: 0; - flex-direction: column; - gap: 3px; -} - -.detailHeader strong, -.detailHeader small { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.detailHeader strong { - color: var(--ink); - font-size: 12px; - font-weight: 670; -} - -.detailHeader small { - color: var(--ink-faint); - font-size: 9px; -} - -.detailMeta { - display: flex; - flex: 0 0 auto; - align-items: center; - gap: 8px; - color: var(--ink-faint); - font-size: 8px; -} - -.detailMeta b { - padding: 3px 7px; - border-radius: 999px; - background: var(--sidebar); - color: var(--ink-soft); - font-weight: 650; -} - -.detailMeta b[data-status="active"] { - background: var(--green-soft); - color: var(--green); -} - -.detailMeta b[data-status="systemError"] { - background: var(--red-soft); - color: var(--red); -} - -.detailScroll { - min-height: 0; - overflow-y: auto; - scrollbar-width: thin; -} - -.detailLoading, -.detailError, -.emptyTranscript { - display: flex; - min-height: 220px; - align-items: center; - justify-content: center; - flex-direction: column; - gap: 8px; - padding: 24px; - color: var(--ink-faint); - text-align: center; -} - -.detailLoading { - flex-direction: row; - font-size: 10px; -} - -.detailError strong, -.emptyTranscript strong { - color: var(--ink-soft); - font-size: 11px; -} - -.detailError p, -.emptyTranscript p { - max-width: 460px; - margin: 0; - font-size: 9.5px; - line-height: 1.55; -} - -.detailError { - color: var(--red); -} - -.transcript { - width: min(100%, 760px); - margin: 0 auto; - padding: 30px 24px 80px; -} - -.transcriptMessage { - display: flex; - width: min(100%, 650px); - flex-direction: column; - gap: 7px; - margin: 0 0 25px; -} - -.transcriptMessage[data-role="user"] { - width: fit-content; - max-width: min(100%, 580px); - margin-left: auto; -} - -.transcriptAuthor { - color: var(--ink-faint); - font-size: 9px; - font-weight: 650; -} - -.transcriptMessage[data-role="user"] .transcriptAuthor { - text-align: right; -} - -.transcriptMessage[data-role="user"] :global(.markdown-content) { - padding: 10px 12px; - border: 1px solid var(--agent-dialog-user-border); - border-radius: 10px 10px 3px 10px; - background: var(--agent-dialog-user-surface); - color: var(--ink); -} - -.transcript :global(.message) { - margin-bottom: 23px; -} - -.transcript :global(.message-codex-activity), -.transcript :global(.message-codex-turn-activity) { - grid-template-columns: 0 minmax(0, 1fr) 0; -} - -.attachments { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); - gap: 7px; -} - -.attachments img { - width: 100%; - height: auto; - max-height: 260px; - border: 1px solid var(--line); - border-radius: 8px; - object-fit: cover; -} - -.attachmentNote { - color: var(--ink-faint); - font-size: 9px; -} - -.streaming { - display: inline-flex; - gap: 4px; -} - -.streaming i { - width: 5px; - height: 5px; - border-radius: 50%; - background: var(--ink-faint); - animation: pulse 1s ease-in-out infinite; -} - -.streaming i:nth-child(2) { - animation-delay: 120ms; -} - -.streaming i:nth-child(3) { - animation-delay: 240ms; -} - -.footer { - display: flex; - min-height: 38px; - align-items: center; - justify-content: space-between; - gap: 12px; - padding: 7px 14px; - border-top: 1px solid var(--line); - background: var(--agent-dialog-sidebar); - color: var(--ink-faint); - font-size: 8.5px; -} - -.footer code { - color: var(--ink-soft); - font-size: 8.5px; -} - -:global(html[data-resolved-theme="dark"]) .backdrop { - background: rgb(0 0 0 / 58%); - backdrop-filter: blur(4px) saturate(0.72); -} - -:global(html[data-resolved-theme="dark"]) .dialog { - --agent-dialog-chrome: color-mix( - in srgb, - var(--panel-strong) 88%, - var(--canvas) - ); - --agent-dialog-sidebar: color-mix( - in srgb, - var(--sidebar) 86%, - var(--canvas) - ); - --agent-dialog-content: color-mix( - in srgb, - var(--panel-strong) 94%, - var(--canvas) - ); - - border-color: color-mix(in srgb, var(--ink-faint) 28%, var(--line)); - box-shadow: - 0 30px 96px rgb(0 0 0 / 58%), - inset 0 1px 0 rgb(255 255 255 / 3%); -} - -:global(html[data-resolved-theme="dark"]) .headerIcon { - border-color: color-mix(in srgb, var(--ink-faint) 22%, var(--line)); - background: color-mix( - in srgb, - var(--agent-dialog-sidebar) 76%, - var(--panel-strong) - ); - color: var(--ink-soft); -} - -:global(html[data-resolved-theme="dark"]) .header > button:hover { - background: color-mix(in srgb, var(--hover) 82%, var(--panel-strong)); -} - -:global(html[data-resolved-theme="dark"]) .threadList > button.selected { - background: color-mix(in srgb, var(--selected) 88%, var(--blue-soft)); - box-shadow: inset 0 0 0 1px - color-mix(in srgb, var(--blue) 18%, var(--line)); -} - -:global(html[data-resolved-theme="dark"]) .detailMeta b { - border: 1px solid color-mix(in srgb, var(--ink-faint) 16%, var(--line-soft)); -} - -:global(html[data-resolved-theme="dark"]) - .transcriptMessage[data-role="user"] - :global(.markdown-content) { - box-shadow: inset 0 1px 0 rgb(255 255 255 / 4%); -} - -@keyframes spin { - to { - transform: rotate(360deg); - } -} - -@keyframes pulse { - 0%, - 100% { - opacity: 0.35; - } - - 50% { - opacity: 1; - } -} - -@media (max-width: 760px) { - .backdrop { - align-items: stretch; - padding: 0; - } - - .dialog { - height: 100dvh; - border: 0; - border-radius: 0; - } - - .body { - grid-template-columns: 1fr; - grid-template-rows: minmax(150px, 32%) minmax(0, 1fr); - } - - .threadList { - border-right: 0; - border-bottom: 1px solid var(--line); - } - - .detailHeader { - padding-inline: 13px; - } - - .detailMeta time { - display: none; - } - - .transcript { - padding: 22px 14px 60px; - } - - .footer > span:last-child { - display: none; - } -} diff --git a/src/harnesses/codex/agent-threads-dialog.tsx b/src/harnesses/codex/agent-threads-dialog.tsx deleted file mode 100644 index 3b4cb91..0000000 --- a/src/harnesses/codex/agent-threads-dialog.tsx +++ /dev/null @@ -1,678 +0,0 @@ -"use client"; - -import Image from "next/image"; -import { - Bot, - LoaderCircle, - RefreshCw, - UsersRound, - X, -} from "lucide-react"; -import { - Fragment, - useEffect, - useId, - useMemo, - useRef, - useState, -} from "react"; - -import { MarkdownContent } from "@/components/markdown-content"; -import { - CodexCommandActivity, - CodexFileChangeActivity, - CodexNativeItemActivity, - CodexNativeToolActivity, - CodexTurnActivity, - CodexTurnResult, -} from "@/harnesses/codex/activity"; -import { - codexAgentThreadName, - codexAgentThreadRows, -} from "@/harnesses/codex/agent-threads"; -import { - projectCodexTimeline, - type CodexTimelineEntry, -} from "@/harnesses/codex/events"; -import { - groupCodexTimelineByTurn, - type CodexTurnTimelineGroup, -} from "@/harnesses/codex/timeline"; -import type { - CodexAgentThreads, - CodexThread, -} from "@/harnesses/codex/types"; -import { formatUnixTimestamp } from "@/lib/time"; -import { - apiFetch, - type ApiEnvelope, -} from "@/lib/api-client"; -import type { OperationLanguage } from "@/lib/operation-ui"; - -import styles from "./agent-threads-dialog.module.css"; - -interface CodexAgentThreadsDialogProps { - language: OperationLanguage; - timeZone: string; - sessionId: string; - sessionTitle: string; - harnessLabel: string; - selectedThreadId?: string; - onSelectedThreadChange: (threadId?: string) => void; - onOpenWorkspacePath: (path: string) => void; - onOpenFiles: (path?: string) => void; - onClose: () => void; -} - -function threadStatusLabel( - thread: CodexThread, - language: OperationLanguage, -) { - if (thread.status.type === "active") { - if (thread.status.activeFlags.includes("waitingOnApproval")) { - return language === "zh-CN" ? "等待审批" : "Waiting for approval"; - } - if (thread.status.activeFlags.includes("waitingOnUserInput")) { - return language === "zh-CN" ? "等待输入" : "Waiting for input"; - } - return language === "zh-CN" ? "运行中" : "Running"; - } - if (thread.status.type === "idle") { - return language === "zh-CN" ? "空闲" : "Idle"; - } - if (thread.status.type === "systemError") { - return language === "zh-CN" ? "错误" : "Error"; - } - return language === "zh-CN" ? "已停止" : "Stopped"; -} - -function threadUpdatedAt( - thread: CodexThread, - language: OperationLanguage, - timeZone: string, -) { - if (thread.updatedAt === undefined) return ""; - return formatUnixTimestamp( - thread.updatedAt, - language === "zh-CN" ? "zh-CN" : "en", - timeZone, - { - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - }, - ); -} - -function AgentThreadTranscript({ - thread, - agentName, - root, - harnessLabel, - language, - onOpenWorkspacePath, - onOpenFiles, -}: { - thread: CodexThread; - agentName: string; - root: boolean; - harnessLabel: string; - language: OperationLanguage; - onOpenWorkspacePath: (path: string) => void; - onOpenFiles: (path?: string) => void; -}) { - const projection = useMemo(() => projectCodexTimeline(thread), [thread]); - const turns = useMemo( - () => groupCodexTimelineByTurn(projection), - [projection], - ); - const now = Date.now(); - - function renderActivity(entry: CodexTimelineEntry) { - if (entry.kind === "command") { - return ( - - ); - } - if (entry.kind === "fileChange") { - return ( - - ); - } - if (entry.kind === "nativeItem") { - return ( - - ); - } - if ( - entry.kind === "mcpToolCall" || - entry.kind === "dynamicToolCall" || - entry.kind === "webSearch" || - entry.kind === "collabAgentToolCall" || - entry.kind === "subAgentActivity" || - entry.kind === "imageGeneration" - ) { - return ( - - ); - } - if (entry.kind === "turnResult") { - return ( - - ); - } - - return ( -
- - {entry.role === "assistant" - ? agentName || harnessLabel - : root - ? language === "zh-CN" - ? "你" - : "You" - : language === "zh-CN" - ? "父 Agent" - : "Parent agent"} - - {entry.attachments?.length ? ( -
- {entry.attachments.map((attachment) => ( - {attachment.name} - ))} -
- ) : null} - {entry.localImages?.length ? ( - - {language === "zh-CN" - ? `${entry.localImages.length} 个工作区图片引用` - : `${entry.localImages.length} Workspace image reference${ - entry.localImages.length === 1 ? "" : "s" - }`} - - ) : null} - {entry.content ? ( - - ) : entry.streaming ? ( - - - - - - ) : null} -
- ); - } - - function renderTurn(turn: CodexTurnTimelineGroup) { - const lastActivityIndex = turn.blocks.findLastIndex( - (block) => block.kind === "activity", - ); - return ( - - {turn.blocks.map((block, index) => { - if (block.kind === "message" || block.kind === "result") { - return renderActivity(block.entry); - } - const activeTurn = - block.id === turn.activeActivityBlockId - ? projection.activeTurn - : undefined; - if (block.entries.length === 0 && !activeTurn) return null; - return ( - - {block.entries.map(renderActivity)} - - ); - })} - - ); - } - - if (turns.length === 0) { - return ( -
-
- ); - } - - return
{turns.map(renderTurn)}
; -} - -export function CodexAgentThreadsDialog({ - language, - timeZone, - sessionId, - sessionTitle, - harnessLabel, - selectedThreadId, - onSelectedThreadChange, - onOpenWorkspacePath, - onOpenFiles, - onClose, -}: CodexAgentThreadsDialogProps) { - const titleId = useId(); - const dialogRef = useRef(null); - const [tree, setTree] = useState(); - const [activeThreadId, setActiveThreadId] = useState( - selectedThreadId ?? "", - ); - const [thread, setThread] = useState(); - const [listLoading, setListLoading] = useState(true); - const [threadLoading, setThreadLoading] = useState(false); - const [listError, setListError] = useState(""); - const [threadError, setThreadError] = useState(""); - const [refreshSequence, setRefreshSequence] = useState(0); - const rows = useMemo( - () => (tree ? codexAgentThreadRows(tree) : []), - [tree], - ); - const activeRow = rows.find((row) => row.thread.id === activeThreadId); - const activeName = activeRow - ? activeRow.root - ? sessionTitle - : codexAgentThreadName(activeRow.thread) - : ""; - - useEffect(() => { - const previouslyFocused = document.activeElement as HTMLElement | null; - const previousOverflow = document.body.style.overflow; - document.body.style.overflow = "hidden"; - const frame = window.requestAnimationFrame(() => dialogRef.current?.focus()); - return () => { - window.cancelAnimationFrame(frame); - document.body.style.overflow = previousOverflow; - previouslyFocused?.focus(); - }; - }, []); - - useEffect(() => { - setTree(undefined); - setThread(undefined); - setActiveThreadId(""); - setListError(""); - setThreadError(""); - }, [sessionId]); - - useEffect(() => { - if (selectedThreadId) setActiveThreadId(selectedThreadId); - }, [selectedThreadId]); - - useEffect(() => { - let disposed = false; - let controller: AbortController | undefined; - const load = async (initial: boolean) => { - controller?.abort(); - const requestController = new AbortController(); - controller = requestController; - if (initial) setListLoading(true); - try { - const response = await apiFetch>( - `/api/v1/sessions/${encodeURIComponent(sessionId)}/agents`, - { signal: requestController.signal }, - ); - if (disposed) return; - setTree(response.data); - setListError(""); - setActiveThreadId((current) => { - const available = new Set([ - response.data.root.id, - ...response.data.descendants.map((candidate) => candidate.id), - ]); - if (current && available.has(current)) return current; - if (current) onSelectedThreadChange(undefined); - return response.data.root.id; - }); - } catch (cause) { - if (!disposed && !requestController.signal.aborted) { - setListError( - cause instanceof Error - ? cause.message - : language === "zh-CN" - ? "无法读取 Agent Threads。" - : "Could not load Agent Threads.", - ); - } - } finally { - if (!disposed && initial) setListLoading(false); - } - }; - void load(true); - const timer = window.setInterval(() => void load(false), 3_000); - return () => { - disposed = true; - controller?.abort(); - window.clearInterval(timer); - }; - }, [language, onSelectedThreadChange, refreshSequence, sessionId]); - - useEffect(() => { - if (!activeThreadId) return; - let disposed = false; - let controller: AbortController | undefined; - const load = async (background: boolean) => { - controller?.abort(); - const requestController = new AbortController(); - controller = requestController; - if (!background) setThreadLoading(true); - try { - const response = await apiFetch>( - `/api/v1/sessions/${encodeURIComponent(sessionId)}/agents/${encodeURIComponent(activeThreadId)}`, - { signal: requestController.signal }, - ); - if (disposed) return; - setThread(response.data); - setThreadError(""); - } catch (cause) { - if (!disposed && !requestController.signal.aborted) { - setThreadError( - cause instanceof Error - ? cause.message - : language === "zh-CN" - ? "无法读取 Agent Thread。" - : "Could not load the Agent Thread.", - ); - } - } finally { - if (!disposed && !background) setThreadLoading(false); - } - }; - setThread(undefined); - setThreadError(""); - void load(false); - const shouldPoll = - activeThreadId !== tree?.root.id && - activeRow?.thread.status.type === "active"; - const timer = shouldPoll - ? window.setInterval(() => void load(true), 3_000) - : undefined; - return () => { - disposed = true; - controller?.abort(); - if (timer !== undefined) window.clearInterval(timer); - }; - }, [ - activeRow?.thread.status.type, - activeThreadId, - language, - refreshSequence, - sessionId, - tree?.root.id, - ]); - - return ( -
{ - if (event.target === event.currentTarget) onClose(); - }} - > -
{ - if (event.key === "Tab") { - const focusable = Array.from( - dialogRef.current?.querySelectorAll( - 'button:not([disabled]), [href], [tabindex]:not([tabindex="-1"])', - ) ?? [], - ); - const first = focusable[0]; - const last = focusable[focusable.length - 1]; - if (event.shiftKey && document.activeElement === first) { - event.preventDefault(); - last?.focus(); - } else if (!event.shiftKey && document.activeElement === last) { - event.preventDefault(); - first?.focus(); - } - } else if (event.key === "Escape") { - event.preventDefault(); - onClose(); - } - }} - > -
- - - - Agent Threads - - {language === "zh-CN" - ? "Codex 原生线程树与各子 Agent 对话" - : "Native Codex thread tree and sub-agent conversations"} - - - - -
- -
- - -
- {activeRow ? ( -
- - {activeName} - - {activeRow.thread.agentRole - ? activeRow.thread.agentRole - : activeRow.root - ? language === "zh-CN" - ? "当前 Sandpi Session" - : "Current Sandpi Session" - : language === "zh-CN" - ? "Codex 子 Agent" - : "Codex sub-agent"} - - - - - {threadStatusLabel(activeRow.thread, language)} - - {threadUpdatedAt(activeRow.thread, language, timeZone) ? ( - - ) : null} - -
- ) : null} -
- {threadLoading ? ( -
-
- ) : null} - {threadError ? ( -
- - {language === "zh-CN" - ? "无法打开 Agent Thread" - : "Could not open Agent Thread"} - -

{threadError}

-
- ) : null} - {thread && !threadLoading ? ( - - ) : null} -
-
-
- -
- - /agent - - - {language === "zh-CN" - ? "Activity 仍用于查看父线程的审计记录" - : "Activity remains the parent-thread audit trail"} - -
-
-
- ); -} diff --git a/src/harnesses/codex/agent-threads.test.ts b/src/harnesses/codex/agent-threads.test.ts deleted file mode 100644 index 7aede95..0000000 --- a/src/harnesses/codex/agent-threads.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; - -import { - codexAgentThreadName, - codexAgentThreadRows, -} from "./agent-threads"; -import type { CodexAgentThreads, CodexThread } from "./types"; - -function thread( - id: string, - parentThreadId?: string, - createdAt = 1, -): CodexThread { - return { - id, - parentThreadId, - createdAt, - status: { type: "notLoaded" }, - turns: [], - }; -} - -test("orders native Agent Threads by their parent relation", () => { - const tree: CodexAgentThreads = { - root: thread("root"), - descendants: [ - thread("grandchild", "child-a", 3), - thread("child-b", "root", 2), - thread("child-a", "root", 1), - ], - }; - - assert.deepEqual( - codexAgentThreadRows(tree).map(({ thread: value, depth, root }) => ({ - id: value.id, - depth, - root, - })), - [ - { id: "root", depth: 0, root: true }, - { id: "child-a", depth: 1, root: false }, - { id: "grandchild", depth: 2, root: false }, - { id: "child-b", depth: 1, root: false }, - ], - ); -}); - -test("uses native nickname, role, preview and id fallbacks for Agent names", () => { - assert.equal( - codexAgentThreadName({ - ...thread("one"), - agentNickname: "Scout", - agentRole: "explorer", - }), - "Scout", - ); - assert.equal( - codexAgentThreadName({ ...thread("two"), agentRole: "reviewer" }), - "reviewer", - ); - assert.equal( - codexAgentThreadName({ ...thread("three"), preview: "Inspect auth\nlater" }), - "Inspect auth", - ); - assert.equal(codexAgentThreadName(thread("thread-12345678")), "Agent 12345678"); -}); diff --git a/src/harnesses/codex/agent-threads.ts b/src/harnesses/codex/agent-threads.ts deleted file mode 100644 index e696572..0000000 --- a/src/harnesses/codex/agent-threads.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { CodexAgentThreads, CodexThread } from "./types"; - -export interface CodexAgentThreadRow { - thread: CodexThread; - depth: number; - root: boolean; -} - -function compareThreads(left: CodexThread, right: CodexThread) { - const createdDifference = (left.createdAt ?? 0) - (right.createdAt ?? 0); - return createdDifference || left.id.localeCompare(right.id); -} - -/** Flatten the native parent graph into a stable tree suitable for a picker. */ -export function codexAgentThreadRows( - tree: CodexAgentThreads, -): CodexAgentThreadRow[] { - const children = new Map(); - for (const thread of tree.descendants) { - const parentId = thread.parentThreadId ?? tree.root.id; - const siblings = children.get(parentId) ?? []; - siblings.push(thread); - children.set(parentId, siblings); - } - for (const siblings of children.values()) { - siblings.sort(compareThreads); - } - - const rows: CodexAgentThreadRow[] = [ - { thread: tree.root, depth: 0, root: true }, - ]; - const visited = new Set([tree.root.id]); - const appendChildren = (parentId: string, depth: number) => { - for (const thread of children.get(parentId) ?? []) { - if (visited.has(thread.id)) continue; - visited.add(thread.id); - rows.push({ thread, depth, root: false }); - appendChildren(thread.id, depth + 1); - } - }; - appendChildren(tree.root.id, 1); - - // The server validates ancestry. Keep malformed mocked or older responses - // visible instead of silently losing them in the browser. - for (const thread of [...tree.descendants].sort(compareThreads)) { - if (!visited.has(thread.id)) { - rows.push({ thread, depth: 1, root: false }); - } - } - return rows; -} - -export function codexAgentThreadName(thread: CodexThread, root = false) { - if (root) return thread.name?.trim() || "Main"; - return ( - thread.agentNickname?.trim() || - thread.name?.trim() || - thread.agentRole?.trim() || - thread.preview?.trim().split("\n", 1)[0]?.slice(0, 72) || - `Agent ${thread.id.slice(-8)}` - ); -} diff --git a/src/harnesses/codex/composer.tsx b/src/harnesses/codex/composer.tsx index 364bf5a..9fb2dc5 100644 --- a/src/harnesses/codex/composer.tsx +++ b/src/harnesses/codex/composer.tsx @@ -70,7 +70,6 @@ interface CodexComposerToolbarProps { fastEnabled: boolean; fastDisabled: boolean; onFastEnabledChange: (enabled: boolean) => void; - mentionOpenRequest?: number; contextUsedPercent?: number | null; status: CodexComposerStatus; action: ReactNode; @@ -86,7 +85,6 @@ interface CodexComposerProps { > & { value: string; }; - slashCommandMenu: ReactNode; images: readonly CodexComposerImage[]; onRemoveImage: (id: string) => void; localImages: CodexComposerLocalImage[]; @@ -96,12 +94,6 @@ interface CodexComposerProps { tone: "info" | "error"; message: string; } | null; - mode?: { - label: string; - description: string; - exitLabel: string; - onExit: () => void; - } | null; toolbar: Omit; } @@ -410,14 +402,12 @@ export function CodexComposer({ language, inputRef, inputProps, - slashCommandMenu, images, onRemoveImage, localImages, onRemoveLocalImage, attachmentError, notice, - mode, toolbar, }: CodexComposerProps) { const copy = composerCopy[language]; @@ -428,7 +418,6 @@ export function CodexComposer({ return (
- {slashCommandMenu} {images.length > 0 ? (
{images.map((image) => ( @@ -466,22 +455,6 @@ export function CodexComposer({ {notice.message}
) : null} - {mode ? ( -
- - {mode.label} - {" · "} - {mode.description} - - -
- ) : null} { - if (!mentionOpenRequest) return; - setMentionOpen(true); - setMentionQuery(""); - setMentionResults([]); - setMentionState("idle"); - onAttachmentError(""); - }, [mentionOpenRequest, onAttachmentError]); - useEffect(() => { if (!mentionOpen) return; const frame = window.requestAnimationFrame(() => diff --git a/src/harnesses/codex/conversation.tsx b/src/harnesses/codex/conversation.tsx index 8aedcf6..17f9ede 100644 --- a/src/harnesses/codex/conversation.tsx +++ b/src/harnesses/codex/conversation.tsx @@ -48,15 +48,6 @@ import { encodeCodexComposerLocalImages, } from "@/harnesses/codex/composer"; import { insertCodexFileMentions } from "@/harnesses/codex/file-mentions"; -import { - CodexSlashCommandMenu, - useCodexSlashCommandMenu, -} from "@/harnesses/codex/slash-command-menu"; -import { - CODEX_INIT_COMMAND_PROMPT, - parseCodexSlashInvocation, - type CodexSlashCommand, -} from "@/harnesses/codex/slash-commands"; import { codexModelOptionsFromNativeResult, codexReasoningEffortForModel, @@ -87,12 +78,6 @@ import { CodexTurnResult, } from "@/harnesses/codex/activity"; import { CodexSessionActivityView } from "@/harnesses/codex/session-activity-view"; -import { CodexAgentThreadsDialog } from "@/harnesses/codex/agent-threads-dialog"; -import { - CodexNativeCommandDialog, - type CodexNativeDialogMode, -} from "@/harnesses/codex/native-command-dialog"; -import { parseCodexTokenUsageView } from "@/harnesses/codex/token-usage"; import { codexContextUsedPercent, normalizeCodexThreadTokenUsage, @@ -165,10 +150,7 @@ interface ConversationProps { onInspectorTabChange: (tab: InspectorTab) => void; onInspectorWidthRatioChange: (ratio: number, persist: boolean) => void; onToggleTerminal: () => void; - onNewSession: (options?: { - title?: string; - source?: "startup" | "clear"; - }) => void; + onNewSession: () => void; onOpenEnvironmentSettings: ( tab: EnvironmentSettingsTab, options?: EnvironmentSettingsOpenOptions, @@ -223,23 +205,6 @@ interface PendingCodexSteer { phase: "submitting" | "accepted"; } -interface SubmitCodexMessageOptions { - content?: string; - collaborationMode?: "plan"; - bypassSlash?: boolean; - restoreDraft?: string; -} - -interface CodexGoalProjection { - goal: { - objective: string; - status: string; - tokenBudget: number | null; - tokensUsed: number; - timeUsedSeconds: number; - } | null; -} - export function CodexConversation({ language, timeZone, @@ -339,7 +304,6 @@ export function CodexConversation({ id: selectedModelId || session.harnessState.modelId || "default", displayName: selectedModelId || session.harnessState.modelId || "Default", isDefault: false, - supportsPersonality: false, defaultReasoningEffort: fallbackReasoningEffort, supportedReasoningEfforts: fallbackReasoningEffort ? [ @@ -363,9 +327,7 @@ export function CodexConversation({ tone: "info" | "error"; message: string; } | null>(null); - const [planMode, setPlanMode] = useState(false); const [fastMode, setFastMode] = useState(false); - const [mentionOpenRequest, setMentionOpenRequest] = useState(0); const [sending, setSending] = useState(false); const [interrupting, setInterrupting] = useState(false); const [openingAgentsFile, setOpeningAgentsFile] = useState(false); @@ -386,15 +348,6 @@ export function CodexConversation({ const [activityClock, setActivityClock] = useState(() => Date.now()); const [pendingTurn, setPendingTurn] = useState(null); const [pendingSteers, setPendingSteers] = useState([]); - const [agentThreadsOpen, setAgentThreadsOpen] = useState(false); - const [nativeDialog, setNativeDialog] = useState<{ - mode: CodexNativeDialogMode; - usageView?: "daily" | "weekly" | "cumulative"; - editGoalImmediately?: boolean; - }>(); - const [selectedAgentThreadId, setSelectedAgentThreadId] = useState< - string | undefined - >(); const composerRef = useRef(null); const scrollbarHideTimerRef = useRef(null); const pendingTurnStartedAtRef = useRef(null); @@ -600,13 +553,6 @@ export function CodexConversation({ const nativeHistoryLoading = !nativeSnapshot && !nativeHistoryError; const nativeHistorySyncing = nativeSnapshotFromCache && !nativeStreamReady && !nativeHistoryError; - const slashMenu = useCodexSlashCommandMenu({ - value: draft, - context: "session", - turnRunning, - onComplete: setComposerDraft, - onExecute: (command) => void executeSlashCommand(command, ""), - }); const { scrollRef: conversationScrollRef, contentRef: conversationContentRef, @@ -615,44 +561,6 @@ export function CodexConversation({ following: followingLatest, } = useConversationAutoScroll({ resetKey: session.id }); - const updateAgentThreadsUrl = useCallback( - (open: boolean, threadId?: string) => { - const url = new URL(window.location.href); - if (open) { - url.searchParams.set("agents", "1"); - if (threadId) { - url.searchParams.set("agent", threadId); - } else { - url.searchParams.delete("agent"); - } - } else { - url.searchParams.delete("agents"); - url.searchParams.delete("agent"); - } - window.history.replaceState(window.history.state, "", url); - }, - [], - ); - - const openAgentThreads = useCallback(() => { - setAgentThreadsOpen(true); - updateAgentThreadsUrl(true, selectedAgentThreadId); - }, [selectedAgentThreadId, updateAgentThreadsUrl]); - - const closeAgentThreads = useCallback(() => { - setAgentThreadsOpen(false); - setSelectedAgentThreadId(undefined); - updateAgentThreadsUrl(false); - }, [updateAgentThreadsUrl]); - - const selectAgentThread = useCallback( - (threadId?: string) => { - setSelectedAgentThreadId(threadId); - updateAgentThreadsUrl(true, threadId); - }, - [updateAgentThreadsUrl], - ); - useEffect(() => { sessionRef.current = session; }, [session]); @@ -728,21 +636,10 @@ export function CodexConversation({ setLocalImages([]); setAttachmentError(""); setCommandNotice(null); - setPlanMode(false); setFastMode(false); - setMentionOpenRequest(0); setSending(false); setInterrupting(false); setForkingMessageId(null); - const agentThreadUrl = new URL(window.location.href); - const requestedAgentThread = - agentThreadUrl.searchParams.get("agent")?.trim() || undefined; - setSelectedAgentThreadId(requestedAgentThread); - setAgentThreadsOpen( - agentThreadUrl.searchParams.get("agents") === "1" || - Boolean(requestedAgentThread), - ); - setNativeDialog(undefined); setNativeSnapshot(cachedSnapshot ?? null); setNativeSnapshotFromCache(Boolean(cachedSnapshot)); setLiveNotifications([]); @@ -1333,400 +1230,11 @@ export function CodexConversation({ }); } - function setComposerDraft(value: string) { - setDraft(value); - window.requestAnimationFrame(() => { - const textarea = composerRef.current; - if (!textarea) return; - textarea.focus(); - textarea.setSelectionRange(value.length, value.length); - }); - } - - function clearComposerDraft() { - setDraft(""); - window.requestAnimationFrame(() => { - const textarea = composerRef.current; - if (!textarea) return; - textarea.focus(); - }); - } - - function markNativeCommandRunning() { - const current = sessionRef.current; - const next: CodexSession = { - ...current, - status: "running", - unread: false, - completed: false, - }; - sessionRef.current = next; - onSessionChange(next); - } - - async function executeSlashCommand( - command: CodexSlashCommand, - argumentsValue: string, - ) { - if (command.unavailableWhileTurnRunning && turnRunning) { - setCommandNotice({ - tone: "error", - message: - language === "zh-CN" - ? `请等待当前 Turn 完成后再运行 /${command.name}。` - : `Wait for the current Turn to finish before running /${command.name}.`, - }); - return; - } - clearComposerDraft(); - setCommandNotice(null); - - if (command.intent === "session.new") { - onNewSession({ - ...(argumentsValue ? { title: argumentsValue } : {}), - source: command.name === "clear" ? "clear" : "startup", - }); - return; - } - if (command.intent === "composer.mention") { - setMentionOpenRequest((request) => request + 1); - return; - } - if (command.intent === "workspace.open") { - onOpenInspector("files"); - return; - } - if (command.intent === "agents.open") { - openAgentThreads(); - return; - } - if (command.intent === "environment.skills") { - onOpenEnvironmentSettings("skills"); - return; - } - if (command.intent === "environment.mcp") { - if (argumentsValue && argumentsValue.toLowerCase() !== "verbose") { - setCommandNotice({ - tone: "error", - message: - language === "zh-CN" - ? "/mcp 仅支持可选参数 verbose。" - : "/mcp only accepts the optional verbose argument.", - }); - return; - } - onOpenEnvironmentSettings("mcp", { - mcpVerbose: argumentsValue.toLowerCase() === "verbose", - }); - return; - } - if (command.intent === "environment.network") { - onOpenEnvironmentSettings("network"); - return; - } - if (command.intent === "environment.credentials") { - onOpenEnvironmentSettings("credentials"); - return; - } - if ( - command.intent === "codex.personality" || - command.intent === "codex.memories" || - command.intent === "codex.hooks" || - command.intent === "codex.processes" - ) { - setNativeDialog({ - mode: - command.intent === "codex.personality" - ? "personality" - : command.intent === "codex.memories" - ? "memories" - : command.intent === "codex.hooks" - ? "hooks" - : "processes", - }); - return; - } - if (command.intent === "codex.usage") { - const view = parseCodexTokenUsageView(argumentsValue); - if (!view) { - setCommandNotice({ - tone: "error", - message: - language === "zh-CN" - ? "/usage 参数必须是 daily、weekly 或 cumulative。" - : "/usage expects daily, weekly, or cumulative.", - }); - return; - } - setNativeDialog({ mode: "usage", usageView: view }); - return; - } - if ( - command.intent === "codex.goal" && - (!argumentsValue || argumentsValue.toLowerCase() === "edit") - ) { - setNativeDialog({ - mode: "goal", - editGoalImmediately: argumentsValue.toLowerCase() === "edit", - }); - return; - } - if (command.intent === "session.rename" && !argumentsValue) { - setNativeDialog({ mode: "rename" }); - return; - } - if (command.intent === "response.copy") { - const latest = [...visibleTimeline.entries] - .reverse() - .find( - (entry): entry is CodexMessageView => - entry.kind === "message" && - entry.role === "assistant" && - Boolean(entry.content.trim()), - ); - if (!latest) { - setCommandNotice({ - tone: "error", - message: - language === "zh-CN" - ? "当前 Session 还没有可复制的助手回复。" - : "This Session has no assistant response to copy yet.", - }); - return; - } - await copyMessage(latest); - setCommandNotice({ - tone: "info", - message: - language === "zh-CN" - ? "已复制最近一条助手回复。" - : "Copied the latest assistant response.", - }); - return; - } - if (command.intent === "composer.plan") { - setPlanMode(true); - if (argumentsValue) { - await submitMessage({ - content: argumentsValue, - collaborationMode: "plan", - bypassSlash: true, - restoreDraft: `/plan ${argumentsValue}`, - }); - } else { - setCommandNotice({ - tone: "info", - message: - language === "zh-CN" - ? "计划模式已开启,将应用到后续消息。" - : "Plan mode is active for subsequent messages.", - }); - } - return; - } - if (command.intent === "codex.init") { - await submitMessage({ - content: CODEX_INIT_COMMAND_PROMPT, - bypassSlash: true, - restoreDraft: "/init", - }); - return; - } - - const originalDraft = - draft || `/${command.name}${argumentsValue ? ` ${argumentsValue}` : ""}`; - clearComposerDraft(); - setSending(true); - setAttachmentError(""); - try { - if (command.intent === "codex.goal") { - const action = argumentsValue.toLowerCase(); - const clear = action === "clear"; - const status = - action === "pause" - ? ("paused" as const) - : action === "resume" - ? ("active" as const) - : undefined; - const response = await apiFetch>( - `/api/v1/sessions/${encodeURIComponent(session.id)}/goal`, - clear - ? { method: "DELETE" } - : { - method: "PUT", - body: JSON.stringify( - status ? { status } : { objective: argumentsValue }, - ), - }, - ); - const goal = response.data.goal; - setCommandNotice({ - tone: "info", - message: clear - ? language === "zh-CN" - ? "已清除 Codex 原生 Session 目标。" - : "Cleared the native Codex Session goal." - : goal - ? [ - language === "zh-CN" ? "目标" : "Goal", - goal.status, - goal.objective, - goal.tokenBudget === null - ? `${goal.tokensUsed} tokens` - : `${goal.tokensUsed}/${goal.tokenBudget} tokens`, - ].join(" · ") - : language === "zh-CN" - ? "当前没有 Codex Session 目标。" - : "This Codex Session has no goal.", - }); - return; - } - if (command.intent === "codex.stop") { - await apiFetch>( - `/api/v1/sessions/${encodeURIComponent( - session.id, - )}/background-terminals`, - { method: "DELETE" }, - ); - setCommandNotice({ - tone: "info", - message: - language === "zh-CN" - ? "已停止并清理所有 Codex 后台终端。" - : "Stopped and cleaned all Codex background terminals.", - }); - return; - } - if (command.intent === "session.fork") { - const response = await apiFetch>( - `/api/v1/sessions/${encodeURIComponent(session.id)}/fork`, - { method: "POST", body: JSON.stringify({}) }, - ); - onDerivedSessionCreated(response.data); - return; - } - if (command.intent === "codex.compact") { - await apiFetch>( - `/api/v1/sessions/${encodeURIComponent(session.id)}/compact`, - { method: "POST", body: JSON.stringify({}) }, - ); - markNativeCommandRunning(); - setCommandNotice({ - tone: "info", - message: - language === "zh-CN" - ? "Codex 正在压缩当前 Session 上下文。" - : "Codex is compacting this Session context.", - }); - return; - } - if (command.intent === "codex.review") { - await apiFetch>( - `/api/v1/sessions/${encodeURIComponent(session.id)}/review`, - { - method: "POST", - body: JSON.stringify( - argumentsValue ? { instructions: argumentsValue } : {}, - ), - }, - ); - markNativeCommandRunning(); - setCommandNotice({ - tone: "info", - message: - language === "zh-CN" - ? "Codex 原生代码审查已开始。" - : "The native Codex review has started.", - }); - return; - } - if (command.intent === "session.rename") { - const response = await apiFetch>( - `/api/v1/sessions/${encodeURIComponent(session.id)}/metadata`, - { - method: "PUT", - body: JSON.stringify({ title: argumentsValue }), - }, - ); - onSessionChange(response.data); - setCommandNotice({ - tone: "info", - message: - language === "zh-CN" - ? `Session 已重命名为“${response.data.title}”。` - : `Renamed the Session to “${response.data.title}”.`, - }); - return; - } - if (command.intent === "session.archive") { - const response = await apiFetch>( - `/api/v1/sessions/${encodeURIComponent(session.id)}/metadata`, - { - method: "PUT", - body: JSON.stringify({ archived: true }), - }, - ); - onSessionChange(response.data); - onNewSession(); - } - } catch (error) { - setComposerDraft(originalDraft); - setCommandNotice({ - tone: "error", - message: - error instanceof Error - ? error.message - : language === "zh-CN" - ? `无法运行 /${command.name}。` - : `Could not run /${command.name}.`, - }); - } finally { - setSending(false); - } - } - - async function submitMessage(options: SubmitCodexMessageOptions = {}) { - if (!options.bypassSlash) { - const invocation = parseCodexSlashInvocation(draft, "session"); - if (invocation.kind === "command") { - await executeSlashCommand(invocation.command, invocation.arguments); - return; - } - if (invocation.kind === "unknown") { - setCommandNotice({ - tone: "error", - message: - language === "zh-CN" - ? `未知命令 /${invocation.name}。输入 / 查看可用命令。` - : `Unknown command /${invocation.name}. Type / to see available commands.`, - }); - return; - } - if (invocation.kind === "unavailable") { - setCommandNotice({ - tone: "error", - message: - language === "zh-CN" - ? `/${invocation.command.name} 不适用于当前输入框。` - : `/${invocation.command.name} is not available in this composer.`, - }); - return; - } - if (invocation.kind === "missing-arguments") { - setCommandNotice({ - tone: "error", - message: - language === "zh-CN" - ? `/${invocation.command.name} 需要${invocation.command.argumentHint?.[language] ?? "参数"}。` - : `/${invocation.command.name} requires ${invocation.command.argumentHint?.[language] ?? "an argument"}.`, - }); - return; - } - } - const submittedDraft = options.restoreDraft ?? draft; + async function submitMessage() { + const submittedDraft = draft; const submittedImages = pastedImages; const submittedLocalImages = localImages; - const content = (options.content ?? draft).trim(); + const content = draft.trim(); if ( !content && submittedImages.length === 0 && @@ -1795,14 +1303,6 @@ export function CodexConversation({ ...(selectedReasoningEffort ? { reasoningEffort: selectedReasoningEffort } : {}), - ...((options.collaborationMode ?? - (planMode ? "plan" : undefined)) - ? { - collaborationMode: - options.collaborationMode ?? - (planMode ? ("plan" as const) : undefined), - } - : {}), ...(fastMode && selectedModel.fastServiceTier ? { serviceTier: selectedModel.fastServiceTier.id } : {}), @@ -2480,10 +1980,6 @@ export function CodexConversation({ {ui.jumpToLatest} ) : null} - {/* - Slash commands, approvals, steering and other composer behavior are Codex-native. - Do not lift them into the shared dispatcher when additional harnesses are added. - */} { setDraft(event.target.value); - slashMenu.show(); setCommandNotice(null); }, onPaste: (event) => { @@ -2504,7 +1999,6 @@ export function CodexConversation({ void addPastedImages(imageFiles); }, onKeyDown: (event) => { - if (slashMenu.handleKeyDown(event)) return; if ( shouldSubmitComposer( { @@ -2521,26 +2015,11 @@ export function CodexConversation({ void submitMessage(); } }, - "aria-controls": - slashMenu.commands.length > 0 ? slashMenu.id : undefined, - "aria-activedescendant": slashMenu.activeCommand - ? `${slashMenu.id}-${slashMenu.activeCommand.name}` - : undefined, "aria-label": ui.messageAgent(environment.codingAgent.label), placeholder: turnRunning ? ui.steerPlaceholder(environment.codingAgent.label) : ui.askPlaceholder(environment.codingAgent.label), }} - slashCommandMenu={ - - } images={pastedImages} onRemoveImage={(id) => { setPastedImages((current) => @@ -2557,23 +2036,6 @@ export function CodexConversation({ }} attachmentError={attachmentError} notice={commandNotice} - mode={ - planMode - ? { - label: language === "zh-CN" ? "计划模式" : "Plan mode", - description: - language === "zh-CN" - ? "后续消息使用 Codex 计划协作模式" - : "Subsequent messages use Codex Plan collaboration mode", - exitLabel: - language === "zh-CN" ? "退出计划模式" : "Exit Plan mode", - onExit: () => { - setPlanMode(false); - setCommandNotice(null); - }, - } - : null - } toolbar={{ environmentId: environment.id, agentLabel: environment.codingAgent.label, @@ -2597,7 +2059,6 @@ export function CodexConversation({ setFastMode(enabled); setCommandNotice(null); }, - mentionOpenRequest, contextUsedPercent, status: { state: nativeHistoryError @@ -2679,38 +2140,6 @@ export function CodexConversation({

- {nativeDialog ? ( - setNativeDialog(undefined)} - /> - ) : null} - {agentThreadsOpen ? ( - { - closeAgentThreads(); - onOpenWorkspacePath(path); - }} - onOpenFiles={(path) => { - closeAgentThreads(); - openChangedFile(path); - }} - onClose={closeAgentThreads} - /> - ) : null} {inspectorOpen || mountedInspectorEnvironmentId === environment.id ? ( { supportedReasoningEfforts: [ { id: "medium", description: "Medium" }, ], - supportsPersonality: false, }, { id: "native-next", @@ -143,7 +139,6 @@ test("stably deduplicates the live Codex catalog by native model id", () => { isDefault: false, defaultReasoningEffort: "", supportedReasoningEfforts: [], - supportsPersonality: false, }, ], ); diff --git a/src/harnesses/codex/models.ts b/src/harnesses/codex/models.ts index d593b19..ad9bb54 100644 --- a/src/harnesses/codex/models.ts +++ b/src/harnesses/codex/models.ts @@ -9,7 +9,6 @@ export interface CodexModelOption { isDefault: boolean; defaultReasoningEffort: string; supportedReasoningEfforts: CodexReasoningEffortOption[]; - supportsPersonality: boolean; fastServiceTier?: { id: string; name: string; @@ -27,7 +26,6 @@ interface NativeCodexModel { supportedReasoningEfforts?: unknown; additionalSpeedTiers?: unknown; serviceTiers?: unknown; - supportsPersonality?: unknown; } function reasoningEffortOptions(value: unknown) { @@ -144,7 +142,6 @@ export function codexModelOptionsFromNativeResult(result: unknown) { ? nativeDefaultReasoningEffort : (supportedReasoningEfforts[0]?.id ?? ""), supportedReasoningEfforts, - supportsPersonality: model.supportsPersonality === true, ...(nativeFastServiceTier ? { fastServiceTier: nativeFastServiceTier } : {}), diff --git a/src/harnesses/codex/native-capabilities.ts b/src/harnesses/codex/native-capabilities.ts index 989cd64..fa538c6 100644 --- a/src/harnesses/codex/native-capabilities.ts +++ b/src/harnesses/codex/native-capabilities.ts @@ -3,46 +3,6 @@ * the Sandbox0-pinned @openai/codex 0.144.1 protocol; they normalize native * values but must not introduce alternate harness behavior. */ -export type CodexPersonality = "friendly" | "pragmatic" | "none"; -export type CodexPersonalitySelection = Exclude; - -export interface CodexPersonalitySettings { - personality: CodexPersonality; - supported: boolean; -} - -export interface CodexTokenUsageSummary { - lifetimeTokens: number | null; - peakDailyTokens: number | null; - longestRunningTurnSec: number | null; - currentStreakDays: number | null; - longestStreakDays: number | null; -} - -export interface CodexTokenUsageDailyBucket { - startDate: string; - tokens: number; -} - -export interface CodexTokenUsage { - summary: CodexTokenUsageSummary; - dailyUsageBuckets: CodexTokenUsageDailyBucket[]; -} - -export interface CodexBackgroundTerminal { - itemId: string; - processId: string; - command: string; - cwd: string; - osPid: number | null; - cpuPercent: number | null; - rssKb: number | null; -} - -export interface CodexBackgroundTerminals { - terminals: CodexBackgroundTerminal[]; -} - export interface CodexMemoriesSettings { featureEnabled: boolean; useMemories: boolean; @@ -58,39 +18,3 @@ export function codexMemoriesFeatureToggleSettings( generateMemories: featureEnabled, }; } - -export type CodexHookTrustStatus = - | "trusted" - | "untrusted" - | "modified" - | "managed"; - -export interface CodexHook { - key: string; - eventName: string; - handlerType: string; - isManaged: boolean; - matcher: string | null; - command: string | null; - timeoutSec: number; - statusMessage: string | null; - sourcePath: string; - source: string; - pluginId: string | null; - displayOrder: number; - enabled: boolean; - currentHash: string; - trustStatus: CodexHookTrustStatus; -} - -export interface CodexHookIssue { - path?: string; - message: string; -} - -export interface CodexHooksInventory { - cwd: string; - hooks: CodexHook[]; - warnings: string[]; - errors: CodexHookIssue[]; -} diff --git a/src/harnesses/codex/native-command-dialog.module.css b/src/harnesses/codex/native-command-dialog.module.css deleted file mode 100644 index 8711f8e..0000000 --- a/src/harnesses/codex/native-command-dialog.module.css +++ /dev/null @@ -1,611 +0,0 @@ -.backdrop { - position: fixed; - z-index: 96; - inset: 0; - display: flex; - align-items: center; - justify-content: center; - padding: 20px; - background: rgb(24 24 21 / 38%); - backdrop-filter: blur(2px); -} - -.dialog { - display: grid; - width: min(760px, 100%); - max-height: min(760px, calc(100dvh - 40px)); - grid-template-rows: auto minmax(0, 1fr); - overflow: hidden; - border: 1px solid var(--line); - border-radius: 14px; - outline: 0; - background: var(--panel-strong); - box-shadow: var(--shadow-lg); -} - -.header { - display: flex; - min-height: 64px; - align-items: center; - gap: 11px; - padding: 10px 12px 10px 17px; - border-bottom: 1px solid var(--line); - background: var(--panel); -} - -.headerIcon { - display: inline-flex; - width: 34px; - height: 34px; - flex: 0 0 auto; - align-items: center; - justify-content: center; - border: 1px solid var(--line); - border-radius: 9px; - background: var(--sidebar); - color: var(--ink-soft); -} - -.headerCopy { - display: flex; - min-width: 0; - flex: 1; - flex-direction: column; - gap: 3px; -} - -.headerCopy strong { - color: var(--ink); - font-size: 13px; -} - -.headerCopy small { - overflow: hidden; - color: var(--ink-faint); - font-size: 10px; - text-overflow: ellipsis; - white-space: nowrap; -} - -.header > button, -.iconButton { - display: inline-flex; - width: 32px; - height: 32px; - align-items: center; - justify-content: center; - padding: 0; - border: 0; - border-radius: 7px; - background: transparent; - color: var(--ink-faint); - cursor: pointer; -} - -.header > button:hover, -.iconButton:hover { - background: var(--hover); - color: var(--ink); -} - -.body { - min-height: 0; - overflow: auto; - padding: 20px; - scrollbar-width: thin; -} - -.stack { - display: flex; - flex-direction: column; - gap: 12px; -} - -.choiceGrid { - display: grid; - gap: 9px; -} - -.choiceGrid > button { - display: flex; - min-height: 72px; - align-items: center; - justify-content: space-between; - gap: 16px; - padding: 13px 15px; - border: 1px solid var(--line); - border-radius: 10px; - background: var(--panel); - color: var(--ink-soft); - text-align: left; - cursor: pointer; -} - -.choiceGrid > button:hover:not(:disabled) { - border-color: color-mix(in srgb, var(--green) 35%, var(--line)); - background: color-mix(in srgb, var(--green-soft) 42%, var(--panel)); -} - -.choiceGrid > button.selectedChoice { - border-color: color-mix(in srgb, var(--green) 45%, var(--line)); - background: var(--green-soft); - color: var(--green); -} - -.choiceGrid > button > span:first-child { - display: flex; - flex-direction: column; - gap: 5px; -} - -.choiceGrid strong, -.settingRow strong { - color: var(--ink); - font-size: 12px; -} - -.choiceGrid small, -.settingRow small { - color: var(--ink-faint); - font-size: 10px; - line-height: 1.5; -} - -.toolbar { - display: flex; - min-height: 34px; - align-items: center; - justify-content: space-between; - gap: 12px; - color: var(--ink-faint); - font-size: 10px; -} - -.toolbarActions, -.actionRow, -.hookActions { - display: flex; - align-items: center; - gap: 8px; -} - -.segmented { - display: inline-flex; - padding: 3px; - border: 1px solid var(--line); - border-radius: 8px; - background: var(--sidebar); -} - -.segmented button { - padding: 6px 10px; - border: 0; - border-radius: 5px; - background: transparent; - color: var(--ink-faint); - font-size: 10px; - text-transform: capitalize; - cursor: pointer; -} - -.segmented button[aria-pressed="true"] { - background: var(--panel-strong); - color: var(--ink); - box-shadow: 0 1px 3px rgb(24 24 20 / 10%); -} - -.summaryGrid { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 8px; -} - -.summaryMetric { - display: flex; - min-width: 0; - min-height: 66px; - flex-direction: column; - justify-content: center; - gap: 6px; - padding: 10px 12px; - border: 1px solid var(--line-soft); - border-radius: 9px; - background: var(--panel); -} - -.summaryMetric small { - color: var(--ink-faint); - font-size: 9px; -} - -.summaryMetric strong { - overflow: hidden; - color: var(--ink); - font-size: 15px; - text-overflow: ellipsis; -} - -.chart { - display: flex; - height: 210px; - align-items: flex-end; - gap: 3px; - overflow-x: auto; - padding: 16px 12px 0; - border: 1px solid var(--line-soft); - border-radius: 10px; - background: - repeating-linear-gradient( - to top, - transparent 0, - transparent 51px, - var(--line-soft) 52px - ), - var(--panel); -} - -.barColumn { - display: flex; - min-width: 8px; - height: 100%; - flex: 1 0 8px; - align-items: flex-end; -} - -.barColumn i { - display: block; - width: 100%; - min-height: 3px; - border-radius: 3px 3px 1px 1px; - background: color-mix(in srgb, var(--green) 82%, var(--blue)); -} - -.chartCaption { - display: flex; - justify-content: space-between; - color: var(--ink-faint); - font-family: "SFMono-Regular", Consolas, monospace; - font-size: 8px; -} - -.settingRow { - display: flex; - min-height: 72px; - align-items: center; - justify-content: space-between; - gap: 18px; - padding: 13px 15px; - border: 1px solid var(--line-soft); - border-radius: 10px; - background: var(--panel); -} - -.settingRow > span { - display: flex; - flex-direction: column; - gap: 5px; -} - -.switch { - position: relative; - width: 34px; - height: 20px; - flex: 0 0 auto; - padding: 0; - border: 1px solid var(--line); - border-radius: 999px; - background: var(--selected); - cursor: pointer; -} - -.switch span { - position: absolute; - top: 2px; - left: 2px; - width: 14px; - height: 14px; - border-radius: 50%; - background: var(--panel-strong); - box-shadow: 0 1px 3px rgb(24 24 20 / 24%); - transition: transform 120ms ease; -} - -.switch[data-checked="true"] { - border-color: var(--green); - background: var(--green); -} - -.switch[data-checked="true"] span { - transform: translateX(14px); -} - -.switch:disabled { - opacity: 0.48; -} - -.actionRow { - flex-wrap: wrap; - padding-top: 4px; -} - -.actionRow button, -.hookActions > button, -.processRow > button { - display: inline-flex; - min-height: 32px; - align-items: center; - justify-content: center; - gap: 6px; - padding: 6px 10px; - border: 1px solid var(--line); - border-radius: 7px; - background: var(--panel); - color: var(--ink-soft); - font-size: 10px; - cursor: pointer; -} - -.actionRow button:hover:not(:disabled), -.hookActions > button:hover:not(:disabled), -.processRow > button:hover:not(:disabled) { - background: var(--hover); - color: var(--ink); -} - -.actionRow button.primaryButton { - border-color: var(--green); - background: var(--green); - color: #fff; -} - -.actionRow button.dangerButton, -.toolbar button.dangerButton { - display: inline-flex; - min-height: 30px; - align-items: center; - gap: 6px; - padding: 5px 9px; - border: 1px solid color-mix(in srgb, var(--red) 32%, var(--line)); - border-radius: 7px; - background: var(--red-soft); - color: var(--red); - font-size: 10px; - cursor: pointer; -} - -.confirmText { - color: var(--red); - font-size: 10px; -} - -.hookList, -.processList { - display: flex; - flex-direction: column; - gap: 8px; -} - -.hookRow, -.processRow { - display: flex; - align-items: center; - gap: 13px; - padding: 12px 13px; - border: 1px solid var(--line-soft); - border-radius: 9px; - background: var(--panel); -} - -.hookMain, -.processRow > div { - display: flex; - min-width: 0; - flex: 1; - flex-direction: column; - gap: 6px; -} - -.hookHeading { - display: flex; - align-items: center; - gap: 6px; -} - -.hookHeading strong { - color: var(--ink); - font-size: 11px; -} - -.hookHeading span { - padding: 2px 5px; - border: 1px solid var(--line); - border-radius: 4px; - color: var(--ink-faint); - font-size: 8px; -} - -.hookHeading span[data-tone="warning"] { - border-color: color-mix(in srgb, var(--amber) 35%, var(--line)); - background: var(--amber-soft); - color: var(--amber); -} - -.hookMain code, -.processRow code { - overflow: hidden; - color: var(--ink-soft); - font-size: 9px; - line-height: 1.45; - text-overflow: ellipsis; - white-space: nowrap; -} - -.hookMain small, -.processRow small { - overflow: hidden; - color: var(--ink-faint); - font-size: 8px; - text-overflow: ellipsis; - white-space: nowrap; -} - -.processId { - min-width: 42px; - padding: 4px 6px; - border-radius: 5px; - background: var(--sidebar); - color: var(--ink-faint); - font-family: "SFMono-Regular", Consolas, monospace; - font-size: 9px; - text-align: center; -} - -.goalStatus { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - padding: 10px 12px; - border: 1px solid var(--line-soft); - border-radius: 8px; - background: var(--panel); -} - -.goalStatus span { - padding: 3px 7px; - border-radius: 999px; - background: var(--green-soft); - color: var(--green); - font-size: 9px; - font-weight: 700; -} - -.goalStatus span:not([data-status="active"]) { - background: var(--amber-soft); - color: var(--amber); -} - -.goalStatus small { - color: var(--ink-faint); - font-size: 9px; -} - -.field { - display: flex; - flex-direction: column; - gap: 7px; - color: var(--ink-soft); - font-size: 10px; -} - -.field input, -.field textarea { - width: 100%; - padding: 10px 11px; - resize: vertical; - border: 1px solid var(--line); - border-radius: 8px; - outline: 0; - background: var(--panel); - color: var(--ink); - font-size: 12px; - line-height: 1.5; -} - -.field input:focus, -.field textarea:focus { - border-color: color-mix(in srgb, var(--blue) 55%, var(--line)); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--blue-soft) 65%, transparent); -} - -.loading, -.empty, -.error, -.notice { - padding: 12px 13px; - border-radius: 8px; - font-size: 10px; - line-height: 1.5; -} - -.loading { - display: flex; - min-height: 120px; - align-items: center; - justify-content: center; - gap: 8px; - color: var(--ink-faint); -} - -.empty { - border: 1px dashed var(--line); - background: var(--panel); - color: var(--ink-faint); - text-align: center; -} - -.error { - border: 1px solid color-mix(in srgb, var(--red) 25%, var(--line)); - background: var(--red-soft); - color: var(--red); -} - -.notice { - border: 1px solid color-mix(in srgb, var(--blue) 24%, var(--line)); - background: var(--blue-soft); - color: var(--blue); -} - -.footnote { - margin: 2px 0 0; - color: var(--ink-faint); - font-size: 9px; - line-height: 1.5; -} - -.footnote code { - color: var(--ink-soft); -} - -.spin { - animation: spin 0.9s linear infinite; -} - -@keyframes spin { - to { - transform: rotate(360deg); - } -} - -@media (max-width: 640px) { - .backdrop { - align-items: flex-end; - padding: 0; - } - - .dialog { - width: 100%; - max-height: 92dvh; - border-width: 1px 0 0; - border-radius: 14px 14px 0 0; - } - - .body { - padding: 15px; - } - - .summaryGrid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .hookRow, - .processRow { - align-items: flex-start; - flex-wrap: wrap; - } - - .hookActions { - width: 100%; - justify-content: flex-end; - } -} diff --git a/src/harnesses/codex/native-command-dialog.tsx b/src/harnesses/codex/native-command-dialog.tsx deleted file mode 100644 index 7781267..0000000 --- a/src/harnesses/codex/native-command-dialog.tsx +++ /dev/null @@ -1,1347 +0,0 @@ -"use client"; - -import { - Activity, - Brain, - Check, - Flag, - Gauge, - LoaderCircle, - RefreshCw, - SlidersHorizontal, - Sparkles, - SquareTerminal, - Trash2, - Webhook, - X, -} from "lucide-react"; -import { - useCallback, - useEffect, - useId, - useMemo, - useRef, - useState, - type ReactNode, -} from "react"; - -import { - codexMemoriesFeatureToggleSettings, - type CodexBackgroundTerminals, - type CodexHooksInventory, - type CodexMemoriesSettings, - type CodexPersonalitySelection, - type CodexPersonalitySettings, - type CodexTokenUsage, -} from "@/harnesses/codex/native-capabilities"; -import { - codexTokenUsagePoints, - type CodexTokenUsageView, -} from "@/harnesses/codex/token-usage"; -import type { CodexSession } from "@/harnesses/codex/types"; -import { apiFetch, type ApiEnvelope } from "@/lib/api-client"; -import type { OperationLanguage } from "@/lib/operation-ui"; - -import styles from "./native-command-dialog.module.css"; - -export type CodexNativeDialogMode = - | "goal" - | "hooks" - | "memories" - | "personality" - | "processes" - | "rename" - | "usage"; - -interface CodexNativeCommandDialogProps { - mode: CodexNativeDialogMode; - language: OperationLanguage; - environmentId: string; - session?: CodexSession; - initialUsageView?: CodexTokenUsageView; - editGoalImmediately?: boolean; - onSessionChange?: (session: CodexSession) => void; - onClose: () => void; -} - -const MODE_META: Record< - CodexNativeDialogMode, - { title: string; description: string; icon: typeof Activity } -> = { - goal: { - title: "Goal", - description: "Manage the persisted goal on this native Codex Thread.", - icon: Flag, - }, - hooks: { - title: "Lifecycle hooks", - description: "Review, trust, enable, or disable hooks discovered by Codex.", - icon: Webhook, - }, - memories: { - title: "Memories", - description: "Control memory use and future memory generation in this Environment.", - icon: Brain, - }, - personality: { - title: "Personality", - description: "Choose the response style used by Codex on subsequent Turns.", - icon: Sparkles, - }, - processes: { - title: "Background terminals", - description: "Native shell processes that outlived their initiating command.", - icon: SquareTerminal, - }, - rename: { - title: "Rename Session", - description: "Change the Sandpi Session title.", - icon: SlidersHorizontal, - }, - usage: { - title: "Token activity", - description: "ChatGPT account token activity reported directly by Codex.", - icon: Gauge, - }, -}; - -export function CodexNativeCommandDialog({ - mode, - language, - environmentId, - session, - initialUsageView = "daily", - editGoalImmediately = false, - onSessionChange, - onClose, -}: CodexNativeCommandDialogProps) { - const titleId = useId(); - const dialogRef = useRef(null); - const meta = MODE_META[mode]; - const Icon = meta.icon; - - useEffect(() => { - const previouslyFocused = document.activeElement as HTMLElement | null; - const previousOverflow = document.body.style.overflow; - document.body.style.overflow = "hidden"; - const frame = window.requestAnimationFrame(() => dialogRef.current?.focus()); - return () => { - window.cancelAnimationFrame(frame); - document.body.style.overflow = previousOverflow; - previouslyFocused?.focus(); - }; - }, []); - - return ( -
{ - if (event.target === event.currentTarget) onClose(); - }} - > -
{ - if (event.key === "Escape") { - event.preventDefault(); - onClose(); - return; - } - if (event.key !== "Tab") return; - const focusable = Array.from( - dialogRef.current?.querySelectorAll( - 'button:not([disabled]), input:not([disabled]), textarea:not([disabled]), [href], [tabindex]:not([tabindex="-1"])', - ) ?? [], - ); - const first = focusable[0]; - const last = focusable.at(-1); - if (event.shiftKey && document.activeElement === first) { - event.preventDefault(); - last?.focus(); - } else if (!event.shiftKey && document.activeElement === last) { - event.preventDefault(); - first?.focus(); - } - }} - > -
- - - - {meta.title} - {meta.description} - - -
-
- {mode === "personality" ? ( - - ) : mode === "usage" ? ( - - ) : mode === "memories" ? ( - - ) : mode === "hooks" ? ( - - ) : mode === "processes" && session ? ( - - ) : mode === "goal" && session ? ( - - ) : mode === "rename" && session ? ( - - ) : ( - - )} -
-
-
- ); -} - -function PersonalityPanel({ - language, - environmentId, - sessionId, -}: { - language: OperationLanguage; - environmentId: string; - sessionId?: string; -}) { - const [settings, setSettings] = useState(); - const [loading, setLoading] = useState(true); - const [busy, setBusy] = useState(); - const [error, setError] = useState(""); - const endpoint = sessionId - ? `/api/v1/sessions/${encodeURIComponent(sessionId)}/personality` - : `/api/v1/environments/${encodeURIComponent(environmentId)}/harnesses/codex/personality`; - - useEffect(() => { - const controller = new AbortController(); - void apiFetch>(endpoint, { - signal: controller.signal, - }) - .then((response) => setSettings(response.data)) - .catch((cause) => { - if (!controller.signal.aborted) { - setError(errorMessage(cause, "Could not read Codex personality.")); - } - }) - .finally(() => { - if (!controller.signal.aborted) setLoading(false); - }); - return () => controller.abort(); - }, [endpoint]); - - async function select(personality: CodexPersonalitySelection) { - if (busy || !settings?.supported) return; - setBusy(personality); - setError(""); - try { - const response = await apiFetch>( - endpoint, - { - method: "PUT", - body: JSON.stringify({ personality }), - }, - ); - setSettings(response.data); - } catch (cause) { - setError(errorMessage(cause, "Could not update Codex personality.")); - } finally { - setBusy(undefined); - } - } - - if (loading) return ; - if (error && !settings) return ; - const choices: Array<{ - id: CodexPersonalitySelection; - title: string; - detail: string; - }> = [ - { - id: "friendly", - title: language === "zh-CN" ? "友好" : "Friendly", - detail: - language === "zh-CN" - ? "更温暖、自然,也更愿意解释上下文。" - : "Warmer, more conversational, and more willing to explain context.", - }, - { - id: "pragmatic", - title: language === "zh-CN" ? "务实" : "Pragmatic", - detail: - language === "zh-CN" - ? "直接、紧凑,优先给出可执行结论。" - : "Direct and compact, prioritizing actionable outcomes.", - }, - ]; - return ( -
- {!settings?.supported ? ( - - {language === "zh-CN" - ? "当前模型不支持 personality。请选择支持该能力的 Codex 模型。" - : "The current model does not support personality. Choose a compatible Codex model first."} - - ) : null} - {error ? : null} -
- {choices.map((choice) => ( - - ))} -
- - {sessionId - ? "The choice is saved to Codex config and queued on this loaded Thread." - : "The choice is saved to this Environment's native Codex config."} - -
- ); -} - -function UsagePanel({ - language, - environmentId, - initialView, -}: { - language: OperationLanguage; - environmentId: string; - initialView: CodexTokenUsageView; -}) { - const [usage, setUsage] = useState(); - const [view, setView] = useState(initialView); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(""); - const load = useCallback(async () => { - setLoading(true); - setError(""); - try { - const response = await apiFetch>( - `/api/v1/environments/${encodeURIComponent(environmentId)}/harnesses/codex/token-usage`, - ); - setUsage(response.data); - } catch (cause) { - setError(errorMessage(cause, "Could not load Codex token activity.")); - } finally { - setLoading(false); - } - }, [environmentId]); - - useEffect(() => void load(), [load]); - const points = useMemo( - () => (usage ? codexTokenUsagePoints(usage, view) : []), - [usage, view], - ); - const maximum = Math.max(1, ...points.map((point) => point.value)); - - if (loading && !usage) return ; - return ( -
-
-
- {(["daily", "weekly", "cumulative"] as const).map((candidate) => ( - - ))} -
- -
- {error ? : null} - {usage ? ( - <> -
- - - - -
- {points.length ? ( -
- {points.map((point) => ( - - - - ))} -
- ) : ( - - {language === "zh-CN" - ? "Codex 尚未返回每日 token 活动。" - : "Codex has not returned daily token activity yet."} - - )} - {points.length ? ( -
- {points[0]?.label} - {points.at(-1)?.label} -
- ) : null} - - ) : null} - - This is account token activity from account/usage/read, not - Sandpi Sandbox billing usage. - -
- ); -} - -function MemoriesPanel({ - language, - environmentId, - sessionId, -}: { - language: OperationLanguage; - environmentId: string; - sessionId?: string; -}) { - const [settings, setSettings] = useState(); - const [draft, setDraft] = useState(); - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); - const [resetting, setResetting] = useState(false); - const [confirmReset, setConfirmReset] = useState(false); - const [notice, setNotice] = useState(""); - const [error, setError] = useState(""); - const endpoint = sessionId - ? `/api/v1/sessions/${encodeURIComponent(sessionId)}/memories` - : `/api/v1/environments/${encodeURIComponent(environmentId)}/harnesses/codex/memories`; - - useEffect(() => { - const controller = new AbortController(); - void apiFetch>(endpoint, { - signal: controller.signal, - }) - .then((response) => { - setSettings(response.data); - setDraft(response.data); - }) - .catch((cause) => { - if (!controller.signal.aborted) { - setError(errorMessage(cause, "Could not read Codex memories.")); - } - }) - .finally(() => { - if (!controller.signal.aborted) setLoading(false); - }); - return () => controller.abort(); - }, [endpoint]); - - async function save() { - if (!draft || saving) return; - setSaving(true); - setError(""); - setNotice(""); - try { - const response = await apiFetch>( - endpoint, - { method: "PUT", body: JSON.stringify(draft) }, - ); - setSettings(response.data); - setDraft(response.data); - setNotice( - language === "zh-CN" - ? "Codex 记忆设置已保存。" - : "Codex memory settings were saved.", - ); - } catch (cause) { - setError(errorMessage(cause, "Could not update Codex memories.")); - } finally { - setSaving(false); - } - } - - async function reset() { - if (!confirmReset || resetting) return; - setResetting(true); - setError(""); - try { - await apiFetch( - `/api/v1/environments/${encodeURIComponent(environmentId)}/harnesses/codex/memories`, - { method: "DELETE" }, - ); - setConfirmReset(false); - setNotice( - language === "zh-CN" - ? "本环境中的 Codex 本地记忆已清除。" - : "Local Codex memories were cleared for this Environment.", - ); - } catch (cause) { - setError(errorMessage(cause, "Could not reset Codex memories.")); - } finally { - setResetting(false); - } - } - - if (loading) return ; - if (!draft) return ; - const changed = JSON.stringify(settings) !== JSON.stringify(draft); - return ( -
- {error ? : null} - {notice ? {notice} : null} - - setDraft(codexMemoriesFeatureToggleSettings(featureEnabled)) - } - /> - - setDraft((current) => - current ? { ...current, useMemories } : current, - ) - } - /> - - setDraft((current) => - current ? { ...current, generateMemories } : current, - ) - } - /> -
- - {!confirmReset ? ( - - ) : ( - <> - - {language === "zh-CN" - ? "确认清除所有本地记忆?" - : "Clear all local memories?"} - - - - - )} -
-
- ); -} - -function HooksPanel({ - language, - environmentId, -}: { - language: OperationLanguage; - environmentId: string; -}) { - const [inventory, setInventory] = useState(); - const [loading, setLoading] = useState(true); - const [busyKey, setBusyKey] = useState(""); - const [error, setError] = useState(""); - const endpoint = `/api/v1/environments/${encodeURIComponent(environmentId)}/harnesses/codex/hooks`; - const load = useCallback(async () => { - setLoading(true); - setError(""); - try { - const response = - await apiFetch>(endpoint); - setInventory(response.data); - } catch (cause) { - setError(errorMessage(cause, "Could not load Codex hooks.")); - } finally { - setLoading(false); - } - }, [endpoint]); - - useEffect(() => void load(), [load]); - - async function update( - key: string, - update: { enabled?: boolean; trustedHash?: string }, - ) { - if (busyKey) return; - setBusyKey(key); - setError(""); - try { - const response = await apiFetch>( - endpoint, - { - method: "PUT", - body: JSON.stringify({ key, ...update }), - }, - ); - setInventory(response.data); - } catch (cause) { - setError(errorMessage(cause, "Could not update the Codex hook.")); - } finally { - setBusyKey(""); - } - } - - if (loading && !inventory) return ; - return ( -
-
- - {inventory - ? `${inventory.hooks.length} ${inventory.hooks.length === 1 ? "hook" : "hooks"}` - : "Hooks"} - - -
- {error ? : null} - {inventory?.warnings.map((warning) => ( - {warning} - ))} - {inventory?.errors.map((issue) => ( - - ))} - {inventory?.hooks.length ? ( -
- {inventory.hooks.map((hook) => { - const needsTrust = - hook.trustStatus === "untrusted" || - hook.trustStatus === "modified"; - return ( -
-
-
- {hook.eventName} - {hook.handlerType} - - {hook.trustStatus} - -
- {hook.command ?? hook.key} - - {[hook.source, hook.matcher, hook.sourcePath] - .filter(Boolean) - .join(" · ")} - -
-
- {needsTrust ? ( - - ) : null} - - void update(hook.key, { enabled }) - } - /> -
-
- ); - })} -
- ) : ( - - {language === "zh-CN" - ? "当前 Workspace 未发现 Codex hooks。" - : "No Codex hooks were discovered for this Workspace."} - - )} - - Unmanaged hook definitions must be trusted at their current hash before - Codex can run them. - -
- ); -} - -function ProcessesPanel({ - language, - sessionId, -}: { - language: OperationLanguage; - sessionId: string; -}) { - const [inventory, setInventory] = useState(); - const [loading, setLoading] = useState(true); - const [busy, setBusy] = useState(""); - const [error, setError] = useState(""); - const endpoint = `/api/v1/sessions/${encodeURIComponent(sessionId)}/background-terminals`; - const load = useCallback(async () => { - setLoading(true); - setError(""); - try { - const response = - await apiFetch>(endpoint); - setInventory(response.data); - } catch (cause) { - setError( - errorMessage(cause, "Could not load Codex background terminals."), - ); - } finally { - setLoading(false); - } - }, [endpoint]); - useEffect(() => void load(), [load]); - - async function terminate(processId: string) { - if (busy) return; - setBusy(processId); - setError(""); - try { - await apiFetch(`${endpoint}/${encodeURIComponent(processId)}`, { - method: "DELETE", - }); - await load(); - } catch (cause) { - setError(errorMessage(cause, "Could not stop the background terminal.")); - } finally { - setBusy(""); - } - } - - async function clean() { - if (busy) return; - setBusy("*"); - setError(""); - try { - await apiFetch(endpoint, { method: "DELETE" }); - setInventory({ terminals: [] }); - } catch (cause) { - setError(errorMessage(cause, "Could not stop background terminals.")); - } finally { - setBusy(""); - } - } - - if (loading && !inventory) { - return ; - } - return ( -
-
- - {inventory?.terminals.length ?? 0}{" "} - {language === "zh-CN" ? "个后台终端" : "background terminals"} - -
- - -
-
- {error ? : null} - {inventory?.terminals.length ? ( -
- {inventory.terminals.map((terminal) => ( -
- {terminal.processId} -
- {terminal.command} - - {terminal.cwd} - {terminal.osPid !== null ? ` · PID ${terminal.osPid}` : ""} - {terminal.cpuPercent !== null - ? ` · ${terminal.cpuPercent.toFixed(1)}% CPU` - : ""} - {terminal.rssKb !== null - ? ` · ${formatBytes(terminal.rssKb * 1024)}` - : ""} - -
- -
- ))} -
- ) : ( - - {language === "zh-CN" - ? "当前 Thread 没有后台终端。" - : "This Thread has no background terminals."} - - )} -
- ); -} - -interface GoalProjection { - goal: { - objective: string; - status: string; - tokenBudget: number | null; - tokensUsed: number; - timeUsedSeconds: number; - } | null; -} - -function GoalPanel({ - language, - sessionId, - editImmediately, -}: { - language: OperationLanguage; - sessionId: string; - editImmediately: boolean; -}) { - const inputRef = useRef(null); - const [projection, setProjection] = useState(); - const [objective, setObjective] = useState(""); - const [loading, setLoading] = useState(true); - const [busy, setBusy] = useState(""); - const [error, setError] = useState(""); - const endpoint = `/api/v1/sessions/${encodeURIComponent(sessionId)}/goal`; - useEffect(() => { - const controller = new AbortController(); - void apiFetch>(endpoint, { - signal: controller.signal, - }) - .then((response) => { - setProjection(response.data); - setObjective(response.data.goal?.objective ?? ""); - if (editImmediately) { - window.requestAnimationFrame(() => { - inputRef.current?.focus(); - inputRef.current?.select(); - }); - } - }) - .catch((cause) => { - if (!controller.signal.aborted) { - setError(errorMessage(cause, "Could not load the Codex goal.")); - } - }) - .finally(() => { - if (!controller.signal.aborted) setLoading(false); - }); - return () => controller.abort(); - }, [editImmediately, endpoint]); - - async function update( - body: { objective?: string; status?: "active" | "paused" }, - action: string, - ) { - if (busy) return; - setBusy(action); - setError(""); - try { - const response = await apiFetch>(endpoint, { - method: "PUT", - body: JSON.stringify(body), - }); - setProjection(response.data); - setObjective(response.data.goal?.objective ?? ""); - } catch (cause) { - setError(errorMessage(cause, "Could not update the Codex goal.")); - } finally { - setBusy(""); - } - } - - async function clear() { - if (busy) return; - setBusy("clear"); - setError(""); - try { - const response = await apiFetch>(endpoint, { - method: "DELETE", - }); - setProjection(response.data); - setObjective(""); - } catch (cause) { - setError(errorMessage(cause, "Could not clear the Codex goal.")); - } finally { - setBusy(""); - } - } - - if (loading) return ; - return ( -
- {error ? : null} - {projection?.goal ? ( -
- - {projection.goal.status} - - - {projection.goal.tokenBudget === null - ? `${projection.goal.tokensUsed.toLocaleString()} tokens` - : `${projection.goal.tokensUsed.toLocaleString()} / ${projection.goal.tokenBudget.toLocaleString()} tokens`} - {" · "} - {formatDuration(projection.goal.timeUsedSeconds)} - -
- ) : ( - - {language === "zh-CN" - ? "当前 Thread 还没有目标。" - : "This Thread does not have a goal yet."} - - )} -