mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 17:30:31 +00:00
initial commit
This commit is contained in:
commit
fdf94feace
671 changed files with 24986 additions and 0 deletions
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
node_modules/
|
||||
main.js
|
||||
data.json
|
||||
*.log
|
||||
.DS_Store
|
||||
dist/
|
||||
tmp/
|
||||
4
.prettierrc.json
Normal file
4
.prettierrc.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"printWidth": 140,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2026 murashit
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
112
README.md
Normal file
112
README.md
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
# Codex Panel
|
||||
|
||||
Codex Panel is a desktop-only Obsidian plugin that opens a right-sidebar panel for Codex. It starts `codex app-server` locally over stdio and uses the current vault root as the Codex working directory.
|
||||
|
||||
The plugin does not manage Codex runtime policy itself. Model, reasoning effort, sandbox, approvals, network access, MCP, hooks, and related behavior are resolved by Codex for the vault root, including a vault-local `.codex/config.toml` when present.
|
||||
|
||||
## Status
|
||||
|
||||
Codex Panel is currently distributed as a beta plugin for BRAT/manual installation. It is developed and tested with Codex CLI 0.130.0.
|
||||
|
||||
The plugin depends on the experimental `codex app-server` API. Later Codex CLI releases may require regenerated app-server bindings or compatibility fixes.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Obsidian desktop app.
|
||||
- Codex CLI installed, authenticated, and available as `codex`, or a custom executable path configured in the plugin settings.
|
||||
- A vault where Codex is allowed to work according to your Codex CLI configuration.
|
||||
|
||||
## Installation
|
||||
|
||||
### BRAT
|
||||
|
||||
1. Install and enable the BRAT plugin in Obsidian.
|
||||
2. In BRAT, choose `Add Beta plugin`.
|
||||
3. Enter the repository URL:
|
||||
|
||||
```text
|
||||
https://github.com/murashit/codex-panel
|
||||
```
|
||||
|
||||
4. Enable `Codex Panel` from Obsidian's Community plugins list.
|
||||
5. Open the panel from the ribbon or command palette.
|
||||
|
||||
### Manual
|
||||
|
||||
Download the release assets and place them in:
|
||||
|
||||
```text
|
||||
<vault>/.obsidian/plugins/codex-panel/
|
||||
```
|
||||
|
||||
Required release assets:
|
||||
|
||||
- `main.js`
|
||||
- `manifest.json`
|
||||
- `styles.css`
|
||||
|
||||
Then reload Obsidian and enable `Codex Panel` from Community plugins.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Open the command palette and run:
|
||||
|
||||
- `Codex Panel: Open panel`
|
||||
- `Codex Panel: New chat`
|
||||
|
||||
If Obsidian cannot find `codex`, open the plugin settings and set `Codex executable` to an absolute path such as `/opt/homebrew/bin/codex`.
|
||||
|
||||
The status dot in the panel opens connection controls, diagnostics, usage limits, and the effective Codex config for the current vault. The effective config view is diagnostic only; it does not save settings.
|
||||
|
||||
## Features
|
||||
|
||||
- Start, resume, rename, and archive Codex threads from Obsidian.
|
||||
- Stream user, assistant, reasoning, command, tool, hook, and file-change events.
|
||||
- Respond to command, file, permission, and Plan mode approval requests.
|
||||
- Toggle Plan mode, fast mode, model override, and reasoning effort override for subsequent turns.
|
||||
- Send steering messages during a running turn, or interrupt the turn when the composer is empty.
|
||||
- Show recent chat history, paged older turns, context usage, connection diagnostics, and effective config.
|
||||
- Inspect and manage discovered Codex hooks, including enabled state, trust status, and current hash.
|
||||
- Complete vault wikilinks, slash commands, and enabled Codex skills in the composer.
|
||||
- Resolve Obsidian wikilinks in sent messages into Codex file mentions when the target exists.
|
||||
|
||||
## Slash Commands
|
||||
|
||||
- `/compact`: request context compaction for the active thread.
|
||||
- `/fast`: toggle fast service tier for subsequent turns.
|
||||
- `/plan`: toggle Plan mode for subsequent turns. Add text after the command to toggle mode and send that text immediately, for example `/plan OK, implement it`.
|
||||
- `/status`: show current connection, thread, runtime, and context status.
|
||||
- `/doctor`: show connection diagnostics.
|
||||
- `/model`: show, set, or clear model override.
|
||||
- `/effort`: show, set, or clear reasoning effort override.
|
||||
- `/help`: list slash commands.
|
||||
|
||||
## Privacy and Security
|
||||
|
||||
Codex Panel does not make its own network requests. It exchanges data with the configured `codex app-server` process over stdio. Messages, steering input, approvals, resolved file mentions, thread history requests, hook status requests, effective config requests, and archived-thread management requests are sent to Codex app-server and then handled according to the user's Codex CLI configuration, authentication, model provider, sandbox, approval, MCP, hook, and network settings.
|
||||
|
||||
The plugin stores only panel settings in Obsidian plugin data: the Codex executable path and optional automatic thread naming model/effort overrides. It does not store API keys.
|
||||
|
||||
Obsidian wikilinks in sent messages are resolved into structured file mentions only when the target file exists. The visible message text is preserved, unresolved wikilinks are not expanded, and note bodies are not automatically attached by the panel.
|
||||
|
||||
## Development
|
||||
|
||||
```sh
|
||||
npm ci
|
||||
npm run format
|
||||
npm run check
|
||||
npm run build
|
||||
```
|
||||
|
||||
Run `npm run format` after edits and before `npm run check` so Prettier-only issues are fixed upfront. `npm run check` runs TypeScript type checking, unit tests, ESLint, Prettier check, and a production esbuild bundle.
|
||||
|
||||
`main.js`, `data.json`, and `node_modules/` are ignored by Git. `main.js` is still the file Obsidian loads, so run `npm run build` or `npm run build:prod` after source changes.
|
||||
|
||||
The app-server TypeScript bindings in `src/generated/app-server/` are generated from the installed Codex CLI:
|
||||
|
||||
```sh
|
||||
npm run generate:app-server-types
|
||||
npm run check
|
||||
```
|
||||
|
||||
The generation script uses `codex app-server generate-ts --experimental` because the panel depends on experimental app-server fields such as collaboration mode and generated v2 types.
|
||||
25
esbuild.config.mjs
Normal file
25
esbuild.config.mjs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import esbuild from "esbuild";
|
||||
|
||||
const production = process.argv.includes("--production");
|
||||
const watch = process.argv.includes("--watch");
|
||||
|
||||
const context = await esbuild.context({
|
||||
entryPoints: ["src/main.ts"],
|
||||
bundle: true,
|
||||
external: ["obsidian", "electron"],
|
||||
format: "cjs",
|
||||
platform: "node",
|
||||
target: "es2022",
|
||||
outfile: "main.js",
|
||||
sourcemap: production ? false : "inline",
|
||||
minify: production,
|
||||
logLevel: "info",
|
||||
});
|
||||
|
||||
if (watch) {
|
||||
await context.watch();
|
||||
console.log("Watching Codex Panel plugin...");
|
||||
} else {
|
||||
await context.rebuild();
|
||||
await context.dispose();
|
||||
}
|
||||
38
eslint.config.mjs
Normal file
38
eslint.config.mjs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import tsParser from "@typescript-eslint/parser";
|
||||
import tsPlugin from "@typescript-eslint/eslint-plugin";
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: ["main.js", "node_modules/**", "src/generated/**"],
|
||||
},
|
||||
{
|
||||
files: ["src/**/*.ts", "tests/**/*.ts"],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: "module",
|
||||
parser: tsParser,
|
||||
parserOptions: {
|
||||
project: "./tsconfig.json",
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
globals: {
|
||||
HTMLElement: "readonly",
|
||||
HTMLTextAreaElement: "readonly",
|
||||
KeyboardEvent: "readonly",
|
||||
NodeJS: "readonly",
|
||||
console: "readonly",
|
||||
document: "readonly",
|
||||
requestAnimationFrame: "readonly",
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
"@typescript-eslint": tsPlugin,
|
||||
},
|
||||
rules: {
|
||||
"@typescript-eslint/consistent-type-imports": "error",
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
||||
},
|
||||
},
|
||||
];
|
||||
9
manifest.json
Normal file
9
manifest.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"id": "codex-panel",
|
||||
"name": "Codex Panel",
|
||||
"version": "0.1.0",
|
||||
"minAppVersion": "1.5.0",
|
||||
"description": "Run Codex from an Obsidian side panel.",
|
||||
"author": "murashit",
|
||||
"isDesktopOnly": true
|
||||
}
|
||||
3594
package-lock.json
generated
Normal file
3594
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
40
package.json
Normal file
40
package.json
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"name": "codex-panel",
|
||||
"version": "0.1.0",
|
||||
"description": "Obsidian side panel for Codex app-server.",
|
||||
"main": "main.js",
|
||||
"author": "murashit",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/murashit/codex-panel.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/murashit/codex-panel/issues"
|
||||
},
|
||||
"homepage": "https://github.com/murashit/codex-panel#readme",
|
||||
"scripts": {
|
||||
"build": "node esbuild.config.mjs",
|
||||
"build:prod": "node esbuild.config.mjs --production",
|
||||
"dev": "node esbuild.config.mjs --watch",
|
||||
"format": "prettier --write \"src/**/*.{ts,tsx,js,mjs,json,css,md}\" \"tests/**/*.{ts,tsx,js,mjs,json,css,md}\" \"*.{json,mjs,md,css}\" \"!src/generated/**\"",
|
||||
"format:check": "prettier --check \"src/**/*.{ts,tsx,js,mjs,json,css,md}\" \"tests/**/*.{ts,tsx,js,mjs,json,css,md}\" \"*.{json,mjs,md,css}\" \"!src/generated/**\"",
|
||||
"generate:app-server-types": "codex app-server generate-ts --experimental --out src/generated/app-server",
|
||||
"lint": "eslint src tests --max-warnings=0",
|
||||
"test": "vitest run",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"check": "npm run typecheck && npm run test && npm run lint && npm run format:check && npm run build:prod"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.59.2",
|
||||
"@typescript-eslint/parser": "^8.59.2",
|
||||
"esbuild": "^0.25.0",
|
||||
"eslint": "^10.3.0",
|
||||
"jsdom": "^29.1.1",
|
||||
"obsidian": "^1.12.3",
|
||||
"prettier": "^3.8.3",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^4.1.5"
|
||||
}
|
||||
}
|
||||
421
src/app-server/client.ts
Normal file
421
src/app-server/client.ts
Normal file
|
|
@ -0,0 +1,421 @@
|
|||
import type { InitializeResponse } from "../generated/app-server/InitializeResponse";
|
||||
import type { CollaborationMode } from "../generated/app-server/CollaborationMode";
|
||||
import type { RequestId } from "../generated/app-server/RequestId";
|
||||
import type { ReasoningEffort } from "../generated/app-server/ReasoningEffort";
|
||||
import type { ConfigReadResponse } from "../generated/app-server/v2/ConfigReadResponse";
|
||||
import type { ConfigWriteResponse } from "../generated/app-server/v2/ConfigWriteResponse";
|
||||
import type { GetAccountRateLimitsResponse } from "../generated/app-server/v2/GetAccountRateLimitsResponse";
|
||||
import type { HookMetadata } from "../generated/app-server/v2/HookMetadata";
|
||||
import type { HooksListResponse } from "../generated/app-server/v2/HooksListResponse";
|
||||
import type { ModelListResponse } from "../generated/app-server/v2/ModelListResponse";
|
||||
import type { SkillsListResponse } from "../generated/app-server/v2/SkillsListResponse";
|
||||
import type { ThreadArchiveResponse } from "../generated/app-server/v2/ThreadArchiveResponse";
|
||||
import type { ThreadListResponse } from "../generated/app-server/v2/ThreadListResponse";
|
||||
import type { ThreadResumeResponse } from "../generated/app-server/v2/ThreadResumeResponse";
|
||||
import type { ThreadSetNameResponse } from "../generated/app-server/v2/ThreadSetNameResponse";
|
||||
import type { SortDirection } from "../generated/app-server/v2/SortDirection";
|
||||
import type { ThreadStartResponse } from "../generated/app-server/v2/ThreadStartResponse";
|
||||
import type { ThreadTurnsListResponse } from "../generated/app-server/v2/ThreadTurnsListResponse";
|
||||
import type { ThreadUnarchiveResponse } from "../generated/app-server/v2/ThreadUnarchiveResponse";
|
||||
import type { TurnItemsView } from "../generated/app-server/v2/TurnItemsView";
|
||||
import type { TurnStartResponse } from "../generated/app-server/v2/TurnStartResponse";
|
||||
import type { TurnSteerResponse } from "../generated/app-server/v2/TurnSteerResponse";
|
||||
import type { UserInput } from "../generated/app-server/v2/UserInput";
|
||||
import { CLIENT_VERSION } from "../constants";
|
||||
import { StdioAppServerTransport, type AppServerTransport, type AppServerTransportHandlers } from "./transport";
|
||||
import type { ClientRequestMethod, ClientRequestParams, PendingRequest, RpcInboundMessage, RpcOutboundMessage } from "./types";
|
||||
import type { ServerNotification } from "../generated/app-server/ServerNotification";
|
||||
import type { ServerRequest } from "../generated/app-server/ServerRequest";
|
||||
import type { JsonValue } from "../generated/app-server/serde_json/JsonValue";
|
||||
import type { ServiceTierRequest } from "./service-tier";
|
||||
|
||||
const DEFAULT_REQUEST_TIMEOUT_MS = 120_000;
|
||||
|
||||
export interface AppServerClientHandlers {
|
||||
onNotification: (notification: ServerNotification) => void;
|
||||
onServerRequest: (request: ServerRequest) => void;
|
||||
onLog: (message: string) => void;
|
||||
onExit: (code: number | null, signal: NodeJS.Signals | null) => void;
|
||||
}
|
||||
|
||||
export type AppServerTransportFactory = (handlers: AppServerTransportHandlers) => AppServerTransport;
|
||||
|
||||
interface ClientResponseByMethod {
|
||||
initialize: InitializeResponse;
|
||||
"config/batchWrite": ConfigWriteResponse;
|
||||
"config/read": ConfigReadResponse;
|
||||
"hooks/list": HooksListResponse;
|
||||
"thread/start": ThreadStartResponse;
|
||||
"thread/resume": ThreadResumeResponse;
|
||||
"thread/list": ThreadListResponse;
|
||||
"thread/archive": ThreadArchiveResponse;
|
||||
"thread/unarchive": ThreadUnarchiveResponse;
|
||||
"thread/name/set": ThreadSetNameResponse;
|
||||
"thread/turns/list": ThreadTurnsListResponse;
|
||||
"skills/list": SkillsListResponse;
|
||||
"model/list": ModelListResponse;
|
||||
"account/rateLimits/read": GetAccountRateLimitsResponse;
|
||||
"thread/compact/start": Record<string, never>;
|
||||
"turn/start": TurnStartResponse;
|
||||
"turn/steer": TurnSteerResponse;
|
||||
"turn/interrupt": unknown;
|
||||
}
|
||||
|
||||
type TypedClientRequestMethod = Extract<ClientRequestMethod, keyof ClientResponseByMethod>;
|
||||
|
||||
function toUserInput(input: string | UserInput[]): UserInput[] {
|
||||
if (typeof input !== "string") return input;
|
||||
return [{ type: "text", text: input, text_elements: [] }];
|
||||
}
|
||||
|
||||
export class AppServerClient {
|
||||
private transport: AppServerTransport | null = null;
|
||||
private nextId = 1;
|
||||
private pending = new Map<RequestId, PendingRequest>();
|
||||
private initialized = false;
|
||||
private initResponse: InitializeResponse | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly codexPath: string,
|
||||
private readonly cwd: string,
|
||||
private readonly handlers: AppServerClientHandlers,
|
||||
private readonly requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS,
|
||||
private readonly transportFactory?: AppServerTransportFactory,
|
||||
) {}
|
||||
|
||||
async connect(): Promise<InitializeResponse> {
|
||||
if (this.transport?.isRunning()) {
|
||||
throw new Error("Codex app-server is already running.");
|
||||
}
|
||||
|
||||
const transportHandlers: AppServerTransportHandlers = {
|
||||
onLine: (line) => this.handleLine(line),
|
||||
onLog: this.handlers.onLog,
|
||||
onError: (error) => this.rejectAll(error),
|
||||
onExit: (code, signal) => {
|
||||
this.initialized = false;
|
||||
this.initResponse = null;
|
||||
this.rejectAll(new Error(`Codex app-server exited: ${code ?? signal ?? "unknown"}`));
|
||||
this.handlers.onExit(code, signal);
|
||||
},
|
||||
};
|
||||
this.transport = this.transportFactory
|
||||
? this.transportFactory(transportHandlers)
|
||||
: new StdioAppServerTransport(this.codexPath, this.cwd, transportHandlers);
|
||||
this.transport.start();
|
||||
|
||||
const init = await this.request("initialize", {
|
||||
clientInfo: {
|
||||
name: "obsidian_codex_panel",
|
||||
title: "Codex Panel",
|
||||
version: CLIENT_VERSION,
|
||||
},
|
||||
capabilities: {
|
||||
experimentalApi: true,
|
||||
},
|
||||
});
|
||||
this.notify({ method: "initialized" });
|
||||
this.initialized = true;
|
||||
this.initResponse = init;
|
||||
return init;
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
this.initialized = false;
|
||||
this.initResponse = null;
|
||||
this.transport?.stop();
|
||||
this.transport = null;
|
||||
this.rejectAll(new Error("Codex app-server disconnected."));
|
||||
}
|
||||
|
||||
isConnected(): boolean {
|
||||
return this.initialized && this.transport !== null && this.transport.isRunning();
|
||||
}
|
||||
|
||||
get initializeResponse(): InitializeResponse {
|
||||
if (!this.initResponse) throw new Error("Codex app-server has not initialized.");
|
||||
return this.initResponse;
|
||||
}
|
||||
|
||||
readEffectiveConfig(cwd: string): Promise<ConfigReadResponse> {
|
||||
return this.request("config/read", { cwd, includeLayers: false });
|
||||
}
|
||||
|
||||
listHooks(cwd: string): Promise<HooksListResponse> {
|
||||
return this.request("hooks/list", { cwds: [cwd] });
|
||||
}
|
||||
|
||||
trustHook(hook: HookMetadata): Promise<ConfigWriteResponse> {
|
||||
return this.writeHookState(hook.key, {
|
||||
enabled: true,
|
||||
trusted_hash: hook.currentHash,
|
||||
});
|
||||
}
|
||||
|
||||
setHookEnabled(hook: HookMetadata, enabled: boolean): Promise<ConfigWriteResponse> {
|
||||
const state: { [key: string]: JsonValue } = { enabled };
|
||||
if (hook.trustStatus === "trusted") {
|
||||
state.trusted_hash = hook.currentHash;
|
||||
}
|
||||
return this.writeHookState(hook.key, state);
|
||||
}
|
||||
|
||||
startThread(cwd: string, serviceTier?: ServiceTierRequest): Promise<ThreadStartResponse> {
|
||||
return this.request("thread/start", {
|
||||
cwd,
|
||||
serviceName: "codex-panel",
|
||||
...(serviceTier !== undefined ? { serviceTier } : {}),
|
||||
experimentalRawEvents: false,
|
||||
persistExtendedHistory: false,
|
||||
});
|
||||
}
|
||||
|
||||
startEphemeralThread(cwd: string, serviceName: string, developerInstructions: string): Promise<ThreadStartResponse> {
|
||||
return this.request("thread/start", {
|
||||
cwd,
|
||||
serviceName,
|
||||
developerInstructions,
|
||||
ephemeral: true,
|
||||
sandbox: "read-only",
|
||||
approvalPolicy: "never",
|
||||
environments: [],
|
||||
experimentalRawEvents: false,
|
||||
persistExtendedHistory: false,
|
||||
});
|
||||
}
|
||||
|
||||
resumeThread(threadId: string, cwd: string): Promise<ThreadResumeResponse> {
|
||||
return this.request("thread/resume", {
|
||||
threadId,
|
||||
cwd,
|
||||
excludeTurns: true,
|
||||
persistExtendedHistory: false,
|
||||
});
|
||||
}
|
||||
|
||||
listThreads(cwd: string, archived = false): Promise<ThreadListResponse> {
|
||||
return this.request("thread/list", {
|
||||
cwd,
|
||||
limit: 20,
|
||||
archived,
|
||||
sortKey: "updated_at",
|
||||
sortDirection: "desc",
|
||||
});
|
||||
}
|
||||
|
||||
archiveThread(threadId: string): Promise<ThreadArchiveResponse> {
|
||||
return this.request("thread/archive", { threadId });
|
||||
}
|
||||
|
||||
unarchiveThread(threadId: string): Promise<ThreadUnarchiveResponse> {
|
||||
return this.request("thread/unarchive", { threadId });
|
||||
}
|
||||
|
||||
setThreadName(threadId: string, name: string): Promise<ThreadSetNameResponse> {
|
||||
return this.request("thread/name/set", { threadId, name });
|
||||
}
|
||||
|
||||
threadTurnsList(
|
||||
threadId: string,
|
||||
cursor: string | null = null,
|
||||
limit = 20,
|
||||
sortDirection: SortDirection = "desc",
|
||||
itemsView: TurnItemsView = "full",
|
||||
): Promise<ThreadTurnsListResponse> {
|
||||
return this.request("thread/turns/list", {
|
||||
threadId,
|
||||
cursor,
|
||||
limit,
|
||||
sortDirection,
|
||||
itemsView,
|
||||
});
|
||||
}
|
||||
|
||||
listSkills(cwd: string): Promise<SkillsListResponse> {
|
||||
return this.request("skills/list", {
|
||||
cwds: [cwd],
|
||||
forceReload: false,
|
||||
});
|
||||
}
|
||||
|
||||
listModels(includeHidden = false): Promise<ModelListResponse> {
|
||||
return this.request("model/list", {
|
||||
includeHidden,
|
||||
limit: 100,
|
||||
});
|
||||
}
|
||||
|
||||
readAccountRateLimits(): Promise<GetAccountRateLimitsResponse> {
|
||||
return this.request("account/rateLimits/read", undefined);
|
||||
}
|
||||
|
||||
compactThread(threadId: string): Promise<Record<string, never>> {
|
||||
return this.request("thread/compact/start", { threadId });
|
||||
}
|
||||
|
||||
startTurn(
|
||||
threadId: string,
|
||||
cwd: string,
|
||||
input: string | UserInput[],
|
||||
serviceTier?: ServiceTierRequest,
|
||||
collaborationMode?: CollaborationMode | null,
|
||||
model?: string | null,
|
||||
effort?: ReasoningEffort | null,
|
||||
): Promise<TurnStartResponse> {
|
||||
const params: ClientRequestParams<"turn/start"> & { collaborationMode?: CollaborationMode | null } = {
|
||||
threadId,
|
||||
cwd,
|
||||
...(serviceTier !== undefined ? { serviceTier } : {}),
|
||||
input: toUserInput(input),
|
||||
};
|
||||
if (collaborationMode) params.collaborationMode = collaborationMode;
|
||||
if (model !== undefined) params.model = model;
|
||||
if (effort !== undefined) params.effort = effort;
|
||||
return this.request("turn/start", params);
|
||||
}
|
||||
|
||||
startStructuredTurn(
|
||||
threadId: string,
|
||||
cwd: string,
|
||||
text: string,
|
||||
outputSchema: JsonValue,
|
||||
model?: string,
|
||||
effort?: ReasoningEffort,
|
||||
): Promise<TurnStartResponse> {
|
||||
const params: ClientRequestParams<"turn/start"> = {
|
||||
threadId,
|
||||
cwd,
|
||||
input: [
|
||||
{
|
||||
type: "text",
|
||||
text,
|
||||
text_elements: [],
|
||||
},
|
||||
],
|
||||
outputSchema,
|
||||
};
|
||||
if (model !== undefined) params.model = model;
|
||||
if (effort !== undefined) params.effort = effort;
|
||||
return this.request("turn/start", params);
|
||||
}
|
||||
|
||||
steerTurn(threadId: string, expectedTurnId: string, input: string | UserInput[]): Promise<TurnSteerResponse> {
|
||||
return this.request("turn/steer", {
|
||||
threadId,
|
||||
expectedTurnId,
|
||||
input: toUserInput(input),
|
||||
});
|
||||
}
|
||||
|
||||
interruptTurn(threadId: string, turnId: string): Promise<unknown> {
|
||||
return this.request("turn/interrupt", { threadId, turnId });
|
||||
}
|
||||
|
||||
private writeHookState(key: string, state: { [key: string]: JsonValue }): Promise<ConfigWriteResponse> {
|
||||
return this.request("config/batchWrite", {
|
||||
edits: [
|
||||
{
|
||||
keyPath: "hooks.state",
|
||||
value: {
|
||||
[key]: state,
|
||||
},
|
||||
mergeStrategy: "upsert",
|
||||
},
|
||||
],
|
||||
reloadUserConfig: true,
|
||||
});
|
||||
}
|
||||
|
||||
respondToServerRequest(requestId: RequestId, result: unknown): void {
|
||||
this.send({ id: requestId, result });
|
||||
}
|
||||
|
||||
rejectServerRequest(requestId: RequestId, code: number, message: string): void {
|
||||
this.send({ id: requestId, error: { code, message } });
|
||||
}
|
||||
|
||||
private request<M extends TypedClientRequestMethod>(method: M, params: ClientRequestParams<M>): Promise<ClientResponseByMethod[M]> {
|
||||
const id = this.nextId++;
|
||||
const promise = new Promise<ClientResponseByMethod[M]>((resolve, reject) => {
|
||||
const timeout = globalThis.setTimeout(() => {
|
||||
this.pending.delete(id);
|
||||
reject(new Error(`Codex app-server request timed out: ${method}`));
|
||||
}, this.requestTimeoutMs);
|
||||
this.pending.set(id, {
|
||||
method,
|
||||
resolve: resolve as (value: unknown) => void,
|
||||
reject,
|
||||
timeout,
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
this.send({ id, method, params } as RpcOutboundMessage);
|
||||
} catch (error) {
|
||||
const pending = this.pending.get(id);
|
||||
if (pending) {
|
||||
globalThis.clearTimeout(pending.timeout);
|
||||
this.pending.delete(id);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
private notify(message: RpcOutboundMessage): void {
|
||||
this.send(message);
|
||||
}
|
||||
|
||||
private send(message: RpcOutboundMessage): void {
|
||||
if (!this.transport?.isRunning()) {
|
||||
throw new Error("Codex app-server is not running.");
|
||||
}
|
||||
this.transport.send(message);
|
||||
}
|
||||
|
||||
private handleLine(line: string): void {
|
||||
if (line.trim().length === 0) return;
|
||||
|
||||
let message: RpcInboundMessage;
|
||||
try {
|
||||
message = JSON.parse(line) as RpcInboundMessage;
|
||||
} catch {
|
||||
this.handlers.onLog(`Invalid app-server JSON: ${line}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if ("id" in message && "method" in message) {
|
||||
this.handlers.onServerRequest(message as ServerRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
if ("id" in message) {
|
||||
const pending = this.pending.get(message.id);
|
||||
if (!pending) {
|
||||
this.handlers.onLog(`Orphan app-server response: ${JSON.stringify(message)}`);
|
||||
return;
|
||||
}
|
||||
globalThis.clearTimeout(pending.timeout);
|
||||
this.pending.delete(message.id);
|
||||
if ("error" in message && message.error) {
|
||||
pending.reject(new Error(message.error.message || "Codex app-server request failed."));
|
||||
} else {
|
||||
pending.resolve(message.result);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ("method" in message) {
|
||||
this.handlers.onNotification(message as ServerNotification);
|
||||
}
|
||||
}
|
||||
|
||||
private rejectAll(error: Error): void {
|
||||
for (const pending of this.pending.values()) {
|
||||
globalThis.clearTimeout(pending.timeout);
|
||||
pending.reject(error);
|
||||
}
|
||||
this.pending.clear();
|
||||
}
|
||||
}
|
||||
31
src/app-server/compatibility.ts
Normal file
31
src/app-server/compatibility.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import type { InitializeResponse } from "../generated/app-server/InitializeResponse";
|
||||
|
||||
export type CapabilityProbeState = "unknown" | "ok" | "failed";
|
||||
|
||||
export interface AppServerCompatibility {
|
||||
modelList: CapabilityProbeState;
|
||||
modelListError: string | null;
|
||||
}
|
||||
|
||||
export function createAppServerCompatibility(): AppServerCompatibility {
|
||||
return {
|
||||
modelList: "unknown",
|
||||
modelListError: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function appServerIdentity(initializeResponse: InitializeResponse | null): string {
|
||||
return initializeResponse?.userAgent || "(not connected)";
|
||||
}
|
||||
|
||||
export function appServerPlatform(initializeResponse: InitializeResponse | null): string {
|
||||
if (!initializeResponse) return "(not connected)";
|
||||
const family = initializeResponse.platformFamily || "unknown";
|
||||
const os = initializeResponse.platformOs || "unknown";
|
||||
return `${os}/${family}`;
|
||||
}
|
||||
|
||||
export function compatibilitySummary(compatibility: AppServerCompatibility): string {
|
||||
const modelList = compatibility.modelList === "failed" ? `model/list failed` : `model/list ${compatibility.modelList}`;
|
||||
return `${modelList}; Plan mode uses experimental collaborationMode override`;
|
||||
}
|
||||
114
src/app-server/connection-manager.ts
Normal file
114
src/app-server/connection-manager.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import type { InitializeResponse } from "../generated/app-server/InitializeResponse";
|
||||
import type { ServerNotification } from "../generated/app-server/ServerNotification";
|
||||
import type { ServerRequest } from "../generated/app-server/ServerRequest";
|
||||
import { AppServerClient, type AppServerClientHandlers } from "./client";
|
||||
|
||||
export interface ConnectionManagerHandlers {
|
||||
onNotification: (notification: ServerNotification) => void;
|
||||
onServerRequest: (request: ServerRequest) => void;
|
||||
onLog: (message: string) => void;
|
||||
onExit: () => void;
|
||||
}
|
||||
|
||||
export type AppServerClientFactory = (codexPath: string, cwd: string, handlers: AppServerClientHandlers) => AppServerClient;
|
||||
|
||||
export class StaleConnectionError extends Error {
|
||||
constructor() {
|
||||
super("Stale Codex app-server connection ignored.");
|
||||
this.name = "StaleConnectionError";
|
||||
}
|
||||
}
|
||||
|
||||
export class ConnectionManager {
|
||||
private client: AppServerClient | null = null;
|
||||
private connectPromise: Promise<InitializeResponse> | null = null;
|
||||
private generation = 0;
|
||||
private disposed = false;
|
||||
|
||||
constructor(
|
||||
private readonly codexPath: () => string,
|
||||
private readonly cwd: string,
|
||||
private readonly handlers: ConnectionManagerHandlers,
|
||||
private readonly clientFactory: AppServerClientFactory = (codexPath, cwd, handlers) => new AppServerClient(codexPath, cwd, handlers),
|
||||
) {}
|
||||
|
||||
currentClient(): AppServerClient | null {
|
||||
return this.client?.isConnected() ? this.client : null;
|
||||
}
|
||||
|
||||
isConnected(): boolean {
|
||||
return Boolean(this.currentClient());
|
||||
}
|
||||
|
||||
async connect(): Promise<InitializeResponse> {
|
||||
this.disposed = false;
|
||||
if (this.client?.isConnected()) {
|
||||
return this.client.initializeResponse;
|
||||
}
|
||||
if (this.connectPromise) return this.connectPromise;
|
||||
|
||||
const generation = ++this.generation;
|
||||
const client = this.clientFactory(this.codexPath(), this.cwd, {
|
||||
onNotification: (notification) => {
|
||||
if (this.isStale(generation)) return;
|
||||
this.handlers.onNotification(notification);
|
||||
},
|
||||
onServerRequest: (request) => {
|
||||
if (this.isStale(generation)) return;
|
||||
this.handlers.onServerRequest(request);
|
||||
},
|
||||
onLog: (message) => {
|
||||
if (this.isStale(generation)) return;
|
||||
this.handlers.onLog(message);
|
||||
},
|
||||
onExit: () => {
|
||||
if (this.isStale(generation)) return;
|
||||
this.client = null;
|
||||
this.connectPromise = null;
|
||||
this.handlers.onExit();
|
||||
},
|
||||
});
|
||||
this.client = client;
|
||||
this.connectPromise = client
|
||||
.connect()
|
||||
.then((response) => {
|
||||
if (this.isStale(generation)) {
|
||||
client.disconnect();
|
||||
throw new StaleConnectionError();
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
if (this.isStale(generation)) {
|
||||
throw error instanceof StaleConnectionError ? error : new StaleConnectionError();
|
||||
}
|
||||
if (!this.isStale(generation) && this.client === client) {
|
||||
this.client = null;
|
||||
client.disconnect();
|
||||
}
|
||||
throw error;
|
||||
})
|
||||
.finally(() => {
|
||||
if (!this.isStale(generation)) this.connectPromise = null;
|
||||
});
|
||||
|
||||
return this.connectPromise;
|
||||
}
|
||||
|
||||
reconnect(): void {
|
||||
this.disconnect();
|
||||
this.disposed = false;
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
this.disposed = true;
|
||||
this.generation += 1;
|
||||
this.connectPromise = null;
|
||||
this.client?.disconnect();
|
||||
this.client = null;
|
||||
}
|
||||
|
||||
private isStale(generation: number): boolean {
|
||||
return this.disposed || generation !== this.generation;
|
||||
}
|
||||
}
|
||||
14
src/app-server/service-tier.ts
Normal file
14
src/app-server/service-tier.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
export type ServiceTier = "fast" | "standard";
|
||||
export type ServiceTierRequest = "fast" | null | undefined;
|
||||
|
||||
export function parseServiceTier(value: unknown): ServiceTier | null {
|
||||
if (value === "fast" || value === "priority") return "fast";
|
||||
if (value === "standard" || value === "default" || value === "flex") return "standard";
|
||||
return null;
|
||||
}
|
||||
|
||||
export function serviceTierRequestValue(value: ServiceTier | null): ServiceTierRequest {
|
||||
if (value === "fast") return "fast";
|
||||
if (value === "standard") return null;
|
||||
return undefined;
|
||||
}
|
||||
22
src/app-server/session-client.ts
Normal file
22
src/app-server/session-client.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { AppServerClient } from "./client";
|
||||
|
||||
export async function withAppServerSession<T>(
|
||||
codexPath: string,
|
||||
cwd: string,
|
||||
operation: (client: AppServerClient) => Promise<T>,
|
||||
): Promise<T> {
|
||||
let client!: AppServerClient;
|
||||
client = new AppServerClient(codexPath, cwd, {
|
||||
onNotification: () => undefined,
|
||||
onServerRequest: (request) => client.rejectServerRequest(request.id, -32601, "This Codex Panel view does not handle server requests."),
|
||||
onLog: () => undefined,
|
||||
onExit: () => undefined,
|
||||
});
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
return await operation(client);
|
||||
} finally {
|
||||
client.disconnect();
|
||||
}
|
||||
}
|
||||
98
src/app-server/transport.ts
Normal file
98
src/app-server/transport.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import { spawn, type ChildProcessWithoutNullStreams } from "child_process";
|
||||
import * as readline from "readline";
|
||||
|
||||
import type { RpcOutboundMessage } from "./types";
|
||||
|
||||
export interface AppServerTransport {
|
||||
start(): void;
|
||||
send(message: RpcOutboundMessage): void;
|
||||
stop(): void;
|
||||
isRunning(): boolean;
|
||||
}
|
||||
|
||||
export interface AppServerTransportHandlers {
|
||||
onLine: (line: string) => void;
|
||||
onLog: (message: string) => void;
|
||||
onExit: (code: number | null, signal: NodeJS.Signals | null) => void;
|
||||
onError: (error: Error) => void;
|
||||
}
|
||||
|
||||
export class StdioAppServerTransport implements AppServerTransport {
|
||||
private process: ChildProcessWithoutNullStreams | null = null;
|
||||
private reader: readline.Interface | null = null;
|
||||
private stderrBuffer = "";
|
||||
|
||||
constructor(
|
||||
private readonly codexPath: string,
|
||||
private readonly cwd: string,
|
||||
private readonly handlers: AppServerTransportHandlers,
|
||||
) {}
|
||||
|
||||
start(): void {
|
||||
if (this.process) {
|
||||
throw new Error("Codex app-server is already running.");
|
||||
}
|
||||
|
||||
this.process = spawn(this.codexPath, ["app-server"], {
|
||||
cwd: this.cwd,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
this.process.once("error", (error) => {
|
||||
this.reader?.close();
|
||||
this.reader = null;
|
||||
this.process = null;
|
||||
this.handlers.onError(error instanceof Error ? error : new Error(String(error)));
|
||||
});
|
||||
|
||||
this.process.once("exit", (code, signal) => {
|
||||
this.reader?.close();
|
||||
this.reader = null;
|
||||
this.process = null;
|
||||
this.handlers.onExit(code, signal);
|
||||
});
|
||||
|
||||
this.process.stderr.on("data", (chunk: Buffer) => {
|
||||
this.handleStderr(chunk.toString("utf8"));
|
||||
});
|
||||
this.process.stdin.on("error", (error) => {
|
||||
this.handlers.onError(error instanceof Error ? error : new Error(String(error)));
|
||||
});
|
||||
|
||||
this.reader = readline.createInterface({ input: this.process.stdout });
|
||||
this.reader.on("line", (line) => this.handlers.onLine(line));
|
||||
}
|
||||
|
||||
send(message: RpcOutboundMessage): void {
|
||||
if (!this.process || this.process.killed || this.process.stdin.destroyed || !this.process.stdin.writable) {
|
||||
throw new Error("Codex app-server is not running.");
|
||||
}
|
||||
this.process.stdin.write(`${JSON.stringify(message)}\n`, (error) => {
|
||||
if (error) this.handlers.onError(error instanceof Error ? error : new Error(String(error)));
|
||||
});
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
this.reader?.close();
|
||||
this.reader = null;
|
||||
if (this.process && !this.process.killed) {
|
||||
this.process.kill();
|
||||
}
|
||||
this.process = null;
|
||||
}
|
||||
|
||||
isRunning(): boolean {
|
||||
return this.process !== null && !this.process.killed;
|
||||
}
|
||||
|
||||
private handleStderr(text: string): void {
|
||||
this.stderrBuffer += text;
|
||||
const lines = this.stderrBuffer.split(/\r?\n/);
|
||||
this.stderrBuffer = lines.pop() ?? "";
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.length > 0) this.handlers.onLog(trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/app-server/types.ts
Normal file
30
src/app-server/types.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import type { ClientNotification } from "../generated/app-server/ClientNotification";
|
||||
import type { ClientRequest } from "../generated/app-server/ClientRequest";
|
||||
import type { RequestId } from "../generated/app-server/RequestId";
|
||||
import type { ServerNotification } from "../generated/app-server/ServerNotification";
|
||||
import type { ServerRequest } from "../generated/app-server/ServerRequest";
|
||||
|
||||
export type ClientRequestMethod = ClientRequest["method"];
|
||||
export type ClientRequestParams<M extends ClientRequestMethod> = Extract<ClientRequest, { method: M }>["params"];
|
||||
export type ClientRequestMessage<M extends ClientRequestMethod = ClientRequestMethod> = Extract<ClientRequest, { method: M }>;
|
||||
|
||||
export interface RpcError {
|
||||
code?: number;
|
||||
message: string;
|
||||
data?: unknown;
|
||||
}
|
||||
|
||||
export type RpcOutboundMessage =
|
||||
| ClientRequest
|
||||
| ClientNotification
|
||||
| { id: RequestId; result: unknown }
|
||||
| { id: RequestId; error: RpcError };
|
||||
|
||||
export type RpcInboundMessage = ServerNotification | ServerRequest | { id: RequestId; result?: unknown; error?: RpcError };
|
||||
|
||||
export interface PendingRequest {
|
||||
method: ClientRequestMethod;
|
||||
reject: (reason: Error) => void;
|
||||
resolve: (value: unknown) => void;
|
||||
timeout: ReturnType<typeof setTimeout>;
|
||||
}
|
||||
152
src/approvals/model.ts
Normal file
152
src/approvals/model.ts
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
import type { RequestId } from "../generated/app-server/RequestId";
|
||||
import type { ServerRequest } from "../generated/app-server/ServerRequest";
|
||||
import type { CommandExecutionRequestApprovalResponse } from "../generated/app-server/v2/CommandExecutionRequestApprovalResponse";
|
||||
import type { FileChangeRequestApprovalResponse } from "../generated/app-server/v2/FileChangeRequestApprovalResponse";
|
||||
import type { GrantedPermissionProfile } from "../generated/app-server/v2/GrantedPermissionProfile";
|
||||
import type { PermissionsRequestApprovalResponse } from "../generated/app-server/v2/PermissionsRequestApprovalResponse";
|
||||
import { jsonPreview } from "../utils";
|
||||
|
||||
export type ApprovalAction = "accept" | "accept-session" | "decline" | "cancel";
|
||||
export type ApprovalRequest = Extract<
|
||||
ServerRequest,
|
||||
{
|
||||
method: "item/commandExecution/requestApproval" | "item/fileChange/requestApproval" | "item/permissions/requestApproval";
|
||||
}
|
||||
>;
|
||||
|
||||
export type PendingApproval = ApprovalRequest extends infer Request
|
||||
? Request extends ApprovalRequest
|
||||
? {
|
||||
requestId: RequestId;
|
||||
method: Request["method"];
|
||||
params: Request["params"];
|
||||
}
|
||||
: never
|
||||
: never;
|
||||
|
||||
export function toPendingApproval(request: ServerRequest): PendingApproval | null {
|
||||
if (!isApprovalRequest(request)) return null;
|
||||
switch (request.method) {
|
||||
case "item/commandExecution/requestApproval":
|
||||
case "item/fileChange/requestApproval":
|
||||
case "item/permissions/requestApproval":
|
||||
return {
|
||||
requestId: request.id,
|
||||
method: request.method,
|
||||
params: request.params,
|
||||
} as PendingApproval;
|
||||
}
|
||||
}
|
||||
|
||||
export function isApprovalRequest(request: ServerRequest): request is ApprovalRequest {
|
||||
return (
|
||||
request.method === "item/commandExecution/requestApproval" ||
|
||||
request.method === "item/fileChange/requestApproval" ||
|
||||
request.method === "item/permissions/requestApproval"
|
||||
);
|
||||
}
|
||||
|
||||
export function approvalResponse(approval: PendingApproval, action: ApprovalAction): unknown {
|
||||
if (approval.method === "item/commandExecution/requestApproval") {
|
||||
return {
|
||||
decision: commandDecision(action),
|
||||
} satisfies CommandExecutionRequestApprovalResponse;
|
||||
}
|
||||
|
||||
if (approval.method === "item/fileChange/requestApproval") {
|
||||
return {
|
||||
decision: fileChangeDecision(action),
|
||||
} satisfies FileChangeRequestApprovalResponse;
|
||||
}
|
||||
|
||||
if (approval.method === "item/permissions/requestApproval") {
|
||||
return {
|
||||
scope: action === "accept-session" ? "session" : "turn",
|
||||
permissions: action === "accept" || action === "accept-session" ? grantedPermissions(approval.params.permissions) : {},
|
||||
} satisfies PermissionsRequestApprovalResponse;
|
||||
}
|
||||
|
||||
throw new Error("Unsupported approval method.");
|
||||
}
|
||||
|
||||
export function approvalTitle(approval: PendingApproval): string {
|
||||
if (approval.method.includes("commandExecution")) return "Command approval";
|
||||
if (approval.method.includes("fileChange")) return "File change approval";
|
||||
if (approval.method.includes("permissions")) return "Permission approval";
|
||||
return approval.method;
|
||||
}
|
||||
|
||||
export function approvalSummary(approval: PendingApproval): string {
|
||||
const params = approval.params as Record<string, unknown>;
|
||||
if (approval.method.includes("commandExecution")) {
|
||||
return typeof params.command === "string" ? params.command : "Command execution requested.";
|
||||
}
|
||||
if (approval.method.includes("fileChange")) {
|
||||
return typeof params.grantRoot === "string"
|
||||
? `grant root: ${params.grantRoot}`
|
||||
: typeof params.reason === "string"
|
||||
? params.reason
|
||||
: "Allow file changes?";
|
||||
}
|
||||
if (approval.method.includes("permissions")) {
|
||||
return [
|
||||
`cwd: ${typeof params.cwd === "string" ? params.cwd : "(unknown)"}`,
|
||||
typeof params.reason === "string" ? params.reason : "",
|
||||
jsonPreview(params.permissions),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
}
|
||||
return jsonPreview(params);
|
||||
}
|
||||
|
||||
export function approvalDetails(approval: PendingApproval): Array<{ key: string; value: string }> {
|
||||
const params = approval.params as Record<string, unknown>;
|
||||
const rows: Array<{ key: string; value: string }> = [
|
||||
{ key: "method", value: approval.method },
|
||||
{ key: "cwd", value: stringValue(params.cwd, "(unknown)") },
|
||||
];
|
||||
addOptional(rows, "reason", params.reason);
|
||||
addOptional(rows, "grant root", params.grantRoot);
|
||||
addOptional(rows, "command", Array.isArray(params.command) ? params.command.join(" ") : params.command);
|
||||
addOptional(rows, "actions", params.commandActions);
|
||||
addOptional(rows, "permissions", params.permissions);
|
||||
addOptional(rows, "future command rule", params.proposedExecpolicyAmendment);
|
||||
addOptional(rows, "future network rules", params.proposedNetworkPolicyAmendments);
|
||||
if (Object.keys(params).length > 0) rows.push({ key: "raw", value: jsonPreview(params) });
|
||||
return rows;
|
||||
}
|
||||
|
||||
function commandDecision(action: ApprovalAction): CommandExecutionRequestApprovalResponse["decision"] {
|
||||
if (action === "accept") return "accept";
|
||||
if (action === "accept-session") return "acceptForSession";
|
||||
if (action === "cancel") return "cancel";
|
||||
return "decline";
|
||||
}
|
||||
|
||||
function fileChangeDecision(action: ApprovalAction): FileChangeRequestApprovalResponse["decision"] {
|
||||
if (action === "accept") return "accept";
|
||||
if (action === "accept-session") return "acceptForSession";
|
||||
if (action === "cancel") return "cancel";
|
||||
return "decline";
|
||||
}
|
||||
|
||||
function grantedPermissions(requested: unknown): GrantedPermissionProfile {
|
||||
const source = requested as { network?: unknown; fileSystem?: unknown } | null;
|
||||
const granted: GrantedPermissionProfile = {};
|
||||
if (source?.network) granted.network = source.network as GrantedPermissionProfile["network"];
|
||||
if (source?.fileSystem) granted.fileSystem = source.fileSystem as GrantedPermissionProfile["fileSystem"];
|
||||
return granted;
|
||||
}
|
||||
|
||||
function addOptional(rows: Array<{ key: string; value: string }>, key: string, value: unknown): void {
|
||||
if (value === null || value === undefined) return;
|
||||
rows.push({ key, value: stringValue(value) });
|
||||
}
|
||||
|
||||
function stringValue(value: unknown, fallback = ""): string {
|
||||
if (typeof value === "string") return value;
|
||||
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
|
||||
if (value === null || value === undefined) return fallback;
|
||||
return jsonPreview(value);
|
||||
}
|
||||
146
src/composer/suggestions.ts
Normal file
146
src/composer/suggestions.ts
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
import type { SkillMetadata } from "../generated/app-server/v2/SkillMetadata";
|
||||
import { SLASH_COMMANDS, type SlashCommandName } from "../panel/slash-commands";
|
||||
|
||||
export interface ComposerSuggestion {
|
||||
display: string;
|
||||
detail: string;
|
||||
replacement: string;
|
||||
start: number;
|
||||
appendSpaceOnInsert?: boolean;
|
||||
}
|
||||
|
||||
export interface NoteCandidate {
|
||||
basename: string;
|
||||
path: string;
|
||||
mtime: number;
|
||||
}
|
||||
|
||||
export function parseSlashCommand(text: string): { command: SlashCommandName; args: string } | null {
|
||||
const match = text.match(/^\/([A-Za-z-]+)(?:\s+([\s\S]*))?$/);
|
||||
if (!match) return null;
|
||||
const command = match[1] as SlashCommandName;
|
||||
if (!SLASH_COMMANDS.some((item) => item.command === `/${command}`)) return null;
|
||||
return { command, args: match[2]?.trim() ?? "" };
|
||||
}
|
||||
|
||||
export function activeComposerSuggestions(beforeCursor: string, notes: NoteCandidate[], skills: SkillMetadata[]): ComposerSuggestion[] {
|
||||
return (
|
||||
activeWikiLinkSuggestions(beforeCursor, notes) ??
|
||||
activeSlashCommandSuggestions(beforeCursor) ??
|
||||
activeSkillSuggestions(beforeCursor, skills) ??
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
export function applyComposerSuggestionInsertion(
|
||||
value: string,
|
||||
cursor: number,
|
||||
suggestion: ComposerSuggestion,
|
||||
): { value: string; cursor: number } {
|
||||
const suffix = value.slice(cursor);
|
||||
const appendSpace = suggestion.appendSpaceOnInsert === true && !suggestion.replacement.endsWith(" ") && !/^\s/.test(suffix);
|
||||
const replacement = `${suggestion.replacement}${appendSpace ? " " : ""}`;
|
||||
const nextValue = `${value.slice(0, suggestion.start)}${replacement}${suffix}`;
|
||||
return { value: nextValue, cursor: suggestion.start + replacement.length };
|
||||
}
|
||||
|
||||
export function composerSuggestionNavigationDirection(event: Pick<KeyboardEvent, "key" | "ctrlKey" | "metaKey" | "altKey">): 1 | -1 | null {
|
||||
if (event.ctrlKey && !event.metaKey && !event.altKey && event.key.toLowerCase() === "n") return 1;
|
||||
if (event.ctrlKey && !event.metaKey && !event.altKey && event.key.toLowerCase() === "p") return -1;
|
||||
if (event.metaKey || event.ctrlKey || event.altKey) return null;
|
||||
if (event.key === "ArrowDown") return 1;
|
||||
if (event.key === "ArrowUp") return -1;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function nextComposerSuggestionIndex(current: number, length: number, direction: 1 | -1): number {
|
||||
if (length <= 0) return 0;
|
||||
return (current + direction + length) % length;
|
||||
}
|
||||
|
||||
export function activeWikiLinkSuggestions(beforeCursor: string, notes: NoteCandidate[]): ComposerSuggestion[] | null {
|
||||
const start = beforeCursor.lastIndexOf("[[");
|
||||
if (start === -1) return null;
|
||||
|
||||
const queryText = beforeCursor.slice(start + 2);
|
||||
if (queryText.includes("]]") || queryText.includes("\n") || queryText.length > 120) return null;
|
||||
return findWikiLinkSuggestions(queryText, start, notes);
|
||||
}
|
||||
|
||||
export function findWikiLinkSuggestions(queryText: string, start: number, notes: NoteCandidate[]): ComposerSuggestion[] {
|
||||
const query = queryText.toLowerCase().trim();
|
||||
const basenameCounts = new Map<string, number>();
|
||||
for (const file of notes) {
|
||||
basenameCounts.set(file.basename, (basenameCounts.get(file.basename) ?? 0) + 1);
|
||||
}
|
||||
|
||||
return notes
|
||||
.map((file) => {
|
||||
const basename = file.basename.toLowerCase();
|
||||
const path = file.path.toLowerCase();
|
||||
const score = query.length === 0 ? 3 : basename.startsWith(query) ? 0 : basename.includes(query) ? 1 : path.includes(query) ? 2 : -1;
|
||||
return { file, score };
|
||||
})
|
||||
.filter((item) => item.score !== -1)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
a.score - b.score ||
|
||||
b.file.mtime - a.file.mtime ||
|
||||
a.file.basename.localeCompare(b.file.basename) ||
|
||||
a.file.path.localeCompare(b.file.path),
|
||||
)
|
||||
.slice(0, 8)
|
||||
.map(({ file }) => {
|
||||
const duplicateBasename = (basenameCounts.get(file.basename) ?? 0) > 1;
|
||||
const target = duplicateBasename ? file.path.replace(/\.md$/i, "") : file.basename;
|
||||
return {
|
||||
display: file.basename,
|
||||
detail: file.path,
|
||||
replacement: `[[${target}]]`,
|
||||
start,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function activeSlashCommandSuggestions(beforeCursor: string): ComposerSuggestion[] | null {
|
||||
const match = beforeCursor.match(/(?:^|\n)(\/[A-Za-z-]*)$/);
|
||||
if (!match || match.index === undefined) return null;
|
||||
|
||||
const query = match[1].toLowerCase();
|
||||
const start = match.index + match[0].lastIndexOf("/");
|
||||
return SLASH_COMMANDS.filter((item) => item.command.toLowerCase().startsWith(query))
|
||||
.slice(0, 8)
|
||||
.map((item) => ({
|
||||
display: item.command,
|
||||
detail: item.detail,
|
||||
replacement: item.command,
|
||||
start,
|
||||
appendSpaceOnInsert: true,
|
||||
}));
|
||||
}
|
||||
|
||||
export function activeSkillSuggestions(beforeCursor: string, skills: SkillMetadata[]): ComposerSuggestion[] | null {
|
||||
const match = beforeCursor.match(/(^|[\s([{])\$([^\s\])}]{0,120})$/);
|
||||
if (!match || match.index === undefined) return null;
|
||||
|
||||
const prefix = match[1] ?? "";
|
||||
const query = (match[2] ?? "").toLowerCase();
|
||||
const start = match.index + prefix.length;
|
||||
return skills
|
||||
.filter((skill) => skill.name.toLowerCase().includes(query))
|
||||
.sort((a, b) => {
|
||||
const aName = a.name.toLowerCase();
|
||||
const bName = b.name.toLowerCase();
|
||||
const aScore = aName.startsWith(query) ? 0 : 1;
|
||||
const bScore = bName.startsWith(query) ? 0 : 1;
|
||||
return aScore - bScore || a.name.localeCompare(b.name);
|
||||
})
|
||||
.slice(0, 8)
|
||||
.map((skill) => ({
|
||||
display: `$${skill.name}`,
|
||||
detail: skill.shortDescription ?? skill.interface?.shortDescription ?? skill.description,
|
||||
replacement: `$${skill.name}`,
|
||||
start,
|
||||
appendSpaceOnInsert: true,
|
||||
}));
|
||||
}
|
||||
62
src/composer/wikilink-context.ts
Normal file
62
src/composer/wikilink-context.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import type { UserInput } from "../generated/app-server/v2/UserInput";
|
||||
|
||||
export interface ParsedWikiLink {
|
||||
raw: string;
|
||||
target: string;
|
||||
subpath: string;
|
||||
display: string;
|
||||
}
|
||||
|
||||
export type WikiLinkMentionResolver = (target: string) => { name: string; path: string } | null;
|
||||
|
||||
const WIKILINK_PATTERN = /\[\[([^\]\n]+?)\]\]/g;
|
||||
|
||||
export function parsedWikiLinks(text: string): ParsedWikiLink[] {
|
||||
const links: ParsedWikiLink[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const match of text.matchAll(WIKILINK_PATTERN)) {
|
||||
const raw = match[1]?.trim() ?? "";
|
||||
const link = parseWikiLink(raw);
|
||||
if (!link) continue;
|
||||
const key = `${link.target}${link.subpath}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
links.push(link);
|
||||
}
|
||||
return links;
|
||||
}
|
||||
|
||||
export function userInputWithWikiLinkMentions(text: string, resolveMention: WikiLinkMentionResolver): UserInput[] {
|
||||
const input: UserInput[] = [{ type: "text", text, text_elements: [] }];
|
||||
const seenPaths = new Set<string>();
|
||||
|
||||
for (const link of parsedWikiLinks(text)) {
|
||||
const mention = resolveMention(link.target);
|
||||
if (!mention || seenPaths.has(mention.path)) continue;
|
||||
seenPaths.add(mention.path);
|
||||
input.push({ type: "mention", name: mention.name, path: mention.path });
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
function parseWikiLink(raw: string): ParsedWikiLink | null {
|
||||
const separator = raw.indexOf("|");
|
||||
const linkPart = (separator === -1 ? raw : raw.slice(0, separator)).trim();
|
||||
const display = separator === -1 ? "" : raw.slice(separator + 1).trim();
|
||||
if (!linkPart) return null;
|
||||
|
||||
const subpathStart = firstSubpathIndex(linkPart);
|
||||
const target = subpathStart === -1 ? linkPart : linkPart.slice(0, subpathStart).trim();
|
||||
const subpath = subpathStart === -1 ? "" : linkPart.slice(subpathStart).trim();
|
||||
if (!target) return null;
|
||||
return { raw, target, subpath, display };
|
||||
}
|
||||
|
||||
function firstSubpathIndex(linkPart: string): number {
|
||||
const headingIndex = linkPart.indexOf("#");
|
||||
const blockIndex = linkPart.indexOf("^");
|
||||
if (headingIndex === -1) return blockIndex;
|
||||
if (blockIndex === -1) return headingIndex;
|
||||
return Math.min(headingIndex, blockIndex);
|
||||
}
|
||||
3
src/constants.ts
Normal file
3
src/constants.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export const VIEW_TYPE_CODEX_PANEL = "codex-panel-view";
|
||||
export const DEFAULT_CODEX_PATH = "codex";
|
||||
export const CLIENT_VERSION = "0.1.0";
|
||||
109
src/display/agent.ts
Normal file
109
src/display/agent.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import type { ThreadItem } from "../generated/app-server/v2/ThreadItem";
|
||||
import type { AgentRunSummary, AgentRunSummaryAgent, AgentStateDisplay, DisplayItem, ExecutionState } from "./types";
|
||||
import { agentActivitySummaryLabel, agentMessagePreview } from "./labels";
|
||||
import { classifyExecutionState } from "./state";
|
||||
|
||||
const ACTIVE_AGENT_PREVIEW_LIMIT = 96;
|
||||
const ACTIVE_AGENT_PREVIEW_COUNT = 3;
|
||||
type AgentRunState = "running" | "completed" | "failed";
|
||||
type CollabAgentToolCallItem = Extract<ThreadItem, { type: "collabAgentToolCall" }>;
|
||||
|
||||
export function agentDisplayItem(item: CollabAgentToolCallItem, turnId?: string): DisplayItem {
|
||||
const agents = agentStatesDisplay(item.agentsStates);
|
||||
const receiverText = item.receiverThreadIds.length > 0 ? `\ntargets: ${item.receiverThreadIds.join(", ")}` : "";
|
||||
const promptText = item.prompt ? `\n${item.prompt}` : "";
|
||||
return {
|
||||
id: item.id,
|
||||
kind: "agent",
|
||||
role: "tool",
|
||||
text: `${agentActivitySummaryLabel(item.tool)}\nstatus: ${item.status}${receiverText}${promptText}`,
|
||||
turnId,
|
||||
itemId: item.id,
|
||||
tool: item.tool,
|
||||
status: item.status,
|
||||
senderThreadId: item.senderThreadId,
|
||||
receiverThreadIds: item.receiverThreadIds,
|
||||
prompt: item.prompt,
|
||||
model: item.model,
|
||||
reasoningEffort: item.reasoningEffort,
|
||||
agents,
|
||||
state: collabAgentExecutionState(item.status, item.receiverThreadIds, agents),
|
||||
};
|
||||
}
|
||||
|
||||
export function activeAgentRunSummary(items: DisplayItem[], activeTurnId: string | null, busy: boolean): AgentRunSummary | null {
|
||||
if (!busy || !activeTurnId) return null;
|
||||
|
||||
const agentStatuses = new Map<string, AgentStateDisplay>();
|
||||
for (const item of items) {
|
||||
if (item.kind !== "agent" || item.turnId !== activeTurnId) continue;
|
||||
if (item.agents.length > 0) {
|
||||
for (const agent of item.agents) {
|
||||
agentStatuses.set(agent.threadId, agent);
|
||||
}
|
||||
} else {
|
||||
for (const threadId of item.receiverThreadIds) {
|
||||
agentStatuses.set(threadId, { threadId, status: item.status, message: null });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (agentStatuses.size === 0) return null;
|
||||
|
||||
const summary = { running: 0, completed: 0, failed: 0, agents: [] as AgentRunSummaryAgent[], additionalAgents: 0 };
|
||||
const agents = [...agentStatuses.values()];
|
||||
for (const agent of agents) {
|
||||
const state = agentRunState(agent.status);
|
||||
summary[state] += 1;
|
||||
}
|
||||
|
||||
if (summary.running === 0 && summary.failed === 0) return null;
|
||||
|
||||
const visibleAgents = agents.sort(compareActiveAgentStates).slice(0, ACTIVE_AGENT_PREVIEW_COUNT);
|
||||
summary.agents = visibleAgents.map((agent) => ({
|
||||
threadId: agent.threadId,
|
||||
status: agent.status,
|
||||
messagePreview: agentMessagePreview(agent.message, ACTIVE_AGENT_PREVIEW_LIMIT),
|
||||
}));
|
||||
summary.additionalAgents = Math.max(0, agents.length - visibleAgents.length);
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
function agentStatesDisplay(states: CollabAgentToolCallItem["agentsStates"]): AgentStateDisplay[] {
|
||||
return Object.entries(states ?? {})
|
||||
.map(([threadId, state]) => ({
|
||||
threadId,
|
||||
status: state?.status ?? "unknown",
|
||||
message: state?.message ?? null,
|
||||
}))
|
||||
.sort((a, b) => a.threadId.localeCompare(b.threadId));
|
||||
}
|
||||
|
||||
function collabAgentExecutionState(status: string, receiverThreadIds: string[], agents: AgentStateDisplay[]): ExecutionState {
|
||||
if (agents.some((agent) => classifyExecutionState({ status: agent.status }) === "failed")) return "failed";
|
||||
if (agents.some((agent) => classifyExecutionState({ status: agent.status }) === "running")) return "running";
|
||||
if (agents.length > 0 && agents.every((agent) => classifyExecutionState({ status: agent.status }) === "completed")) {
|
||||
return "completed";
|
||||
}
|
||||
if (receiverThreadIds.length > 0 && classifyExecutionState({ status }) === "completed") return "running";
|
||||
const state = classifyExecutionState({ status });
|
||||
if (state) return state;
|
||||
return null;
|
||||
}
|
||||
|
||||
function agentRunState(status: string): AgentRunState {
|
||||
return classifyExecutionState({ status }) ?? "running";
|
||||
}
|
||||
|
||||
function compareActiveAgentStates(a: AgentStateDisplay, b: AgentStateDisplay): number {
|
||||
const stateDiff = agentRunStatePriority(agentRunState(a.status)) - agentRunStatePriority(agentRunState(b.status));
|
||||
if (stateDiff !== 0) return stateDiff;
|
||||
return a.threadId.localeCompare(b.threadId);
|
||||
}
|
||||
|
||||
function agentRunStatePriority(state: AgentRunState): number {
|
||||
if (state === "failed") return 0;
|
||||
if (state === "running") return 1;
|
||||
return 2;
|
||||
}
|
||||
45
src/display/labels.ts
Normal file
45
src/display/labels.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import type { TurnPlanStep } from "../generated/app-server/v2/TurnPlanStep";
|
||||
import { truncate } from "../utils";
|
||||
import type { AgentRunSummary } from "./types";
|
||||
|
||||
export function taskStatusMarker(status: TurnPlanStep["status"]): string {
|
||||
if (status === "completed") return "[x]";
|
||||
if (status === "inProgress") return "[>]";
|
||||
return "[ ]";
|
||||
}
|
||||
|
||||
export function agentActivitySummaryLabel(tool: string): string {
|
||||
if (tool === "spawnAgent") return "Spawn agent";
|
||||
if (tool === "sendInput") return "Send input to agent";
|
||||
if (tool === "resumeAgent") return "Resume agent";
|
||||
if (tool === "wait") return "Wait for agent";
|
||||
if (tool === "closeAgent") return "Close agent";
|
||||
return `Agent ${tool}`;
|
||||
}
|
||||
|
||||
export function agentActivityMetaLabel(tool: string): string {
|
||||
if (tool === "spawnAgent") return "spawn";
|
||||
if (tool === "sendInput") return "send input";
|
||||
if (tool === "resumeAgent") return "resume";
|
||||
if (tool === "wait") return "wait";
|
||||
if (tool === "closeAgent") return "close";
|
||||
return tool;
|
||||
}
|
||||
|
||||
export function agentRunSummaryLabel(summary: AgentRunSummary): string {
|
||||
const parts: string[] = [];
|
||||
if (summary.failed > 0) parts.push(`${summary.failed} failed`);
|
||||
if (summary.running > 0) parts.push(`${summary.running} running`);
|
||||
if (summary.completed > 0) parts.push(`${summary.completed} done`);
|
||||
return `Agents ${parts.join(", ")}`;
|
||||
}
|
||||
|
||||
export function agentMessagePreview(message: string | null, maxLength: number): string | null {
|
||||
if (!message) return null;
|
||||
const firstLine = message
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.find((line) => line.length > 0);
|
||||
if (!firstLine) return null;
|
||||
return truncate(firstLine.replace(/\s+/g, " "), maxLength);
|
||||
}
|
||||
851
src/display/model.ts
Normal file
851
src/display/model.ts
Normal file
|
|
@ -0,0 +1,851 @@
|
|||
import type { DisplayBlock, DisplayDetailSection, DisplayFileChange, DisplayItem, DisplayKind } from "./types";
|
||||
import type { ThreadItem } from "../generated/app-server/v2/ThreadItem";
|
||||
import type { Turn } from "../generated/app-server/v2/Turn";
|
||||
import type { TurnPlanStep } from "../generated/app-server/v2/TurnPlanStep";
|
||||
import { inputToText, jsonPreview, truncate } from "../utils";
|
||||
import { taskStatusMarker } from "./labels";
|
||||
import { agentDisplayItem } from "./agent";
|
||||
import { classifyExecutionState, executionState } from "./state";
|
||||
export { activeAgentRunSummary, agentDisplayItem } from "./agent";
|
||||
export { classifyExecutionState, executionState, executionStateLabel } from "./state";
|
||||
export { createAutoReviewResultItem, createReviewResultItem } from "./review";
|
||||
|
||||
export function displayItemsFromTurns(turns: Turn[]): DisplayItem[] {
|
||||
const sortedTurns = [...turns].sort((a, b) => (a.startedAt ?? 0) - (b.startedAt ?? 0));
|
||||
const items: DisplayItem[] = [];
|
||||
for (const turn of sortedTurns) {
|
||||
for (const item of turn.items ?? []) {
|
||||
const displayItem = displayItemFromThreadItem(item, turn.id);
|
||||
if (displayItem) items.push(displayItem);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
export function displayItemFromThreadItem(item: ThreadItem, turnId?: string): DisplayItem | null {
|
||||
if (shouldSuppressThreadItem(item)) return null;
|
||||
|
||||
if (item.type === "userMessage") {
|
||||
return {
|
||||
id: item.id,
|
||||
kind: "message",
|
||||
role: "user",
|
||||
text: inputToText(item.content),
|
||||
turnId,
|
||||
itemId: item.id,
|
||||
markdown: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (item.type === "agentMessage") {
|
||||
return {
|
||||
id: item.id,
|
||||
kind: "message",
|
||||
role: "assistant",
|
||||
text: item.text,
|
||||
turnId,
|
||||
itemId: item.id,
|
||||
markdown: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (item.type === "commandExecution") {
|
||||
return commandDisplayItem(item, turnId);
|
||||
}
|
||||
|
||||
if (item.type === "fileChange") {
|
||||
return fileChangeDisplayItem(item, turnId);
|
||||
}
|
||||
|
||||
if (item.type === "plan") {
|
||||
return {
|
||||
id: item.id,
|
||||
kind: "message",
|
||||
role: "assistant",
|
||||
text: normalizeProposedPlanMarkdown(item.text),
|
||||
turnId,
|
||||
itemId: item.id,
|
||||
markdown: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (item.type === "hookPrompt") {
|
||||
return {
|
||||
id: item.id,
|
||||
kind: "hook",
|
||||
role: "tool",
|
||||
text: item.fragments.map((fragment) => fragment.text).join("\n\n") || "Hook prompt",
|
||||
turnId,
|
||||
itemId: item.id,
|
||||
};
|
||||
}
|
||||
|
||||
if (item.type === "reasoning") {
|
||||
return {
|
||||
id: item.id,
|
||||
kind: "reasoning",
|
||||
role: "tool",
|
||||
text: reasoningText(item),
|
||||
turnId,
|
||||
itemId: item.id,
|
||||
};
|
||||
}
|
||||
|
||||
if (item.type === "mcpToolCall") {
|
||||
const name = `${item.server}.${item.tool}`;
|
||||
const target = jsonTargetLabel(item.arguments);
|
||||
const failure = item.error?.message ? truncate(item.error.message, 96) : failedStatusLabel(item.status);
|
||||
return {
|
||||
id: item.id,
|
||||
kind: "tool",
|
||||
role: "tool",
|
||||
text: compactToolSummary(null, target, statusQualifier(item.status, failure)),
|
||||
toolLabel: name,
|
||||
turnId,
|
||||
itemId: item.id,
|
||||
status: item.status,
|
||||
details: [
|
||||
{ title: "Arguments JSON", body: jsonPreview(item.arguments) },
|
||||
...(item.result ? [{ title: "Result JSON", body: jsonPreview(item.result) }] : []),
|
||||
...(item.error ? [{ title: "Error JSON", body: jsonPreview(item.error) }] : []),
|
||||
],
|
||||
output: "",
|
||||
state: classifyExecutionState({ status: item.status }),
|
||||
};
|
||||
}
|
||||
|
||||
if (item.type === "dynamicToolCall") {
|
||||
const name = `${item.namespace ? `${item.namespace}.` : ""}${item.tool}`;
|
||||
const target = jsonTargetLabel(item.arguments);
|
||||
const failure = item.success === false ? "failed" : failedStatusLabel(item.status);
|
||||
return {
|
||||
id: item.id,
|
||||
kind: "tool",
|
||||
role: "tool",
|
||||
text: compactToolSummary(null, target, statusQualifier(item.status, failure)),
|
||||
toolLabel: name,
|
||||
turnId,
|
||||
itemId: item.id,
|
||||
status: item.status,
|
||||
details: [
|
||||
{ title: "Arguments JSON", body: jsonPreview(item.arguments) },
|
||||
...(item.contentItems ? [{ title: "Result JSON", body: jsonPreview(item.contentItems) }] : []),
|
||||
],
|
||||
output: "",
|
||||
state: item.success === false ? "failed" : classifyExecutionState({ status: item.status }),
|
||||
};
|
||||
}
|
||||
|
||||
if (item.type === "collabAgentToolCall") {
|
||||
return agentDisplayItem(item, turnId);
|
||||
}
|
||||
|
||||
if (item.type === "webSearch") {
|
||||
return {
|
||||
id: item.id,
|
||||
kind: "tool",
|
||||
role: "tool",
|
||||
text: webSearchSummary(item),
|
||||
toolLabel: "web search",
|
||||
turnId,
|
||||
itemId: item.id,
|
||||
details: webSearchDetails(item),
|
||||
output: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (item.type === "imageView") {
|
||||
return {
|
||||
id: item.id,
|
||||
kind: "tool",
|
||||
role: "tool",
|
||||
text: compactToolSummary(null, item.path),
|
||||
toolLabel: "imageView",
|
||||
turnId,
|
||||
itemId: item.id,
|
||||
};
|
||||
}
|
||||
|
||||
if (item.type === "imageGeneration") {
|
||||
const target = item.savedPath ?? item.result ?? item.revisedPrompt ?? null;
|
||||
return {
|
||||
id: item.id,
|
||||
kind: "tool",
|
||||
role: "tool",
|
||||
text: compactToolSummary(null, target, statusQualifier(item.status, failedStatusLabel(item.status))),
|
||||
toolLabel: "imageGeneration",
|
||||
turnId,
|
||||
itemId: item.id,
|
||||
status: item.status,
|
||||
details: [
|
||||
...(item.savedPath ? [{ title: "Saved path", body: item.savedPath }] : []),
|
||||
...(item.revisedPrompt ? [{ title: "Revised prompt", body: item.revisedPrompt }] : []),
|
||||
...(item.result ? [{ title: "Result", body: item.result }] : []),
|
||||
],
|
||||
output: "",
|
||||
state: classifyExecutionState({ status: item.status }),
|
||||
};
|
||||
}
|
||||
|
||||
if (item.type === "enteredReviewMode" || item.type === "exitedReviewMode") {
|
||||
return {
|
||||
id: item.id,
|
||||
kind: "tool",
|
||||
role: "tool",
|
||||
text: item.type === "enteredReviewMode" ? "Entered review mode" : "Exited review mode",
|
||||
toolLabel: item.type,
|
||||
turnId,
|
||||
itemId: item.id,
|
||||
output: item.review,
|
||||
};
|
||||
}
|
||||
|
||||
if (item.type === "contextCompaction") {
|
||||
return {
|
||||
id: item.id,
|
||||
kind: "tool",
|
||||
role: "tool",
|
||||
text: "Context compaction",
|
||||
toolLabel: "contextCompaction",
|
||||
turnId,
|
||||
itemId: item.id,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
type CommandExecutionItem = Extract<ThreadItem, { type: "commandExecution" }>;
|
||||
type CommandAction = CommandExecutionItem["commandActions"][number];
|
||||
type FileChangeItem = Extract<ThreadItem, { type: "fileChange" }>;
|
||||
type ReasoningItem = Extract<ThreadItem, { type: "reasoning" }>;
|
||||
type WebSearchItem = Extract<ThreadItem, { type: "webSearch" }>;
|
||||
|
||||
const TOOL_SUMMARY_LIMIT = 140;
|
||||
|
||||
function reasoningText(item: ReasoningItem): string {
|
||||
return [...item.summary, ...item.content]
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
}
|
||||
|
||||
function compactToolSummary(label: string | null, target?: string | null, qualifier?: string | null): string {
|
||||
const targetText = target?.trim();
|
||||
const base = label ? (targetText ? `${label}: ${targetText}` : label) : (targetText ?? "details");
|
||||
return truncate(qualifier ? `${base} (${qualifier})` : base, TOOL_SUMMARY_LIMIT);
|
||||
}
|
||||
|
||||
function statusQualifier(status: unknown, failure?: string | null): string | null {
|
||||
if (status === "declined") return "declined";
|
||||
if (status === "failed") return failure || "failed";
|
||||
return null;
|
||||
}
|
||||
|
||||
function failedStatusLabel(status: unknown): string | null {
|
||||
if (status === "failed") return "failed";
|
||||
if (status === "declined") return "declined";
|
||||
return null;
|
||||
}
|
||||
|
||||
function commandTargetLabel(item: CommandExecutionItem): string {
|
||||
const action = representativeCommandAction(item.commandActions);
|
||||
if (action?.type === "search") {
|
||||
const query = commandActionValue(action.query);
|
||||
const path = commandActionValue(action.path);
|
||||
if (query && path) return `${quoteInline(query)} in ${path}`;
|
||||
if (query) return quoteInline(query);
|
||||
if (path) return path;
|
||||
}
|
||||
if (action?.type === "read") return commandReadTargetLabel(action, item.cwd);
|
||||
if (action?.type === "listFiles") return commandActionValue(action.path) ?? "workspace";
|
||||
return unwrapShellLoginCommand(firstCommandLine(item.command));
|
||||
}
|
||||
|
||||
function commandActionLabel(item: CommandExecutionItem): string {
|
||||
const action = representativeCommandAction(item.commandActions);
|
||||
if (action?.type === "read") return "read";
|
||||
if (action?.type === "search") return "search";
|
||||
if (action?.type === "listFiles") return "list files";
|
||||
return "command";
|
||||
}
|
||||
|
||||
function representativeCommandAction(actions: CommandAction[]): CommandAction | null {
|
||||
return (
|
||||
actions.find((action) => action.type === "read") ??
|
||||
actions.find((action) => action.type === "search") ??
|
||||
actions.find((action) => action.type === "listFiles") ??
|
||||
actions[0] ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function commandActionValue(value: string | null): string | null {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed ? trimmed : null;
|
||||
}
|
||||
|
||||
function commandReadTargetLabel(action: Extract<CommandAction, { type: "read" }>, cwd: string): string {
|
||||
const path = commandActionValue(action.path);
|
||||
if (path) return pathRelativeToWorkspace(path, cwd);
|
||||
return action.name;
|
||||
}
|
||||
|
||||
function firstCommandLine(command: string): string {
|
||||
return (
|
||||
command
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.find(Boolean) ?? command.trim()
|
||||
);
|
||||
}
|
||||
|
||||
function unwrapShellLoginCommand(command: string): string {
|
||||
const match = command.match(/^(?:\/bin\/)?zsh\s+-lc\s+(.+)$/);
|
||||
if (!match) return command;
|
||||
return unquoteShellCommand(match[1].trim());
|
||||
}
|
||||
|
||||
function unquoteShellCommand(value: string): string {
|
||||
if (value.length < 2) return value;
|
||||
const quote = value[0];
|
||||
if ((quote !== "'" && quote !== '"') || value[value.length - 1] !== quote) return value;
|
||||
const inner = value.slice(1, -1);
|
||||
return quote === "'" ? inner.replace(/'\\''/g, "'") : inner.replace(/\\(["\\$`])/g, "$1");
|
||||
}
|
||||
|
||||
function quoteInline(value: string): string {
|
||||
return value.includes(" ") ? JSON.stringify(value) : value;
|
||||
}
|
||||
|
||||
function fileChangeTargetLabel(changes: DisplayFileChange[]): string {
|
||||
if (changes.length === 0) return "no files";
|
||||
if (changes.length === 1) return changes[0]?.path ?? "1 file";
|
||||
return `${changes.length} files`;
|
||||
}
|
||||
|
||||
function webSearchTarget(item: WebSearchItem): string | null {
|
||||
if (item.action?.type === "openPage") return item.action.url;
|
||||
if (item.action?.type === "findInPage") return item.action.pattern ?? item.action.url;
|
||||
if (item.action?.type === "search") return webSearchQueryList(item.action.query, item.action.queries, item.query);
|
||||
return item.query;
|
||||
}
|
||||
|
||||
function webSearchSummary(item: WebSearchItem): string {
|
||||
const actionType = item.action?.type ?? (item.query ? "search" : "web search");
|
||||
const label = webSearchActionLabel(actionType);
|
||||
return compactToolSummary(label, webSearchTarget(item));
|
||||
}
|
||||
|
||||
function webSearchActionLabel(actionType: string): string {
|
||||
if (actionType === "openPage") return "open page";
|
||||
if (actionType === "findInPage") return "find in page";
|
||||
if (actionType === "search") return "search";
|
||||
if (actionType === "other") return "other";
|
||||
return actionType;
|
||||
}
|
||||
|
||||
function webSearchQueryList(
|
||||
query: string | null | undefined,
|
||||
queries: string[] | null | undefined,
|
||||
fallback?: string | null,
|
||||
): string | null {
|
||||
const values = [query, ...(queries ?? []), fallback].map((value) => value?.trim()).filter((value): value is string => Boolean(value));
|
||||
const unique = [...new Set(values)];
|
||||
if (unique.length === 0) return null;
|
||||
if (unique.length === 1) return unique[0] ?? null;
|
||||
return unique.join("; ");
|
||||
}
|
||||
|
||||
function webSearchDetails(item: WebSearchItem): DisplayDetailSection[] {
|
||||
const rows: Array<{ key: string; value: string }> = [];
|
||||
if (item.action) rows.push({ key: "action", value: webSearchActionLabel(item.action.type) });
|
||||
if (item.action?.type === "search") {
|
||||
const queries = webSearchQueryList(item.action.query, item.action.queries, item.query);
|
||||
if (queries) rows.push({ key: "query", value: queries });
|
||||
} else if (item.action?.type === "openPage") {
|
||||
if (item.action.url) rows.push({ key: "url", value: item.action.url });
|
||||
} else if (item.action?.type === "findInPage") {
|
||||
if (item.action.pattern) rows.push({ key: "pattern", value: item.action.pattern });
|
||||
if (item.action.url) rows.push({ key: "url", value: item.action.url });
|
||||
} else if (item.query) {
|
||||
rows.push({ key: "query", value: item.query });
|
||||
}
|
||||
|
||||
return rows.length > 0 ? [{ title: "web search", rows }] : [];
|
||||
}
|
||||
|
||||
function jsonTargetLabel(value: unknown): string | null {
|
||||
const direct = jsonTargetPrimitive(value);
|
||||
if (direct) return direct;
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
||||
|
||||
const record = value as Record<string, unknown>;
|
||||
const priorityKeys = [
|
||||
"q",
|
||||
"query",
|
||||
"search_query",
|
||||
"url",
|
||||
"ref_id",
|
||||
"path",
|
||||
"file",
|
||||
"filename",
|
||||
"ticker",
|
||||
"location",
|
||||
"team",
|
||||
"league",
|
||||
"id",
|
||||
"target",
|
||||
"command",
|
||||
];
|
||||
|
||||
for (const key of priorityKeys) {
|
||||
const target = jsonTargetPrimitive(record[key]);
|
||||
if (target) return target;
|
||||
}
|
||||
|
||||
const firstEntry = Object.entries(record).find(([, entryValue]) => jsonTargetPrimitive(entryValue));
|
||||
return firstEntry ? jsonTargetPrimitive(firstEntry[1]) : null;
|
||||
}
|
||||
|
||||
function jsonTargetPrimitive(value: unknown): string | null {
|
||||
if (typeof value === "string" && value.trim().length > 0) return value.trim();
|
||||
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
||||
if (!Array.isArray(value)) return null;
|
||||
for (const item of value) {
|
||||
const target = jsonTargetLabel(item);
|
||||
if (target) return target;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function commandDisplayItem(item: CommandExecutionItem, turnId?: string): DisplayItem {
|
||||
const exitCode = typeof item.exitCode === "number" ? item.exitCode : undefined;
|
||||
const durationMs = typeof item.durationMs === "number" ? item.durationMs : undefined;
|
||||
const target = commandTargetLabel(item);
|
||||
const qualifier =
|
||||
typeof exitCode === "number" && exitCode !== 0 ? `exit ${exitCode}` : statusQualifier(item.status, failedStatusLabel(item.status));
|
||||
return {
|
||||
id: item.id,
|
||||
kind: "command",
|
||||
role: "tool",
|
||||
actionLabel: commandActionLabel(item),
|
||||
text: compactToolSummary(null, target, qualifier),
|
||||
turnId,
|
||||
itemId: item.id,
|
||||
command: item.command,
|
||||
cwd: item.cwd ?? "(unknown)",
|
||||
status: item.status ?? "(unknown)",
|
||||
exitCode,
|
||||
durationMs,
|
||||
output: item.aggregatedOutput ?? "",
|
||||
state: classifyExecutionState({ exitCode, status: item.status }),
|
||||
};
|
||||
}
|
||||
|
||||
export function fileChangeDisplayItem(item: FileChangeItem, turnId?: string): DisplayItem {
|
||||
const changes = normalizeFileChanges(item.changes ?? []);
|
||||
const qualifier = statusQualifier(item.status, failedStatusLabel(item.status));
|
||||
return {
|
||||
id: item.id,
|
||||
kind: "fileChange",
|
||||
role: "tool",
|
||||
text: compactToolSummary(null, fileChangeTargetLabel(changes), qualifier),
|
||||
turnId,
|
||||
itemId: item.id,
|
||||
status: item.status,
|
||||
changes,
|
||||
state: classifyExecutionState({ status: item.status }),
|
||||
};
|
||||
}
|
||||
|
||||
export function planProgressDisplayItem(turnId: string, explanation: string | null, plan: TurnPlanStep[]): DisplayItem {
|
||||
const lines = plan.map((step) => `${taskStatusMarker(step.status)} ${step.step}`);
|
||||
const body = [explanation?.trim(), ...lines].filter((line): line is string => Boolean(line && line.length > 0)).join("\n");
|
||||
const status = plan.some((step) => step.status === "inProgress" || step.status === "pending") ? "inProgress" : "completed";
|
||||
return {
|
||||
id: `plan-progress-${turnId}`,
|
||||
kind: "taskProgress",
|
||||
role: "tool",
|
||||
text: body || "Plan updated",
|
||||
turnId,
|
||||
itemId: `plan-progress-${turnId}`,
|
||||
explanation: explanation?.trim() || null,
|
||||
steps: plan.map((step) => ({ step: step.step, status: step.status })),
|
||||
status,
|
||||
state: classifyExecutionState({ status }),
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeFileChanges(changes: unknown[]): DisplayFileChange[] {
|
||||
return changes.flatMap((change) => {
|
||||
if (!change || typeof change !== "object") return [];
|
||||
const record = change as { kind?: unknown; path?: unknown; diff?: unknown; unified_diff?: unknown };
|
||||
return [
|
||||
{
|
||||
kind: typeof record.kind === "string" ? record.kind : "changed",
|
||||
path: typeof record.path === "string" && record.path.length > 0 ? record.path : "(unknown)",
|
||||
diff: typeof record.diff === "string" ? record.diff : typeof record.unified_diff === "string" ? record.unified_diff : "",
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
export function shouldSuppressThreadItem(item: { type: string }): boolean {
|
||||
return item.type === "outputMessage" || item.type === "toolOutputMessage";
|
||||
}
|
||||
|
||||
export function shouldSuppressLifecycleItem(item: ThreadItem): boolean {
|
||||
return item.type === "agentMessage" || item.type === "userMessage";
|
||||
}
|
||||
|
||||
export function upsertDisplayItem(items: DisplayItem[], next: DisplayItem): DisplayItem[] {
|
||||
const index = items.findIndex((item) => item.id === next.id);
|
||||
if (index === -1) return [...items, next];
|
||||
const copy = [...items];
|
||||
const previous = copy[index];
|
||||
copy[index] = {
|
||||
...previous,
|
||||
...next,
|
||||
output: mergeOutput(previous, next),
|
||||
changes: mergeChanges(previous, next),
|
||||
} as DisplayItem;
|
||||
return copy;
|
||||
}
|
||||
|
||||
function mergeOutput(previous: DisplayItem, next: DisplayItem): string | undefined {
|
||||
const previousOutput = "output" in previous ? previous.output : undefined;
|
||||
const nextOutput = "output" in next ? next.output : undefined;
|
||||
return nextOutput && nextOutput.length > 0 ? nextOutput : previousOutput;
|
||||
}
|
||||
|
||||
function mergeChanges(previous: DisplayItem, next: DisplayItem): DisplayFileChange[] | undefined {
|
||||
const previousChanges = previous.kind === "fileChange" ? previous.changes : undefined;
|
||||
const nextChanges = next.kind === "fileChange" ? next.changes : undefined;
|
||||
return nextChanges && nextChanges.length > 0 ? nextChanges : previousChanges;
|
||||
}
|
||||
|
||||
export function appendAssistantDelta(items: DisplayItem[], itemId: string, turnId: string, delta: string): DisplayItem[] {
|
||||
const index = items.findIndex((item) => item.itemId === itemId && item.kind === "message" && item.role === "assistant");
|
||||
if (index !== -1) {
|
||||
return items.map((item, itemIndex) =>
|
||||
itemIndex === index && item.kind === "message"
|
||||
? {
|
||||
...item,
|
||||
text: `${item.text}${delta}`,
|
||||
turnId: item.turnId ?? turnId,
|
||||
markdown: false,
|
||||
}
|
||||
: item,
|
||||
);
|
||||
}
|
||||
return [
|
||||
...items,
|
||||
{
|
||||
id: itemId,
|
||||
kind: "message",
|
||||
role: "assistant",
|
||||
text: delta,
|
||||
turnId,
|
||||
itemId,
|
||||
markdown: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function completeReasoningItems(items: DisplayItem[], turnId: string): DisplayItem[] {
|
||||
return items.map((item) =>
|
||||
item.kind === "reasoning" && item.turnId === turnId
|
||||
? {
|
||||
...item,
|
||||
status: "completed",
|
||||
state: "completed",
|
||||
}
|
||||
: item,
|
||||
);
|
||||
}
|
||||
|
||||
export function appendPlanDelta(items: DisplayItem[], itemId: string, turnId: string, delta: string): DisplayItem[] {
|
||||
const index = items.findIndex((item) => item.itemId === itemId && item.kind === "message" && item.role === "assistant");
|
||||
if (index !== -1) {
|
||||
return items.map((item, itemIndex) =>
|
||||
itemIndex === index && item.kind === "message"
|
||||
? {
|
||||
...item,
|
||||
text: normalizeProposedPlanMarkdown(`${item.text}${delta}`),
|
||||
turnId: item.turnId ?? turnId,
|
||||
markdown: false,
|
||||
}
|
||||
: item,
|
||||
);
|
||||
}
|
||||
return [
|
||||
...items,
|
||||
{
|
||||
id: itemId,
|
||||
kind: "message",
|
||||
role: "assistant",
|
||||
text: normalizeProposedPlanMarkdown(delta),
|
||||
turnId,
|
||||
itemId,
|
||||
markdown: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function normalizeProposedPlanMarkdown(text: string): string {
|
||||
return text
|
||||
.replace(/^\s*<proposed_plan>\s*\n?/i, "")
|
||||
.replace(/\n?\s*<\/proposed_plan>\s*$/i, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function appendItemText(
|
||||
items: DisplayItem[],
|
||||
itemId: string,
|
||||
turnId: string,
|
||||
label: string,
|
||||
delta: string,
|
||||
kind: Extract<DisplayKind, "tool" | "hook" | "reasoning"> = "tool",
|
||||
): DisplayItem[] {
|
||||
const index = items.findIndex((item) => item.itemId === itemId);
|
||||
if (index !== -1) {
|
||||
return items.map((item, itemIndex) => (itemIndex === index ? { ...item, text: `${item.text}${delta}` } : item));
|
||||
}
|
||||
return [
|
||||
...items,
|
||||
{
|
||||
id: itemId,
|
||||
kind,
|
||||
role: "tool",
|
||||
text: `${label}: ${delta}`,
|
||||
turnId,
|
||||
itemId,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function appendToolOutput(
|
||||
items: DisplayItem[],
|
||||
itemId: string,
|
||||
turnId: string,
|
||||
delta: string,
|
||||
fallbackLabel: string,
|
||||
): DisplayItem[] {
|
||||
const index = items.findIndex((item) => item.itemId === itemId);
|
||||
if (index !== -1) {
|
||||
return items.map((item, itemIndex) =>
|
||||
itemIndex === index && (item.kind === "tool" || item.kind === "hook" || item.kind === "reasoning")
|
||||
? ({ ...item, output: `${item.output ?? ""}${delta}` } as DisplayItem)
|
||||
: item,
|
||||
);
|
||||
}
|
||||
return [
|
||||
...items,
|
||||
{
|
||||
id: itemId,
|
||||
kind: "tool",
|
||||
role: "tool",
|
||||
text: "details",
|
||||
toolLabel: fallbackLabel,
|
||||
turnId,
|
||||
itemId,
|
||||
output: delta,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function appendItemOutput(
|
||||
items: DisplayItem[],
|
||||
itemId: string,
|
||||
turnId: string,
|
||||
delta: string,
|
||||
kind: "command" | "fileChange",
|
||||
fallbackText: string,
|
||||
): DisplayItem[] {
|
||||
const index = items.findIndex((item) => item.itemId === itemId);
|
||||
if (index !== -1) {
|
||||
return items.map((item, itemIndex) =>
|
||||
itemIndex === index && (item.kind === "command" || item.kind === "fileChange")
|
||||
? ({ ...item, output: `${item.output ?? ""}${delta}` } as DisplayItem)
|
||||
: item,
|
||||
);
|
||||
}
|
||||
return [
|
||||
...items,
|
||||
{
|
||||
id: itemId,
|
||||
kind,
|
||||
role: "tool",
|
||||
text: fallbackText,
|
||||
turnId,
|
||||
itemId,
|
||||
output: delta,
|
||||
...(kind === "fileChange"
|
||||
? {
|
||||
status: "inProgress",
|
||||
changes: [],
|
||||
}
|
||||
: {
|
||||
command: fallbackText,
|
||||
cwd: "(unknown)",
|
||||
status: "running",
|
||||
}),
|
||||
},
|
||||
] as DisplayItem[];
|
||||
}
|
||||
|
||||
export function displayBlocksForItems(items: DisplayItem[], activeTurnId: string | null, workspaceRoot?: string | null): DisplayBlock[] {
|
||||
const visibleItems = items.filter(shouldShowDisplayItem);
|
||||
const orderedItems = activeTurnId ? moveActiveTaskProgressToEnd(visibleItems, activeTurnId) : visibleItems;
|
||||
const editedFilesByTurn = editedFilesForTurns(visibleItems, workspaceRoot);
|
||||
const finalAssistantIdByTurn = finalAssistantItemsByTurn(visibleItems);
|
||||
const groupedTurnIds = new Set([...finalAssistantIdByTurn.keys()].filter((turnId) => turnId !== activeTurnId));
|
||||
|
||||
const groupedActivities = new Map<string, DisplayItem[]>();
|
||||
for (const item of orderedItems) {
|
||||
if (!item.turnId || !groupedTurnIds.has(item.turnId) || !isCompletedTurnDetailItem(item, finalAssistantIdByTurn)) continue;
|
||||
const group = groupedActivities.get(item.turnId) ?? [];
|
||||
group.push(item);
|
||||
groupedActivities.set(item.turnId, group);
|
||||
}
|
||||
|
||||
const emittedGroups = new Set<string>();
|
||||
const blocks: DisplayBlock[] = [];
|
||||
for (const item of orderedItems) {
|
||||
const turnId = item.turnId;
|
||||
if (turnId && groupedActivities.has(turnId) && isCompletedTurnDetailItem(item, finalAssistantIdByTurn)) {
|
||||
if (!emittedGroups.has(turnId)) {
|
||||
const groupItems = groupedActivities.get(turnId) ?? [];
|
||||
blocks.push({
|
||||
type: "activityGroup",
|
||||
id: `turn-${turnId}-activity`,
|
||||
turnId,
|
||||
summary: turnActivitySummary(groupItems),
|
||||
items: groupItems,
|
||||
});
|
||||
emittedGroups.add(turnId);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
blocks.push({ type: "item", item: itemWithEditedFiles(item, editedFilesByTurn, finalAssistantIdByTurn) });
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
function moveActiveTaskProgressToEnd(items: DisplayItem[], activeTurnId: string): DisplayItem[] {
|
||||
const activeTaskProgress = items.filter((item) => item.kind === "taskProgress" && item.turnId === activeTurnId);
|
||||
if (activeTaskProgress.length === 0) return items;
|
||||
return [...items.filter((item) => item.kind !== "taskProgress" || item.turnId !== activeTurnId), ...activeTaskProgress];
|
||||
}
|
||||
|
||||
function shouldShowDisplayItem(item: DisplayItem): boolean {
|
||||
return item.kind !== "reasoning" || executionState(item) !== "completed" || item.text.trim().length > 0;
|
||||
}
|
||||
|
||||
function isCompletedTurnDetailItem(item: DisplayItem, finalAssistantIdByTurn: Map<string, string>): boolean {
|
||||
if (!item.turnId || item.role === "user") return false;
|
||||
return finalAssistantIdByTurn.get(item.turnId) !== item.id;
|
||||
}
|
||||
|
||||
function finalAssistantItemsByTurn(items: DisplayItem[]): Map<string, string> {
|
||||
const finalAssistantIdByTurn = new Map<string, string>();
|
||||
for (const item of items) {
|
||||
if (!item.turnId || !isFinalAssistantMessage(item)) continue;
|
||||
finalAssistantIdByTurn.set(item.turnId, item.id);
|
||||
}
|
||||
return finalAssistantIdByTurn;
|
||||
}
|
||||
|
||||
function isFinalAssistantMessage(item: DisplayItem): boolean {
|
||||
return item.kind === "message" && item.role === "assistant" && item.markdown !== false;
|
||||
}
|
||||
|
||||
function itemWithEditedFiles(
|
||||
item: DisplayItem,
|
||||
editedFilesByTurn: Map<string, string[]>,
|
||||
finalAssistantIdByTurn: Map<string, string>,
|
||||
): DisplayItem {
|
||||
if (!item.turnId || finalAssistantIdByTurn.get(item.turnId) !== item.id) return item;
|
||||
if (item.kind !== "message") return item;
|
||||
const editedFiles = editedFilesByTurn.get(item.turnId);
|
||||
if (!editedFiles || editedFiles.length === 0) return item;
|
||||
return { ...item, editedFiles };
|
||||
}
|
||||
|
||||
function editedFilesForTurns(items: DisplayItem[], workspaceRoot?: string | null): Map<string, string[]> {
|
||||
const byTurn = new Map<string, Set<string>>();
|
||||
for (const item of items) {
|
||||
if (!item.turnId || item.kind !== "fileChange") continue;
|
||||
const files = editedFilesForItem(item, workspaceRoot);
|
||||
if (files.length === 0) continue;
|
||||
const set = byTurn.get(item.turnId) ?? new Set<string>();
|
||||
files.forEach((file) => set.add(file));
|
||||
byTurn.set(item.turnId, set);
|
||||
}
|
||||
|
||||
return new Map([...byTurn].map(([turnId, files]) => [turnId, [...files].sort((a, b) => a.localeCompare(b))]));
|
||||
}
|
||||
|
||||
function editedFilesForItem(item: DisplayItem, workspaceRoot?: string | null): string[] {
|
||||
if (item.kind !== "fileChange") return [];
|
||||
return item.changes.flatMap((change) =>
|
||||
change.path && change.path !== "(unknown)" ? [pathRelativeToWorkspace(change.path, workspaceRoot)] : [],
|
||||
);
|
||||
}
|
||||
|
||||
export function pathRelativeToWorkspace(path: string, workspaceRoot?: string | null): string {
|
||||
const normalizedPath = path.replace(/\\/g, "/").replace(/^\.\//, "");
|
||||
const root = workspaceRoot?.replace(/\\/g, "/").replace(/\/+$/, "");
|
||||
if (!root) return normalizedPath;
|
||||
if (normalizedPath === root) return ".";
|
||||
return normalizedPath.startsWith(`${root}/`) ? normalizedPath.slice(root.length + 1) : normalizedPath;
|
||||
}
|
||||
|
||||
function turnActivitySummary(items: DisplayItem[]): string {
|
||||
const parts = [
|
||||
countMatchingLabel(items, (item) => item.kind === "message" && item.role === "assistant", "response", "responses"),
|
||||
countLabel(items, "taskProgress", "task progress"),
|
||||
countLabel(items, "agent", "agent"),
|
||||
countLabel(items, "command", "command"),
|
||||
countLabel(items, "fileChange", "file change"),
|
||||
countLabel(items, "tool", "tool"),
|
||||
countLabel(items, "hook", "hook"),
|
||||
countLabel(items, "reasoning", "thought", "thought notes"),
|
||||
countLabel(items, "approvalResult", "approval"),
|
||||
countLabel(items, "userInputResult", "input"),
|
||||
countLabel(items, "reviewResult", "review"),
|
||||
].filter((part): part is string => Boolean(part));
|
||||
|
||||
if (parts.length === 0) return "Work details";
|
||||
return `Work details: ${parts.join(", ")}`;
|
||||
}
|
||||
|
||||
function countMatchingLabel(
|
||||
items: DisplayItem[],
|
||||
predicate: (item: DisplayItem) => boolean,
|
||||
label: string,
|
||||
pluralLabel = `${label}s`,
|
||||
): string | null {
|
||||
const count = items.filter(predicate).length;
|
||||
if (count === 0) return null;
|
||||
if (count === 1) return label;
|
||||
return `${count} ${pluralLabel}`;
|
||||
}
|
||||
|
||||
function countLabel(items: DisplayItem[], kind: DisplayKind, label: string, pluralLabel = `${label}s`): string | null {
|
||||
const count = items.filter((item) => item.kind === kind).length;
|
||||
if (count === 0) return null;
|
||||
if (count === 1) return label;
|
||||
return `${count} ${pluralLabel}`;
|
||||
}
|
||||
|
||||
export function createSystemItem(text: string): DisplayItem {
|
||||
return {
|
||||
id: `system-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||
kind: "system",
|
||||
role: "system",
|
||||
text,
|
||||
};
|
||||
}
|
||||
173
src/display/review.ts
Normal file
173
src/display/review.ts
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
import type { FileSystemPath } from "../generated/app-server/v2/FileSystemPath";
|
||||
import type { GuardianApprovalReviewAction } from "../generated/app-server/v2/GuardianApprovalReviewAction";
|
||||
import type { ItemGuardianApprovalReviewCompletedNotification } from "../generated/app-server/v2/ItemGuardianApprovalReviewCompletedNotification";
|
||||
import type { ItemGuardianApprovalReviewStartedNotification } from "../generated/app-server/v2/ItemGuardianApprovalReviewStartedNotification";
|
||||
import type { DisplayItem } from "./types";
|
||||
import { classifyExecutionState } from "./state";
|
||||
|
||||
type AutoReviewNotification = ItemGuardianApprovalReviewStartedNotification | ItemGuardianApprovalReviewCompletedNotification;
|
||||
type DisplayRow = { key: string; value: string };
|
||||
|
||||
export function createReviewResultItem(text: string): DisplayItem {
|
||||
const parsed = parseAutomaticApprovalReviewMessage(text);
|
||||
if (parsed) {
|
||||
return {
|
||||
id: `review-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||
kind: "reviewResult",
|
||||
role: "tool",
|
||||
text: parsed.summary,
|
||||
markdown: false,
|
||||
state: classifyExecutionState({ status: parsed.status }),
|
||||
details: [{ title: "Review", rows: parsed.rows }],
|
||||
};
|
||||
}
|
||||
return {
|
||||
id: `review-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||
kind: "reviewResult",
|
||||
role: "tool",
|
||||
text,
|
||||
markdown: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function createAutoReviewResultItem(params: AutoReviewNotification): DisplayItem {
|
||||
const completed = "decisionSource" in params;
|
||||
const status = params.review.status;
|
||||
const action = autoReviewActionLabel(params.action);
|
||||
const text = completed ? `Auto-review ${status}: ${action}` : `Auto-review started: ${action}`;
|
||||
const rows = [
|
||||
{ key: "status", value: status },
|
||||
...("decisionSource" in params ? [{ key: "source", value: params.decisionSource }] : []),
|
||||
...(params.review.riskLevel ? [{ key: "risk", value: params.review.riskLevel }] : []),
|
||||
...(params.review.userAuthorization ? [{ key: "authorization", value: params.review.userAuthorization }] : []),
|
||||
...autoReviewActionRows(params.action),
|
||||
...(params.targetItemId ? [{ key: "target", value: params.targetItemId }] : []),
|
||||
...(params.review.rationale ? [{ key: "rationale", value: params.review.rationale }] : []),
|
||||
];
|
||||
return {
|
||||
id: `review-${params.reviewId}`,
|
||||
kind: "reviewResult",
|
||||
role: "tool",
|
||||
text,
|
||||
turnId: params.turnId,
|
||||
markdown: false,
|
||||
state: completed ? classifyExecutionState({ status }) : "running",
|
||||
details: [{ title: "Review", rows }],
|
||||
};
|
||||
}
|
||||
|
||||
function parseAutomaticApprovalReviewMessage(
|
||||
text: string,
|
||||
): { status: string; summary: string; rows: Array<{ key: string; value: string }> } | null {
|
||||
const match = /^Automatic approval review\s+([a-zA-Z][\w-]*)(?:\s+\(([^)]*)\))?:\s*(.+)$/i.exec(text.trim());
|
||||
if (!match) return null;
|
||||
|
||||
const status = match[1]?.trim() ?? "review";
|
||||
const fieldText = match[2]?.trim();
|
||||
const message = match[3]?.trim() ?? "";
|
||||
const rows = [{ key: "status", value: status }];
|
||||
for (const field of fieldText?.split(",") ?? []) {
|
||||
const fieldMatch = /^\s*([^:]+):\s*(.+?)\s*$/.exec(field);
|
||||
if (fieldMatch?.[1] && fieldMatch[2]) rows.push({ key: fieldMatch[1].trim(), value: fieldMatch[2].trim() });
|
||||
}
|
||||
if (message) rows.push({ key: "message", value: message });
|
||||
|
||||
return {
|
||||
status,
|
||||
summary: message ? `Auto-review ${status}: ${message}` : `Auto-review ${status}`,
|
||||
rows,
|
||||
};
|
||||
}
|
||||
|
||||
function autoReviewActionRows(action: GuardianApprovalReviewAction): DisplayRow[] {
|
||||
if (action.type === "command") {
|
||||
return [
|
||||
{ key: "action", value: "command" },
|
||||
{ key: "command", value: action.command },
|
||||
{ key: "cwd", value: action.cwd },
|
||||
{ key: "action source", value: action.source },
|
||||
];
|
||||
}
|
||||
if (action.type === "execve") {
|
||||
return [
|
||||
{ key: "action", value: "execve" },
|
||||
{ key: "program", value: action.program },
|
||||
{ key: "argv", value: action.argv.join(" ") },
|
||||
{ key: "cwd", value: action.cwd },
|
||||
{ key: "action source", value: action.source },
|
||||
];
|
||||
}
|
||||
if (action.type === "applyPatch") {
|
||||
return [
|
||||
{ key: "action", value: "apply patch" },
|
||||
{ key: "cwd", value: action.cwd },
|
||||
{ key: "files", value: action.files.length > 0 ? action.files.join("\n") : "(none)" },
|
||||
];
|
||||
}
|
||||
if (action.type === "networkAccess") {
|
||||
return [
|
||||
{ key: "action", value: "network access" },
|
||||
{ key: "target", value: action.target },
|
||||
{ key: "protocol", value: action.protocol },
|
||||
{ key: "host", value: action.host },
|
||||
{ key: "port", value: String(action.port) },
|
||||
];
|
||||
}
|
||||
if (action.type === "mcpToolCall") {
|
||||
return [
|
||||
{ key: "action", value: "MCP tool call" },
|
||||
{ key: "server", value: action.server },
|
||||
{ key: "tool", value: action.toolName },
|
||||
...(action.toolTitle ? [{ key: "title", value: action.toolTitle }] : []),
|
||||
...(action.connectorName ? [{ key: "connector", value: action.connectorName }] : []),
|
||||
...(action.connectorId ? [{ key: "connector id", value: action.connectorId }] : []),
|
||||
];
|
||||
}
|
||||
if (action.type === "requestPermissions") {
|
||||
return [
|
||||
{ key: "action", value: "request permissions" },
|
||||
...(action.reason ? [{ key: "reason", value: action.reason }] : []),
|
||||
...permissionRows(action.permissions),
|
||||
];
|
||||
}
|
||||
return [{ key: "action", value: "review action" }];
|
||||
}
|
||||
|
||||
function permissionRows(permissions: Extract<GuardianApprovalReviewAction, { type: "requestPermissions" }>["permissions"]): DisplayRow[] {
|
||||
const rows: DisplayRow[] = [];
|
||||
if (permissions.network?.enabled !== null && permissions.network?.enabled !== undefined) {
|
||||
rows.push({ key: "network", value: permissions.network.enabled ? "enabled" : "disabled" });
|
||||
}
|
||||
const fileSystem = permissions.fileSystem;
|
||||
if (!fileSystem) return rows;
|
||||
if (fileSystem.entries && fileSystem.entries.length > 0) {
|
||||
rows.push({
|
||||
key: "filesystem",
|
||||
value: fileSystem.entries.map((entry) => `${fileSystemPathLabel(entry.path)} (${entry.access})`).join("\n"),
|
||||
});
|
||||
}
|
||||
if (fileSystem.read && fileSystem.read.length > 0) rows.push({ key: "read", value: fileSystem.read.join("\n") });
|
||||
if (fileSystem.write && fileSystem.write.length > 0) rows.push({ key: "write", value: fileSystem.write.join("\n") });
|
||||
if (fileSystem.globScanMaxDepth !== null && fileSystem.globScanMaxDepth !== undefined) {
|
||||
rows.push({ key: "glob depth", value: String(fileSystem.globScanMaxDepth) });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
function fileSystemPathLabel(path: FileSystemPath): string {
|
||||
if (path.type === "path") return path.path;
|
||||
if (path.type === "glob_pattern") return path.pattern;
|
||||
if (path.value.kind === "project_roots") return path.value.subpath ? `project_roots/${path.value.subpath}` : "project_roots";
|
||||
if (path.value.kind === "unknown") return path.value.subpath ? `${path.value.path}/${path.value.subpath}` : path.value.path;
|
||||
return path.value.kind;
|
||||
}
|
||||
|
||||
function autoReviewActionLabel(action: GuardianApprovalReviewAction): string {
|
||||
if (action.type === "command") return action.command;
|
||||
if (action.type === "execve") return [action.program, ...action.argv].join(" ");
|
||||
if (action.type === "applyPatch") return `apply patch (${action.files.length} files)`;
|
||||
if (action.type === "networkAccess") return `${action.protocol}://${action.host}:${action.port}`;
|
||||
if (action.type === "mcpToolCall") return `${action.server}.${action.toolName}`;
|
||||
if (action.type === "requestPermissions") return action.reason ?? "permission request";
|
||||
return "review action";
|
||||
}
|
||||
56
src/display/signature.ts
Normal file
56
src/display/signature.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { executionState } from "./model";
|
||||
import type { DisplayItem } from "./types";
|
||||
|
||||
export interface DisplayItemSignatureContext {
|
||||
busy: boolean;
|
||||
activeTurnId: string | null;
|
||||
displayItems: DisplayItem[];
|
||||
workspaceRoot?: string | null;
|
||||
}
|
||||
|
||||
export function displayItemSignature(item: DisplayItem, context: DisplayItemSignatureContext): string {
|
||||
return [
|
||||
item.id,
|
||||
item.kind,
|
||||
item.role,
|
||||
item.turnId ?? "",
|
||||
item.itemId ?? "",
|
||||
item.text,
|
||||
"markdown" in item ? String(item.markdown ?? true) : "",
|
||||
"output" in item ? (item.output ?? "") : "",
|
||||
"details" in item ? JSON.stringify(item.details ?? []) : "",
|
||||
item.kind === "message" ? (item.editedFiles?.join("\n") ?? "") : "",
|
||||
item.kind === "reasoning" && isReasoningActive(item, context) ? "reasoning-active" : "",
|
||||
executionState(item) ?? "",
|
||||
item.kind === "fileChange" ? JSON.stringify(item.changes) : "",
|
||||
item.kind === "fileChange" ? (context.workspaceRoot ?? "") : "",
|
||||
item.kind === "taskProgress" ? JSON.stringify({ explanation: item.explanation, steps: item.steps, status: item.status }) : "",
|
||||
item.kind === "agent"
|
||||
? JSON.stringify({
|
||||
tool: item.tool,
|
||||
status: item.status,
|
||||
senderThreadId: item.senderThreadId,
|
||||
receiverThreadIds: item.receiverThreadIds,
|
||||
prompt: item.prompt,
|
||||
model: item.model,
|
||||
reasoningEffort: item.reasoningEffort,
|
||||
agents: item.agents,
|
||||
})
|
||||
: "",
|
||||
item.kind === "command" ? [item.command, item.cwd, item.status, item.exitCode ?? "", item.durationMs ?? ""].join("\n") : "",
|
||||
item.kind === "fileChange" ? item.status : "",
|
||||
item.kind === "tool" || item.kind === "taskProgress" || item.kind === "agent" || item.kind === "hook" || item.kind === "reasoning"
|
||||
? (item.status ?? "")
|
||||
: "",
|
||||
].join("\u0000");
|
||||
}
|
||||
|
||||
export function isReasoningActive(
|
||||
item: DisplayItem,
|
||||
context: Pick<DisplayItemSignatureContext, "busy" | "activeTurnId" | "displayItems">,
|
||||
): boolean {
|
||||
if (!context.busy || !context.activeTurnId || item.turnId !== context.activeTurnId) return false;
|
||||
if (executionState(item) === "completed") return false;
|
||||
const latestActiveTurnItem = [...context.displayItems].reverse().find((candidate) => candidate.turnId === context.activeTurnId);
|
||||
return latestActiveTurnItem?.id === item.id;
|
||||
}
|
||||
38
src/display/state.ts
Normal file
38
src/display/state.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import type { DisplayItem, ExecutionState } from "./types";
|
||||
|
||||
export function executionState(item: DisplayItem): ExecutionState {
|
||||
if (item.state) return item.state;
|
||||
const exitCode = item.kind === "command" ? item.exitCode : undefined;
|
||||
const status =
|
||||
item.kind === "command" ||
|
||||
item.kind === "fileChange" ||
|
||||
item.kind === "tool" ||
|
||||
item.kind === "taskProgress" ||
|
||||
item.kind === "agent" ||
|
||||
item.kind === "hook" ||
|
||||
item.kind === "reasoning"
|
||||
? item.status
|
||||
: undefined;
|
||||
return classifyExecutionState({ exitCode, status });
|
||||
}
|
||||
|
||||
export function classifyExecutionState(input: { exitCode?: number; status?: unknown }): ExecutionState {
|
||||
if (typeof input.exitCode === "number" && input.exitCode !== 0) return "failed";
|
||||
|
||||
const statusText = [input.status]
|
||||
.filter((value) => value !== null && value !== undefined)
|
||||
.join(" ")
|
||||
.toLowerCase();
|
||||
|
||||
if (/(fail|error|errored|notfound|not_found|missing|denied|declin|cancel|reject|aborted)/.test(statusText)) return "failed";
|
||||
if (/(running|in[_ -]?progress|queued|pending|started)/.test(statusText)) return "running";
|
||||
if (/(completed|complete|success|succeeded|approved|allowed|applied|finished|done)/.test(statusText)) return "completed";
|
||||
if (typeof input.exitCode === "number" && input.exitCode === 0) return "completed";
|
||||
return null;
|
||||
}
|
||||
|
||||
export function executionStateLabel(state: Exclude<ExecutionState, null>): string {
|
||||
if (state === "running") return "Running";
|
||||
if (state === "failed") return "Failed";
|
||||
return "Done";
|
||||
}
|
||||
139
src/display/tool-view.ts
Normal file
139
src/display/tool-view.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import { executionState, pathRelativeToWorkspace } from "./model";
|
||||
import type {
|
||||
CommandDisplayItem,
|
||||
DisplayDetailSection,
|
||||
DisplayFileChange,
|
||||
DisplayItem,
|
||||
ExecutionState,
|
||||
FileChangeDisplayItem,
|
||||
ReviewResultDisplayItem,
|
||||
ToolDisplayItem,
|
||||
} from "./types";
|
||||
|
||||
export type ToolResultDisplayItem = CommandDisplayItem | FileChangeDisplayItem | ToolDisplayItem | ReviewResultDisplayItem;
|
||||
|
||||
export type ToolResultDetailSection =
|
||||
| { kind: "meta"; title?: string; rows: Array<{ key: string; value: string }> }
|
||||
| { kind: "output"; title: string; body: string }
|
||||
| { kind: "diff"; title: string; diff: string };
|
||||
|
||||
export interface ToolResultView {
|
||||
className: string;
|
||||
label: string;
|
||||
summary: string;
|
||||
detailsKey: string;
|
||||
details: ToolResultDetailSection[];
|
||||
state: ExecutionState;
|
||||
}
|
||||
|
||||
export function toolResultView(item: ToolResultDisplayItem, workspaceRoot?: string | null): ToolResultView {
|
||||
if (item.kind === "command") return commandToolView(item);
|
||||
if (item.kind === "fileChange") return fileChangeToolView(item, workspaceRoot);
|
||||
if (item.kind === "reviewResult") return reviewToolView(item);
|
||||
return genericToolView(item);
|
||||
}
|
||||
|
||||
function commandToolView(item: CommandDisplayItem): ToolResultView {
|
||||
const details: ToolResultDetailSection[] = [
|
||||
{
|
||||
kind: "meta",
|
||||
rows: [
|
||||
{ key: "command", value: item.command },
|
||||
{ key: "cwd", value: item.cwd },
|
||||
{ key: "status", value: item.status },
|
||||
...(item.exitCode !== null && item.exitCode !== undefined ? [{ key: "exit", value: String(item.exitCode) }] : []),
|
||||
...(item.durationMs !== null && item.durationMs !== undefined ? [{ key: "duration", value: `${item.durationMs}ms` }] : []),
|
||||
],
|
||||
},
|
||||
...outputSection("Output", item.output),
|
||||
];
|
||||
return {
|
||||
className: "codex-panel__message codex-panel__message--tool codex-panel__tool-item",
|
||||
label: item.actionLabel ?? "command",
|
||||
summary: item.text,
|
||||
detailsKey: `${item.id}:command-details`,
|
||||
details,
|
||||
state: executionState(item),
|
||||
};
|
||||
}
|
||||
|
||||
function fileChangeToolView(item: FileChangeDisplayItem, workspaceRoot?: string | null): ToolResultView {
|
||||
const displayChanges = item.changes.map((change) => ({
|
||||
...change,
|
||||
displayPath: change.path && change.path !== "(unknown)" ? pathRelativeToWorkspace(change.path, workspaceRoot) : change.path,
|
||||
}));
|
||||
const details: ToolResultDetailSection[] = [
|
||||
{
|
||||
kind: "meta",
|
||||
rows: [
|
||||
{ key: "status", value: item.status },
|
||||
{ key: "files", value: String(item.changes.length) },
|
||||
],
|
||||
},
|
||||
...displayChanges.map((change) => ({
|
||||
kind: "diff" as const,
|
||||
title: `${change.kind ?? "changed"} ${change.displayPath ?? "(unknown)"}`,
|
||||
diff: change.diff ?? "",
|
||||
})),
|
||||
...outputSection("Patch output", item.output),
|
||||
];
|
||||
return {
|
||||
className: "codex-panel__message codex-panel__message--tool codex-panel__file-change",
|
||||
label: "file change",
|
||||
summary: fileChangeSummary(item, displayChanges),
|
||||
detailsKey: `${item.id}:file-change-details`,
|
||||
details,
|
||||
state: executionState(item),
|
||||
};
|
||||
}
|
||||
|
||||
function genericToolView(item: ToolDisplayItem): ToolResultView {
|
||||
return {
|
||||
className: `codex-panel__message codex-panel__message--tool codex-panel__tool-item codex-panel__tool-item--${item.kind}`,
|
||||
label: item.toolLabel ?? item.kind,
|
||||
summary: item.text,
|
||||
detailsKey: `${item.id}:details`,
|
||||
details: [
|
||||
...(item.details ?? []).flatMap(detailSection),
|
||||
...outputSection(item.kind === "hook" ? "Hook output" : "Output", item.output),
|
||||
],
|
||||
state: executionState(item),
|
||||
};
|
||||
}
|
||||
|
||||
function reviewToolView(item: ReviewResultDisplayItem): ToolResultView {
|
||||
return {
|
||||
className:
|
||||
"codex-panel__message codex-panel__message--tool codex-panel__message--review-result codex-panel__tool-item codex-panel__tool-item--review",
|
||||
label: "auto-review",
|
||||
summary: item.text,
|
||||
detailsKey: `${item.id}:review-details`,
|
||||
details: (item.details ?? []).flatMap(reviewDetailSection),
|
||||
state: executionState(item),
|
||||
};
|
||||
}
|
||||
|
||||
function reviewDetailSection(section: DisplayDetailSection): ToolResultDetailSection[] {
|
||||
if (section.rows && section.rows.length > 0) return [{ kind: "meta", rows: section.rows }];
|
||||
return detailSection(section);
|
||||
}
|
||||
|
||||
function detailSection(section: DisplayDetailSection): ToolResultDetailSection[] {
|
||||
if (section.rows && section.rows.length > 0) return [{ kind: "meta", title: section.title, rows: section.rows }];
|
||||
if (section.body) return [{ kind: "output", title: section.title ?? "Output", body: section.body }];
|
||||
return [];
|
||||
}
|
||||
|
||||
function outputSection(title: string, body: string | null | undefined): ToolResultDetailSection[] {
|
||||
return body ? [{ kind: "output", title, body }] : [];
|
||||
}
|
||||
|
||||
function fileChangeSummary(item: DisplayItem, changes: Array<DisplayFileChange & { displayPath: string }>): string {
|
||||
if (item.kind !== "fileChange") return item.text;
|
||||
if (changes.length === 0) return item.text;
|
||||
if (changes.length > 1) return item.text;
|
||||
const relativePath = changes[0]?.displayPath;
|
||||
if (!relativePath || relativePath === "(unknown)") return item.text;
|
||||
const suffixMatch = /\s\(([^)]+)\)$/.exec(item.text);
|
||||
return suffixMatch ? `${relativePath} (${suffixMatch[1]})` : relativePath;
|
||||
}
|
||||
161
src/display/types.ts
Normal file
161
src/display/types.ts
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
export type DisplayKind =
|
||||
| "message"
|
||||
| "command"
|
||||
| "fileChange"
|
||||
| "tool"
|
||||
| "taskProgress"
|
||||
| "agent"
|
||||
| "hook"
|
||||
| "reasoning"
|
||||
| "system"
|
||||
| "approvalResult"
|
||||
| "userInputResult"
|
||||
| "reviewResult";
|
||||
export type DisplayRole = "user" | "assistant" | "system" | "tool";
|
||||
export type ExecutionState = "running" | "completed" | "failed" | null;
|
||||
|
||||
export interface DisplayBase {
|
||||
id: string;
|
||||
kind: DisplayKind;
|
||||
role: DisplayRole;
|
||||
text: string;
|
||||
turnId?: string;
|
||||
itemId?: string;
|
||||
state?: ExecutionState;
|
||||
}
|
||||
|
||||
export interface DisplayDetailMetaRow {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface DisplayDetailSection {
|
||||
title?: string;
|
||||
body?: string;
|
||||
rows?: DisplayDetailMetaRow[];
|
||||
}
|
||||
|
||||
export interface MessageDisplayItem extends DisplayBase {
|
||||
kind: "message";
|
||||
role: "user" | "assistant";
|
||||
editedFiles?: string[];
|
||||
markdown?: boolean;
|
||||
}
|
||||
|
||||
export interface SystemDisplayItem extends DisplayBase {
|
||||
kind: "system" | "approvalResult" | "userInputResult";
|
||||
role: "system" | "tool";
|
||||
markdown?: boolean;
|
||||
details?: DisplayDetailSection[];
|
||||
}
|
||||
|
||||
export interface ReviewResultDisplayItem extends DisplayBase {
|
||||
kind: "reviewResult";
|
||||
role: "tool";
|
||||
markdown?: boolean;
|
||||
details?: DisplayDetailSection[];
|
||||
}
|
||||
|
||||
export interface CommandDisplayItem extends DisplayBase {
|
||||
kind: "command";
|
||||
role: "tool";
|
||||
actionLabel?: string;
|
||||
command: string;
|
||||
cwd: string;
|
||||
status: string;
|
||||
exitCode?: number;
|
||||
durationMs?: number;
|
||||
output?: string;
|
||||
}
|
||||
|
||||
export interface DisplayFileChange {
|
||||
kind: string;
|
||||
path: string;
|
||||
diff: string;
|
||||
}
|
||||
|
||||
export interface FileChangeDisplayItem extends DisplayBase {
|
||||
kind: "fileChange";
|
||||
role: "tool";
|
||||
status: string;
|
||||
changes: DisplayFileChange[];
|
||||
output?: string;
|
||||
}
|
||||
|
||||
export interface ToolDisplayItem extends DisplayBase {
|
||||
kind: "tool" | "hook" | "reasoning";
|
||||
role: "tool";
|
||||
toolLabel?: string;
|
||||
status?: string;
|
||||
output?: string;
|
||||
details?: DisplayDetailSection[];
|
||||
}
|
||||
|
||||
export interface TaskProgressStep {
|
||||
step: string;
|
||||
status: "pending" | "inProgress" | "completed";
|
||||
}
|
||||
|
||||
export interface TaskProgressDisplayItem extends DisplayBase {
|
||||
kind: "taskProgress";
|
||||
role: "tool";
|
||||
explanation: string | null;
|
||||
steps: TaskProgressStep[];
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface AgentStateDisplay {
|
||||
threadId: string;
|
||||
status: string;
|
||||
message: string | null;
|
||||
}
|
||||
|
||||
export interface AgentRunSummaryAgent {
|
||||
threadId: string;
|
||||
status: string;
|
||||
messagePreview: string | null;
|
||||
}
|
||||
|
||||
export interface AgentDisplayItem extends DisplayBase {
|
||||
kind: "agent";
|
||||
role: "tool";
|
||||
tool: string;
|
||||
status: string;
|
||||
senderThreadId: string;
|
||||
receiverThreadIds: string[];
|
||||
prompt: string | null;
|
||||
model: string | null;
|
||||
reasoningEffort: string | null;
|
||||
agents: AgentStateDisplay[];
|
||||
}
|
||||
|
||||
export interface AgentRunSummary {
|
||||
running: number;
|
||||
completed: number;
|
||||
failed: number;
|
||||
agents: AgentRunSummaryAgent[];
|
||||
additionalAgents: number;
|
||||
}
|
||||
|
||||
export type DisplayItem =
|
||||
| MessageDisplayItem
|
||||
| SystemDisplayItem
|
||||
| CommandDisplayItem
|
||||
| FileChangeDisplayItem
|
||||
| ToolDisplayItem
|
||||
| TaskProgressDisplayItem
|
||||
| AgentDisplayItem
|
||||
| ReviewResultDisplayItem;
|
||||
|
||||
export type DisplayBlock =
|
||||
| {
|
||||
type: "item";
|
||||
item: DisplayItem;
|
||||
}
|
||||
| {
|
||||
type: "activityGroup";
|
||||
id: string;
|
||||
turnId: string;
|
||||
summary: string;
|
||||
items: DisplayItem[];
|
||||
};
|
||||
14
src/generated/app-server/AbsolutePathBuf.ts
Normal file
14
src/generated/app-server/AbsolutePathBuf.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* A path that is guaranteed to be absolute and normalized (though it is not
|
||||
* guaranteed to be canonicalized or exist on the filesystem).
|
||||
*
|
||||
* IMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set
|
||||
* using [AbsolutePathBufGuard::new]. If no base path is set, the
|
||||
* deserialization will fail unless the path being deserialized is already
|
||||
* absolute.
|
||||
*/
|
||||
export type AbsolutePathBuf = string;
|
||||
5
src/generated/app-server/AgentPath.ts
Normal file
5
src/generated/app-server/AgentPath.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type AgentPath = string;
|
||||
21
src/generated/app-server/ApplyPatchApprovalParams.ts
Normal file
21
src/generated/app-server/ApplyPatchApprovalParams.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { FileChange } from "./FileChange";
|
||||
import type { ThreadId } from "./ThreadId";
|
||||
|
||||
export type ApplyPatchApprovalParams = { conversationId: ThreadId,
|
||||
/**
|
||||
* Use to correlate this with [codex_protocol::protocol::PatchApplyBeginEvent]
|
||||
* and [codex_protocol::protocol::PatchApplyEndEvent].
|
||||
*/
|
||||
callId: string, fileChanges: { [key in string]?: FileChange },
|
||||
/**
|
||||
* Optional explanatory reason (e.g. request for extra write access).
|
||||
*/
|
||||
reason: string | null,
|
||||
/**
|
||||
* When set, the agent is asking the user to allow writes under this root
|
||||
* for the remainder of the session (unclear if this is honored today).
|
||||
*/
|
||||
grantRoot: string | null, };
|
||||
6
src/generated/app-server/ApplyPatchApprovalResponse.ts
Normal file
6
src/generated/app-server/ApplyPatchApprovalResponse.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ReviewDecision } from "./ReviewDecision";
|
||||
|
||||
export type ApplyPatchApprovalResponse = { decision: ReviewDecision, };
|
||||
8
src/generated/app-server/AuthMode.ts
Normal file
8
src/generated/app-server/AuthMode.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Authentication mode for OpenAI-backed providers.
|
||||
*/
|
||||
export type AuthMode = "apikey" | "chatgpt" | "chatgptAuthTokens" | "agentIdentity";
|
||||
5
src/generated/app-server/ClientInfo.ts
Normal file
5
src/generated/app-server/ClientInfo.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ClientInfo = { name: string, title: string | null, version: string, };
|
||||
5
src/generated/app-server/ClientNotification.ts
Normal file
5
src/generated/app-server/ClientNotification.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ClientNotification = { "method": "initialized" };
|
||||
105
src/generated/app-server/ClientRequest.ts
Normal file
105
src/generated/app-server/ClientRequest.ts
Normal file
File diff suppressed because one or more lines are too long
10
src/generated/app-server/CollaborationMode.ts
Normal file
10
src/generated/app-server/CollaborationMode.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ModeKind } from "./ModeKind";
|
||||
import type { Settings } from "./Settings";
|
||||
|
||||
/**
|
||||
* Collaboration mode for a Codex session.
|
||||
*/
|
||||
export type CollaborationMode = { mode: ModeKind, settings: Settings, };
|
||||
6
src/generated/app-server/ContentItem.ts
Normal file
6
src/generated/app-server/ContentItem.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ImageDetail } from "./ImageDetail";
|
||||
|
||||
export type ContentItem = { "type": "input_text", text: string, } | { "type": "input_image", image_url: string, detail?: ImageDetail, } | { "type": "output_text", text: string, };
|
||||
5
src/generated/app-server/ConversationGitInfo.ts
Normal file
5
src/generated/app-server/ConversationGitInfo.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ConversationGitInfo = { sha: string | null, branch: string | null, origin_url: string | null, };
|
||||
8
src/generated/app-server/ConversationSummary.ts
Normal file
8
src/generated/app-server/ConversationSummary.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ConversationGitInfo } from "./ConversationGitInfo";
|
||||
import type { SessionSource } from "./SessionSource";
|
||||
import type { ThreadId } from "./ThreadId";
|
||||
|
||||
export type ConversationSummary = { conversationId: ThreadId, path: string, preview: string, timestamp: string | null, updatedAt: string | null, modelProvider: string, cwd: string, cliVersion: string, source: SessionSource, gitInfo: ConversationGitInfo | null, };
|
||||
16
src/generated/app-server/ExecCommandApprovalParams.ts
Normal file
16
src/generated/app-server/ExecCommandApprovalParams.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ParsedCommand } from "./ParsedCommand";
|
||||
import type { ThreadId } from "./ThreadId";
|
||||
|
||||
export type ExecCommandApprovalParams = { conversationId: ThreadId,
|
||||
/**
|
||||
* Use to correlate this with [codex_protocol::protocol::ExecCommandBeginEvent]
|
||||
* and [codex_protocol::protocol::ExecCommandEndEvent].
|
||||
*/
|
||||
callId: string,
|
||||
/**
|
||||
* Identifier for this specific approval callback.
|
||||
*/
|
||||
approvalId: string | null, command: Array<string>, cwd: string, reason: string | null, parsedCmd: Array<ParsedCommand>, };
|
||||
6
src/generated/app-server/ExecCommandApprovalResponse.ts
Normal file
6
src/generated/app-server/ExecCommandApprovalResponse.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ReviewDecision } from "./ReviewDecision";
|
||||
|
||||
export type ExecCommandApprovalResponse = { decision: ReviewDecision, };
|
||||
12
src/generated/app-server/ExecPolicyAmendment.ts
Normal file
12
src/generated/app-server/ExecPolicyAmendment.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Proposed execpolicy change to allow commands starting with this prefix.
|
||||
*
|
||||
* The `command` tokens form the prefix that would be added as an execpolicy
|
||||
* `prefix_rule(..., decision="allow")`, letting the agent bypass approval for
|
||||
* commands that start with this token sequence.
|
||||
*/
|
||||
export type ExecPolicyAmendment = Array<string>;
|
||||
5
src/generated/app-server/FileChange.ts
Normal file
5
src/generated/app-server/FileChange.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type FileChange = { "type": "add", content: string, } | { "type": "delete", content: string, } | { "type": "update", unified_diff: string, move_path: string | null, };
|
||||
5
src/generated/app-server/ForcedLoginMethod.ts
Normal file
5
src/generated/app-server/ForcedLoginMethod.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ForcedLoginMethod = "chatgpt" | "api";
|
||||
6
src/generated/app-server/FunctionCallOutputBody.ts
Normal file
6
src/generated/app-server/FunctionCallOutputBody.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { FunctionCallOutputContentItem } from "./FunctionCallOutputContentItem";
|
||||
|
||||
export type FunctionCallOutputBody = string | Array<FunctionCallOutputContentItem>;
|
||||
10
src/generated/app-server/FunctionCallOutputContentItem.ts
Normal file
10
src/generated/app-server/FunctionCallOutputContentItem.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ImageDetail } from "./ImageDetail";
|
||||
|
||||
/**
|
||||
* Responses API compatible content items that can be returned by a tool call.
|
||||
* This is a subset of ContentItem with the types we support as function call outputs.
|
||||
*/
|
||||
export type FunctionCallOutputContentItem = { "type": "input_text", text: string, } | { "type": "input_image", image_url: string, detail?: ImageDetail, };
|
||||
5
src/generated/app-server/FuzzyFileSearchMatchType.ts
Normal file
5
src/generated/app-server/FuzzyFileSearchMatchType.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type FuzzyFileSearchMatchType = "file" | "directory";
|
||||
5
src/generated/app-server/FuzzyFileSearchParams.ts
Normal file
5
src/generated/app-server/FuzzyFileSearchParams.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type FuzzyFileSearchParams = { query: string, roots: Array<string>, cancellationToken: string | null, };
|
||||
6
src/generated/app-server/FuzzyFileSearchResponse.ts
Normal file
6
src/generated/app-server/FuzzyFileSearchResponse.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { FuzzyFileSearchResult } from "./FuzzyFileSearchResult";
|
||||
|
||||
export type FuzzyFileSearchResponse = { files: Array<FuzzyFileSearchResult>, };
|
||||
9
src/generated/app-server/FuzzyFileSearchResult.ts
Normal file
9
src/generated/app-server/FuzzyFileSearchResult.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { FuzzyFileSearchMatchType } from "./FuzzyFileSearchMatchType";
|
||||
|
||||
/**
|
||||
* Superset of [`codex_file_search::FileMatch`]
|
||||
*/
|
||||
export type FuzzyFileSearchResult = { root: string, path: string, match_type: FuzzyFileSearchMatchType, file_name: string, score: number, indices: Array<number> | null, };
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type FuzzyFileSearchSessionCompletedNotification = { sessionId: string, };
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type FuzzyFileSearchSessionStartParams = { sessionId: string, roots: Array<string>, };
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type FuzzyFileSearchSessionStartResponse = Record<string, never>;
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type FuzzyFileSearchSessionStopParams = { sessionId: string, };
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type FuzzyFileSearchSessionStopResponse = Record<string, never>;
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type FuzzyFileSearchSessionUpdateParams = { sessionId: string, query: string, };
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type FuzzyFileSearchSessionUpdateResponse = Record<string, never>;
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { FuzzyFileSearchResult } from "./FuzzyFileSearchResult";
|
||||
|
||||
export type FuzzyFileSearchSessionUpdatedNotification = { sessionId: string, query: string, files: Array<FuzzyFileSearchResult>, };
|
||||
5
src/generated/app-server/GetAuthStatusParams.ts
Normal file
5
src/generated/app-server/GetAuthStatusParams.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type GetAuthStatusParams = { includeToken: boolean | null, refreshToken: boolean | null, };
|
||||
6
src/generated/app-server/GetAuthStatusResponse.ts
Normal file
6
src/generated/app-server/GetAuthStatusResponse.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AuthMode } from "./AuthMode";
|
||||
|
||||
export type GetAuthStatusResponse = { authMethod: AuthMode | null, authToken: string | null, requiresOpenaiAuth: boolean | null, };
|
||||
6
src/generated/app-server/GetConversationSummaryParams.ts
Normal file
6
src/generated/app-server/GetConversationSummaryParams.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ThreadId } from "./ThreadId";
|
||||
|
||||
export type GetConversationSummaryParams = { rolloutPath: string, } | { conversationId: ThreadId, };
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ConversationSummary } from "./ConversationSummary";
|
||||
|
||||
export type GetConversationSummaryResponse = { summary: ConversationSummary, };
|
||||
5
src/generated/app-server/GitDiffToRemoteParams.ts
Normal file
5
src/generated/app-server/GitDiffToRemoteParams.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type GitDiffToRemoteParams = { cwd: string, };
|
||||
6
src/generated/app-server/GitDiffToRemoteResponse.ts
Normal file
6
src/generated/app-server/GitDiffToRemoteResponse.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { GitSha } from "./GitSha";
|
||||
|
||||
export type GitDiffToRemoteResponse = { sha: GitSha, diff: string, };
|
||||
5
src/generated/app-server/GitSha.ts
Normal file
5
src/generated/app-server/GitSha.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type GitSha = string;
|
||||
5
src/generated/app-server/ImageDetail.ts
Normal file
5
src/generated/app-server/ImageDetail.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ImageDetail = "auto" | "low" | "high" | "original";
|
||||
17
src/generated/app-server/InitializeCapabilities.ts
Normal file
17
src/generated/app-server/InitializeCapabilities.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Client-declared capabilities negotiated during initialize.
|
||||
*/
|
||||
export type InitializeCapabilities = {
|
||||
/**
|
||||
* Opt into receiving experimental API methods and fields.
|
||||
*/
|
||||
experimentalApi: boolean,
|
||||
/**
|
||||
* Exact notification method names that should be suppressed for this
|
||||
* connection (for example `thread/started`).
|
||||
*/
|
||||
optOutNotificationMethods?: Array<string> | null, };
|
||||
7
src/generated/app-server/InitializeParams.ts
Normal file
7
src/generated/app-server/InitializeParams.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ClientInfo } from "./ClientInfo";
|
||||
import type { InitializeCapabilities } from "./InitializeCapabilities";
|
||||
|
||||
export type InitializeParams = { clientInfo: ClientInfo, capabilities: InitializeCapabilities | null, };
|
||||
20
src/generated/app-server/InitializeResponse.ts
Normal file
20
src/generated/app-server/InitializeResponse.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AbsolutePathBuf } from "./AbsolutePathBuf";
|
||||
|
||||
export type InitializeResponse = { userAgent: string,
|
||||
/**
|
||||
* Absolute path to the server's $CODEX_HOME directory.
|
||||
*/
|
||||
codexHome: AbsolutePathBuf,
|
||||
/**
|
||||
* Platform family for the running app-server target, for example
|
||||
* `"unix"` or `"windows"`.
|
||||
*/
|
||||
platformFamily: string,
|
||||
/**
|
||||
* Operating system for the running app-server target, for example
|
||||
* `"macos"`, `"linux"`, or `"windows"`.
|
||||
*/
|
||||
platformOs: string, };
|
||||
8
src/generated/app-server/InputModality.ts
Normal file
8
src/generated/app-server/InputModality.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Canonical user-input modality tags advertised by a model.
|
||||
*/
|
||||
export type InputModality = "text" | "image";
|
||||
5
src/generated/app-server/InternalSessionSource.ts
Normal file
5
src/generated/app-server/InternalSessionSource.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type InternalSessionSource = "memory_consolidation";
|
||||
6
src/generated/app-server/LocalShellAction.ts
Normal file
6
src/generated/app-server/LocalShellAction.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { LocalShellExecAction } from "./LocalShellExecAction";
|
||||
|
||||
export type LocalShellAction = { "type": "exec" } & LocalShellExecAction;
|
||||
5
src/generated/app-server/LocalShellExecAction.ts
Normal file
5
src/generated/app-server/LocalShellExecAction.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type LocalShellExecAction = { command: Array<string>, timeout_ms: bigint | null, working_directory: string | null, env: { [key in string]?: string } | null, user: string | null, };
|
||||
5
src/generated/app-server/LocalShellStatus.ts
Normal file
5
src/generated/app-server/LocalShellStatus.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type LocalShellStatus = "completed" | "in_progress" | "incomplete";
|
||||
11
src/generated/app-server/MessagePhase.ts
Normal file
11
src/generated/app-server/MessagePhase.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Classifies an assistant message as interim commentary or final answer text.
|
||||
*
|
||||
* Providers do not emit this consistently, so callers must treat `None` as
|
||||
* "phase unknown" and keep compatibility behavior for legacy models.
|
||||
*/
|
||||
export type MessagePhase = "commentary" | "final_answer";
|
||||
8
src/generated/app-server/ModeKind.ts
Normal file
8
src/generated/app-server/ModeKind.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Initial collaboration mode to use when the TUI starts.
|
||||
*/
|
||||
export type ModeKind = "plan" | "default";
|
||||
6
src/generated/app-server/NetworkPolicyAmendment.ts
Normal file
6
src/generated/app-server/NetworkPolicyAmendment.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { NetworkPolicyRuleAction } from "./NetworkPolicyRuleAction";
|
||||
|
||||
export type NetworkPolicyAmendment = { host: string, action: NetworkPolicyRuleAction, };
|
||||
5
src/generated/app-server/NetworkPolicyRuleAction.ts
Normal file
5
src/generated/app-server/NetworkPolicyRuleAction.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type NetworkPolicyRuleAction = "allow" | "deny";
|
||||
12
src/generated/app-server/ParsedCommand.ts
Normal file
12
src/generated/app-server/ParsedCommand.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ParsedCommand = { "type": "read", cmd: string, name: string,
|
||||
/**
|
||||
* (Best effort) Path to the file being read by the command. When
|
||||
* possible, this is an absolute path, though when relative, it should
|
||||
* be resolved against the `cwd`` that will be used to run the command
|
||||
* to derive the absolute path.
|
||||
*/
|
||||
path: string, } | { "type": "list_files", cmd: string, path: string | null, } | { "type": "search", cmd: string, query: string | null, path: string | null, } | { "type": "unknown", cmd: string, };
|
||||
5
src/generated/app-server/Personality.ts
Normal file
5
src/generated/app-server/Personality.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type Personality = "none" | "friendly" | "pragmatic";
|
||||
5
src/generated/app-server/PlanType.ts
Normal file
5
src/generated/app-server/PlanType.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type PlanType = "free" | "go" | "plus" | "pro" | "prolite" | "team" | "self_serve_business_usage_based" | "business" | "enterprise_cbp_usage_based" | "enterprise" | "edu" | "unknown";
|
||||
5
src/generated/app-server/RealtimeConversationVersion.ts
Normal file
5
src/generated/app-server/RealtimeConversationVersion.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type RealtimeConversationVersion = "v1" | "v2";
|
||||
5
src/generated/app-server/RealtimeOutputModality.ts
Normal file
5
src/generated/app-server/RealtimeOutputModality.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type RealtimeOutputModality = "text" | "audio";
|
||||
5
src/generated/app-server/RealtimeVoice.ts
Normal file
5
src/generated/app-server/RealtimeVoice.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type RealtimeVoice = "alloy" | "arbor" | "ash" | "ballad" | "breeze" | "cedar" | "coral" | "cove" | "echo" | "ember" | "juniper" | "maple" | "marin" | "sage" | "shimmer" | "sol" | "spruce" | "vale" | "verse";
|
||||
6
src/generated/app-server/RealtimeVoicesList.ts
Normal file
6
src/generated/app-server/RealtimeVoicesList.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { RealtimeVoice } from "./RealtimeVoice";
|
||||
|
||||
export type RealtimeVoicesList = { v1: Array<RealtimeVoice>, v2: Array<RealtimeVoice>, defaultV1: RealtimeVoice, defaultV2: RealtimeVoice, };
|
||||
8
src/generated/app-server/ReasoningEffort.ts
Normal file
8
src/generated/app-server/ReasoningEffort.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning
|
||||
*/
|
||||
export type ReasoningEffort = "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
||||
5
src/generated/app-server/ReasoningItemContent.ts
Normal file
5
src/generated/app-server/ReasoningItemContent.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ReasoningItemContent = { "type": "reasoning_text", text: string, } | { "type": "text", text: string, };
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ReasoningItemReasoningSummary = { "type": "summary_text", text: string, };
|
||||
10
src/generated/app-server/ReasoningSummary.ts
Normal file
10
src/generated/app-server/ReasoningSummary.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* A summary of the reasoning performed by the model. This can be useful for
|
||||
* debugging and understanding the model's reasoning process.
|
||||
* See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#reasoning-summaries
|
||||
*/
|
||||
export type ReasoningSummary = "auto" | "concise" | "detailed" | "none";
|
||||
5
src/generated/app-server/RequestId.ts
Normal file
5
src/generated/app-server/RequestId.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type RequestId = string | number;
|
||||
9
src/generated/app-server/Resource.ts
Normal file
9
src/generated/app-server/Resource.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { JsonValue } from "./serde_json/JsonValue";
|
||||
|
||||
/**
|
||||
* A known resource that the server is capable of reading.
|
||||
*/
|
||||
export type Resource = { annotations?: JsonValue, description?: string, mimeType?: string, name: string, size?: number, title?: string, uri: string, icons?: Array<JsonValue>, _meta?: JsonValue, };
|
||||
17
src/generated/app-server/ResourceContent.ts
Normal file
17
src/generated/app-server/ResourceContent.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { JsonValue } from "./serde_json/JsonValue";
|
||||
|
||||
/**
|
||||
* Contents returned when reading a resource from an MCP server.
|
||||
*/
|
||||
export type ResourceContent = {
|
||||
/**
|
||||
* The URI of this resource.
|
||||
*/
|
||||
uri: string, mimeType?: string, text: string, _meta?: JsonValue, } | {
|
||||
/**
|
||||
* The URI of this resource.
|
||||
*/
|
||||
uri: string, mimeType?: string, blob: string, _meta?: JsonValue, };
|
||||
9
src/generated/app-server/ResourceTemplate.ts
Normal file
9
src/generated/app-server/ResourceTemplate.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { JsonValue } from "./serde_json/JsonValue";
|
||||
|
||||
/**
|
||||
* A template description for resources available on the server.
|
||||
*/
|
||||
export type ResourceTemplate = { annotations?: JsonValue, uriTemplate: string, name: string, title?: string, description?: string, mimeType?: string, };
|
||||
17
src/generated/app-server/ResponseItem.ts
Normal file
17
src/generated/app-server/ResponseItem.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ContentItem } from "./ContentItem";
|
||||
import type { FunctionCallOutputBody } from "./FunctionCallOutputBody";
|
||||
import type { LocalShellAction } from "./LocalShellAction";
|
||||
import type { LocalShellStatus } from "./LocalShellStatus";
|
||||
import type { MessagePhase } from "./MessagePhase";
|
||||
import type { ReasoningItemContent } from "./ReasoningItemContent";
|
||||
import type { ReasoningItemReasoningSummary } from "./ReasoningItemReasoningSummary";
|
||||
import type { WebSearchAction } from "./WebSearchAction";
|
||||
|
||||
export type ResponseItem = { "type": "message", role: string, content: Array<ContentItem>, phase?: MessagePhase, } | { "type": "reasoning", summary: Array<ReasoningItemReasoningSummary>, content?: Array<ReasoningItemContent>, encrypted_content: string | null, } | { "type": "local_shell_call",
|
||||
/**
|
||||
* Set when using the Responses API.
|
||||
*/
|
||||
call_id: string | null, status: LocalShellStatus, action: LocalShellAction, } | { "type": "function_call", name: string, namespace?: string, arguments: string, call_id: string, } | { "type": "tool_search_call", call_id: string | null, status?: string, execution: string, arguments: unknown, } | { "type": "function_call_output", call_id: string, output: FunctionCallOutputBody, } | { "type": "custom_tool_call", status?: string, call_id: string, name: string, input: string, } | { "type": "custom_tool_call_output", call_id: string, name?: string, output: FunctionCallOutputBody, } | { "type": "tool_search_output", call_id: string | null, status: string, execution: string, tools: unknown[], } | { "type": "web_search_call", status?: string, action?: WebSearchAction, } | { "type": "image_generation_call", id: string, status: string, revised_prompt?: string, result: string, } | { "type": "compaction", encrypted_content: string, } | { "type": "context_compaction", encrypted_content?: string, } | { "type": "other" };
|
||||
10
src/generated/app-server/ReviewDecision.ts
Normal file
10
src/generated/app-server/ReviewDecision.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ExecPolicyAmendment } from "./ExecPolicyAmendment";
|
||||
import type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment";
|
||||
|
||||
/**
|
||||
* User's decision in response to an ExecApprovalRequest.
|
||||
*/
|
||||
export type ReviewDecision = "approved" | { "approved_execpolicy_amendment": { proposed_execpolicy_amendment: ExecPolicyAmendment, } } | "approved_for_session" | { "network_policy_amendment": { network_policy_amendment: NetworkPolicyAmendment, } } | "denied" | "timed_out" | "abort";
|
||||
72
src/generated/app-server/ServerNotification.ts
Normal file
72
src/generated/app-server/ServerNotification.ts
Normal file
File diff suppressed because one or more lines are too long
18
src/generated/app-server/ServerRequest.ts
Normal file
18
src/generated/app-server/ServerRequest.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ApplyPatchApprovalParams } from "./ApplyPatchApprovalParams";
|
||||
import type { ExecCommandApprovalParams } from "./ExecCommandApprovalParams";
|
||||
import type { RequestId } from "./RequestId";
|
||||
import type { ChatgptAuthTokensRefreshParams } from "./v2/ChatgptAuthTokensRefreshParams";
|
||||
import type { CommandExecutionRequestApprovalParams } from "./v2/CommandExecutionRequestApprovalParams";
|
||||
import type { DynamicToolCallParams } from "./v2/DynamicToolCallParams";
|
||||
import type { FileChangeRequestApprovalParams } from "./v2/FileChangeRequestApprovalParams";
|
||||
import type { McpServerElicitationRequestParams } from "./v2/McpServerElicitationRequestParams";
|
||||
import type { PermissionsRequestApprovalParams } from "./v2/PermissionsRequestApprovalParams";
|
||||
import type { ToolRequestUserInputParams } from "./v2/ToolRequestUserInputParams";
|
||||
|
||||
/**
|
||||
* Request initiated from the server and sent to the client.
|
||||
*/
|
||||
export type ServerRequest = { "method": "item/commandExecution/requestApproval", id: RequestId, params: CommandExecutionRequestApprovalParams, } | { "method": "item/fileChange/requestApproval", id: RequestId, params: FileChangeRequestApprovalParams, } | { "method": "item/tool/requestUserInput", id: RequestId, params: ToolRequestUserInputParams, } | { "method": "mcpServer/elicitation/request", id: RequestId, params: McpServerElicitationRequestParams, } | { "method": "item/permissions/requestApproval", id: RequestId, params: PermissionsRequestApprovalParams, } | { "method": "item/tool/call", id: RequestId, params: DynamicToolCallParams, } | { "method": "account/chatgptAuthTokens/refresh", id: RequestId, params: ChatgptAuthTokensRefreshParams, } | { "method": "applyPatchApproval", id: RequestId, params: ApplyPatchApprovalParams, } | { "method": "execCommandApproval", id: RequestId, params: ExecCommandApprovalParams, };
|
||||
7
src/generated/app-server/SessionSource.ts
Normal file
7
src/generated/app-server/SessionSource.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { InternalSessionSource } from "./InternalSessionSource";
|
||||
import type { SubAgentSource } from "./SubAgentSource";
|
||||
|
||||
export type SessionSource = "cli" | "vscode" | "exec" | "mcp" | { "custom": string } | { "internal": InternalSessionSource } | { "subagent": SubAgentSource } | "unknown";
|
||||
9
src/generated/app-server/Settings.ts
Normal file
9
src/generated/app-server/Settings.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ReasoningEffort } from "./ReasoningEffort";
|
||||
|
||||
/**
|
||||
* Settings for a collaboration mode.
|
||||
*/
|
||||
export type Settings = { model: string, reasoning_effort: ReasoningEffort | null, developer_instructions: string | null, };
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue