@oh-my-pi/pi-coding-agent 17.2.11 → 17.2.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/{CHANGELOG-d9xenpn9.md → CHANGELOG-k9ghy5sn.md} +13 -0
- package/dist/cli.js +10795 -10791
- package/dist/types/advisor/runtime.d.ts +1 -1
- package/dist/types/config/custom-models.d.ts +31 -0
- package/dist/types/config/model-config-values.d.ts +19 -0
- package/dist/types/config/model-patch.d.ts +93 -0
- package/dist/types/config/model-provider-discovery.d.ts +51 -0
- package/dist/types/config/model-registry.d.ts +5 -52
- package/dist/types/config/settings.d.ts +5 -3
- package/dist/types/debug/raw-sse-buffer.d.ts +9 -0
- package/dist/types/discovery/agent-plugin-format.d.ts +6 -13
- package/dist/types/discovery/contained-path.d.ts +2 -3
- package/dist/types/eval/executor-base.d.ts +8 -2
- package/dist/types/eval/kernel-session-registry.d.ts +60 -0
- package/dist/types/export/share.d.ts +1 -1
- package/dist/types/lsp/diagnostics.d.ts +96 -0
- package/dist/types/lsp/index.d.ts +7 -128
- package/dist/types/lsp/servers.d.ts +72 -0
- package/dist/types/lsp/tool.d.ts +42 -0
- package/dist/types/lsp/workspace-diagnostics.d.ts +12 -0
- package/dist/types/lsp/writethrough.d.ts +33 -0
- package/dist/types/mcp/transports/header-policy.d.ts +2 -1
- package/dist/types/modes/components/agent-hub-projection.d.ts +2 -7
- package/dist/types/modes/components/agent-hub-renderer.d.ts +0 -7
- package/dist/types/modes/theme/color.d.ts +19 -0
- package/dist/types/modes/theme/loader.d.ts +19 -0
- package/dist/types/modes/theme/schema.d.ts +175 -0
- package/dist/types/modes/theme/symbols.d.ts +28 -0
- package/dist/types/modes/theme/theme-class.d.ts +244 -0
- package/dist/types/modes/theme/theme.d.ts +9 -280
- package/dist/types/modes/theme/tui-adapters.d.ts +13 -0
- package/dist/types/registry/agent-registry.d.ts +1 -3
- package/dist/types/secrets/index.d.ts +3 -1
- package/dist/types/secrets/message-transform.d.ts +40 -0
- package/dist/types/secrets/obfuscator.d.ts +0 -108
- package/dist/types/secrets/placeholder-scan.d.ts +95 -0
- package/dist/types/secrets/placeholder.d.ts +94 -0
- package/dist/types/secrets/replacement.d.ts +82 -0
- package/dist/types/session/agent-session-types.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/session-handoff.d.ts +3 -3
- package/dist/types/session/session-manager.d.ts +32 -0
- package/dist/types/session/session-provider-boundary.d.ts +1 -1
- package/dist/types/session/turn-recovery.d.ts +2 -2
- package/dist/types/slash-commands/builtin-collaboration.d.ts +2 -0
- package/dist/types/slash-commands/builtin-completions.d.ts +36 -0
- package/dist/types/slash-commands/builtin-control.d.ts +2 -0
- package/dist/types/slash-commands/builtin-lifecycle.d.ts +3 -0
- package/dist/types/slash-commands/builtin-marketplace.d.ts +11 -0
- package/dist/types/slash-commands/builtin-modes.d.ts +5 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
- package/dist/types/slash-commands/builtin-session.d.ts +2 -0
- package/dist/types/tools/gh-common.d.ts +69 -0
- package/dist/types/tools/gh-pr-checkout.d.ts +84 -0
- package/dist/types/tools/gh-pr-diff.d.ts +102 -0
- package/dist/types/tools/gh-run-watch.d.ts +78 -0
- package/dist/types/tools/gh-search.d.ts +61 -0
- package/dist/types/tools/gh-types.d.ts +340 -0
- package/dist/types/tools/gh-view.d.ts +88 -0
- package/dist/types/tools/gh.d.ts +4 -201
- package/dist/types/tools/read-archive.d.ts +16 -0
- package/dist/types/tools/read-format.d.ts +104 -0
- package/dist/types/tools/read-path-resolution.d.ts +19 -0
- package/dist/types/tools/read-pdf-images.d.ts +12 -0
- package/dist/types/tools/read-renderer.d.ts +24 -0
- package/dist/types/tools/read-selector.d.ts +27 -0
- package/dist/types/tools/read-sqlite.d.ts +16 -0
- package/dist/types/tools/read-summary.d.ts +19 -0
- package/dist/types/tools/read.d.ts +1 -23
- package/dist/types/tools/shell-tokenize.d.ts +2 -1
- package/dist/types/utils/changelog.d.ts +0 -5
- package/package.json +13 -13
- package/src/advisor/runtime.ts +12 -7
- package/src/config/custom-models.ts +188 -0
- package/src/config/model-config-values.ts +128 -0
- package/src/config/model-patch.ts +252 -0
- package/src/config/model-provider-discovery.ts +132 -0
- package/src/config/model-registry.ts +64 -704
- package/src/config/settings.ts +7 -3
- package/src/debug/raw-sse-buffer.ts +20 -0
- package/src/discovery/agent-plugin-format.ts +7 -7
- package/src/discovery/contained-path.ts +2 -5
- package/src/edit/hashline/diff.ts +5 -1
- package/src/eval/executor-base.ts +39 -6
- package/src/eval/jl/executor.ts +82 -371
- package/src/eval/kernel-session-registry.ts +398 -0
- package/src/eval/py/executor.ts +53 -261
- package/src/eval/rb/executor.ts +36 -279
- package/src/export/share.ts +2 -1
- package/src/lsp/diagnostics.ts +516 -0
- package/src/lsp/index.ts +20 -2819
- package/src/lsp/servers.ts +296 -0
- package/src/lsp/tool.ts +1352 -0
- package/src/lsp/workspace-diagnostics.ts +170 -0
- package/src/lsp/writethrough.ts +561 -0
- package/src/mcp/transports/header-policy.ts +1 -1
- package/src/modes/components/agent-hub-projection.ts +2 -2
- package/src/modes/components/agent-hub-renderer.ts +3 -7
- package/src/modes/controllers/command-controller.ts +4 -1
- package/src/modes/theme/color.ts +133 -0
- package/src/modes/theme/loader.ts +178 -0
- package/src/modes/theme/schema.ts +263 -0
- package/src/modes/theme/symbols.ts +1000 -0
- package/src/modes/theme/theme-class.ts +611 -0
- package/src/modes/theme/theme.ts +27 -2453
- package/src/modes/theme/tui-adapters.ts +279 -0
- package/src/registry/agent-registry.ts +2 -2
- package/src/secrets/index.ts +6 -12
- package/src/secrets/message-transform.ts +287 -0
- package/src/secrets/obfuscator.ts +39 -1303
- package/src/secrets/placeholder-scan.ts +506 -0
- package/src/secrets/placeholder.ts +309 -0
- package/src/secrets/replacement.ts +216 -0
- package/src/session/agent-session-types.ts +6 -0
- package/src/session/agent-session.ts +113 -7
- package/src/session/indexed-session-storage.ts +7 -2
- package/src/session/session-advisors.ts +32 -34
- package/src/session/session-handoff.ts +39 -20
- package/src/session/session-manager.ts +67 -3
- package/src/session/session-provider-boundary.ts +3 -6
- package/src/session/turn-recovery.ts +21 -12
- package/src/slash-commands/builtin-collaboration.ts +504 -0
- package/src/slash-commands/builtin-completions.ts +291 -0
- package/src/slash-commands/builtin-control.ts +78 -0
- package/src/slash-commands/builtin-lifecycle.ts +506 -0
- package/src/slash-commands/builtin-marketplace.ts +567 -0
- package/src/slash-commands/builtin-modes.ts +518 -0
- package/src/slash-commands/builtin-registry.ts +21 -3000
- package/src/slash-commands/builtin-session.ts +592 -0
- package/src/task/executor.ts +17 -14
- package/src/tools/gh-common.ts +288 -0
- package/src/tools/gh-pr-checkout.ts +679 -0
- package/src/tools/gh-pr-diff.ts +473 -0
- package/src/tools/gh-run-watch.ts +1015 -0
- package/src/tools/gh-search.ts +500 -0
- package/src/tools/gh-types.ts +379 -0
- package/src/tools/gh-view.ts +612 -0
- package/src/tools/gh.ts +109 -3821
- package/src/tools/read-archive.ts +209 -0
- package/src/tools/read-format.ts +593 -0
- package/src/tools/read-path-resolution.ts +36 -0
- package/src/tools/read-pdf-images.ts +250 -0
- package/src/tools/read-renderer.ts +289 -0
- package/src/tools/read-selector.ts +84 -0
- package/src/tools/read-sqlite.ts +215 -0
- package/src/tools/read-summary.ts +199 -0
- package/src/tools/read.ts +77 -1820
- package/src/tools/shell-tokenize.ts +1 -1
- package/src/utils/changelog.ts +1 -1
- package/src/utils/title-generator.ts +3 -1
- package/src/web/search/providers/zai.ts +12 -3
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
import { Spacer } from "@oh-my-pi/pi-tui";
|
|
2
|
+
import { APP_NAME } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import { CollabGuestLink } from "../collab/guest";
|
|
4
|
+
import { CollabHost } from "../collab/host";
|
|
5
|
+
import type { SettingPath, SettingValue } from "../config/settings";
|
|
6
|
+
import { settings } from "../config/settings";
|
|
7
|
+
import { parseExportArgs } from "../export/html/args";
|
|
8
|
+
import { shareSession } from "../export/share";
|
|
9
|
+
import { theme } from "../modes/theme/theme";
|
|
10
|
+
import type { InteractiveModeContext } from "../modes/types";
|
|
11
|
+
import { extractLastCodeBlock, extractLastCommand } from "../modes/utils/copy-targets";
|
|
12
|
+
import { urlHyperlinkAlways } from "../tui";
|
|
13
|
+
import { copyToClipboard } from "../utils/clipboard";
|
|
14
|
+
import { refreshStatusLine } from "./builtin-modes";
|
|
15
|
+
import { CollabQrCodeComponent } from "./helpers/collab-qrcode";
|
|
16
|
+
import { commandConsumed, errorMessage, parseSubcommand, usage } from "./helpers/parse";
|
|
17
|
+
import type { SlashCommandSpec } from "./types";
|
|
18
|
+
|
|
19
|
+
/** Scheme-less display form of a browser deep link: accent + underline, OSC-8 linked to the full URL. */
|
|
20
|
+
function collabWebLinkClickable(webLink: string): string {
|
|
21
|
+
const display = theme.fg("accent", `\x1b[4m${webLink.replace(/^https?:\/\//, "")}\x1b[24m`);
|
|
22
|
+
return urlHyperlinkAlways(webLink, display);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Join hint printed by /collab: compact terminal link + clickable browser deep link. */
|
|
26
|
+
function collabLinkHint(host: CollabHost, heading: string, view = false): string {
|
|
27
|
+
const bullet = theme.fg("accent", theme.format.bullet);
|
|
28
|
+
const link = view ? host.viewLink : host.link;
|
|
29
|
+
const webLink = view ? host.webViewLink : host.webLink;
|
|
30
|
+
return [
|
|
31
|
+
theme.fg("success", heading),
|
|
32
|
+
` ${bullet} ${theme.fg("muted", view ? "Watch from another terminal:" : "Join from another terminal:")} ${APP_NAME} join "${link}"`,
|
|
33
|
+
` ${bullet} ${theme.fg("muted", "or any web browser:")} ${collabWebLinkClickable(webLink)}`,
|
|
34
|
+
theme.fg(
|
|
35
|
+
"dim",
|
|
36
|
+
view
|
|
37
|
+
? "Anyone with this link can watch the session but cannot prompt the agent."
|
|
38
|
+
: "Anyone with the link can read the session and prompt the agent. Read-only link: /collab view",
|
|
39
|
+
),
|
|
40
|
+
].join("\n");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function showCollabQrCode(ctx: InteractiveModeContext, webLink: string): void {
|
|
44
|
+
try {
|
|
45
|
+
ctx.present([new Spacer(1), new CollabQrCodeComponent(webLink)]);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
ctx.showError(`Failed to render collab QR code: ${errorMessage(err)}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function showCollabLink(ctx: InteractiveModeContext, host: CollabHost, heading: string, view = false): void {
|
|
52
|
+
ctx.showStatus(collabLinkHint(host, heading, view), { dim: false });
|
|
53
|
+
showCollabQrCode(ctx, view ? host.webViewLink : host.webLink);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const BUILTIN_COLLABORATION_SLASH_COMMANDS: ReadonlyArray<SlashCommandSpec> = [
|
|
57
|
+
{
|
|
58
|
+
name: "advisor",
|
|
59
|
+
description: "Toggle the advisor (a second model that reviews each turn and injects notes)",
|
|
60
|
+
acpDescription: "Toggle advisor",
|
|
61
|
+
acpInputHint: "[on|off|status|dump [raw]|configure]",
|
|
62
|
+
subcommands: [
|
|
63
|
+
{ name: "on", description: "Enable the advisor" },
|
|
64
|
+
{ name: "off", description: "Disable the advisor" },
|
|
65
|
+
{ name: "status", description: "Show advisor status" },
|
|
66
|
+
{ name: "dump", description: "Copy the advisor's transcript to clipboard", usage: "[raw]" },
|
|
67
|
+
{ name: "configure", description: "Open the advisor configuration editor (TUI)" },
|
|
68
|
+
],
|
|
69
|
+
allowArgs: true,
|
|
70
|
+
getTuiAutocompleteDescription: runtime => {
|
|
71
|
+
const stats = runtime.ctx.session.getAdvisorStats();
|
|
72
|
+
if (stats.active && stats.advisors.length > 1) return `Advisor: on (${stats.advisors.length} advisors)`;
|
|
73
|
+
if (stats.active && stats.model) return `Advisor: on (${stats.model.provider}/${stats.model.id})`;
|
|
74
|
+
if (stats.configured) return "Advisor: configured, no model";
|
|
75
|
+
return "Advisor: off";
|
|
76
|
+
},
|
|
77
|
+
handle: async (command, runtime) => {
|
|
78
|
+
const { verb, rest } = parseSubcommand(command.args);
|
|
79
|
+
if (!verb || verb === "toggle") {
|
|
80
|
+
const active = runtime.session.toggleAdvisorEnabled();
|
|
81
|
+
const configured = runtime.session.isAdvisorEnabled();
|
|
82
|
+
if (active) {
|
|
83
|
+
await runtime.output("Advisor enabled.");
|
|
84
|
+
} else if (configured) {
|
|
85
|
+
await runtime.output("Advisor setting enabled, but no model is assigned to the 'advisor' role.");
|
|
86
|
+
} else {
|
|
87
|
+
await runtime.output("Advisor disabled.");
|
|
88
|
+
}
|
|
89
|
+
return commandConsumed();
|
|
90
|
+
}
|
|
91
|
+
if (verb === "on") {
|
|
92
|
+
const active = runtime.session.setAdvisorEnabled(true);
|
|
93
|
+
await runtime.output(
|
|
94
|
+
active ? "Advisor enabled." : "Advisor setting enabled, but no model is assigned to the 'advisor' role.",
|
|
95
|
+
);
|
|
96
|
+
return commandConsumed();
|
|
97
|
+
}
|
|
98
|
+
if (verb === "off") {
|
|
99
|
+
runtime.session.setAdvisorEnabled(false);
|
|
100
|
+
await runtime.output("Advisor disabled.");
|
|
101
|
+
return commandConsumed();
|
|
102
|
+
}
|
|
103
|
+
if (verb === "status") {
|
|
104
|
+
await runtime.output(runtime.session.formatAdvisorStatus());
|
|
105
|
+
return commandConsumed();
|
|
106
|
+
}
|
|
107
|
+
if (verb === "dump") {
|
|
108
|
+
const isRaw = rest.toLowerCase() === "raw";
|
|
109
|
+
const text = runtime.session.formatAdvisorHistoryAsText({ compact: !isRaw });
|
|
110
|
+
await runtime.output(text ?? "Advisor is not active for this session.");
|
|
111
|
+
return commandConsumed();
|
|
112
|
+
}
|
|
113
|
+
if (verb === "configure") {
|
|
114
|
+
await runtime.output(
|
|
115
|
+
"/advisor configure opens an interactive editor and is only available in the interactive TUI.",
|
|
116
|
+
);
|
|
117
|
+
return commandConsumed();
|
|
118
|
+
}
|
|
119
|
+
return usage("Usage: /advisor [on|off|status|dump [raw]|configure]", runtime);
|
|
120
|
+
},
|
|
121
|
+
handleTui: async (command, runtime) => {
|
|
122
|
+
const { verb, rest } = parseSubcommand(command.args);
|
|
123
|
+
if (!verb || verb === "toggle") {
|
|
124
|
+
const active = runtime.ctx.session.toggleAdvisorEnabled();
|
|
125
|
+
const configured = runtime.ctx.session.isAdvisorEnabled();
|
|
126
|
+
if (active) {
|
|
127
|
+
runtime.ctx.showStatus("Advisor enabled.");
|
|
128
|
+
} else if (configured) {
|
|
129
|
+
runtime.ctx.showStatus("Advisor setting enabled, but no model is assigned to the 'advisor' role.");
|
|
130
|
+
} else {
|
|
131
|
+
runtime.ctx.showStatus("Advisor disabled.");
|
|
132
|
+
}
|
|
133
|
+
refreshStatusLine(runtime.ctx);
|
|
134
|
+
runtime.ctx.editor.setText("");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (verb === "on") {
|
|
138
|
+
const active = runtime.ctx.session.setAdvisorEnabled(true);
|
|
139
|
+
runtime.ctx.showStatus(
|
|
140
|
+
active ? "Advisor enabled." : "Advisor setting enabled, but no model is assigned to the 'advisor' role.",
|
|
141
|
+
);
|
|
142
|
+
refreshStatusLine(runtime.ctx);
|
|
143
|
+
runtime.ctx.editor.setText("");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (verb === "off") {
|
|
147
|
+
runtime.ctx.session.setAdvisorEnabled(false);
|
|
148
|
+
runtime.ctx.showStatus("Advisor disabled.");
|
|
149
|
+
refreshStatusLine(runtime.ctx);
|
|
150
|
+
runtime.ctx.editor.setText("");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (verb === "status") {
|
|
154
|
+
await runtime.ctx.handleAdvisorStatusCommand();
|
|
155
|
+
runtime.ctx.editor.setText("");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (verb === "dump") {
|
|
159
|
+
const isRaw = rest.toLowerCase() === "raw";
|
|
160
|
+
runtime.ctx.handleAdvisorDumpCommand(isRaw);
|
|
161
|
+
runtime.ctx.editor.setText("");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (verb === "configure") {
|
|
165
|
+
runtime.ctx.showAdvisorConfigure();
|
|
166
|
+
runtime.ctx.editor.setText("");
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
runtime.ctx.showStatus("Usage: /advisor [on|off|status|dump [raw]|configure]");
|
|
170
|
+
runtime.ctx.editor.setText("");
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: "export",
|
|
175
|
+
description: "Export session to HTML file",
|
|
176
|
+
inlineHint: "[--themes] [path]",
|
|
177
|
+
allowArgs: true,
|
|
178
|
+
handle: async (command, runtime) => {
|
|
179
|
+
try {
|
|
180
|
+
const { outputPath, useUserThemes } = parseExportArgs(command.args);
|
|
181
|
+
if (outputPath === "--copy" || outputPath === "clipboard" || outputPath === "copy") {
|
|
182
|
+
return usage("Use /dump to copy the session to clipboard.", runtime);
|
|
183
|
+
}
|
|
184
|
+
const filePath = await runtime.session.exportToHtml(outputPath, useUserThemes);
|
|
185
|
+
await runtime.output(`Session exported to: ${filePath}`);
|
|
186
|
+
return commandConsumed();
|
|
187
|
+
} catch (err) {
|
|
188
|
+
return usage(`Failed to export session: ${errorMessage(err)}`, runtime);
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
handleTui: async (command, runtime) => {
|
|
192
|
+
await runtime.ctx.handleExportCommand(command.text);
|
|
193
|
+
runtime.ctx.editor.setText("");
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: "dump",
|
|
198
|
+
description: "Copy session transcript to clipboard (and write LLM request JSON to tmp)",
|
|
199
|
+
acpDescription: "Return full transcript as plain text, with LLM request JSON path",
|
|
200
|
+
allowArgs: true,
|
|
201
|
+
handle: async (_command, runtime) => {
|
|
202
|
+
const text = runtime.session.formatSessionAsText();
|
|
203
|
+
if (!text) {
|
|
204
|
+
await runtime.output("No messages to dump yet.");
|
|
205
|
+
return commandConsumed();
|
|
206
|
+
}
|
|
207
|
+
let sidecarPath: string | undefined;
|
|
208
|
+
try {
|
|
209
|
+
sidecarPath = await runtime.session.dumpLlmRequestToTmpDir();
|
|
210
|
+
} catch {
|
|
211
|
+
// Sidecar is best-effort; the transcript is still output below.
|
|
212
|
+
}
|
|
213
|
+
const lines = [text];
|
|
214
|
+
if (sidecarPath)
|
|
215
|
+
lines.push(
|
|
216
|
+
"",
|
|
217
|
+
`LLM request JSON: ${sidecarPath}`,
|
|
218
|
+
"This file persists on disk and may contain raw context/secrets — treat accordingly.",
|
|
219
|
+
);
|
|
220
|
+
await runtime.output(lines.join("\n"));
|
|
221
|
+
return commandConsumed();
|
|
222
|
+
},
|
|
223
|
+
handleTui: async (_command, runtime) => {
|
|
224
|
+
await runtime.ctx.handleDumpCommand();
|
|
225
|
+
runtime.ctx.editor.setText("");
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
name: "share",
|
|
230
|
+
description: "Share session via an encrypted link (share server or secret gist)",
|
|
231
|
+
handle: async (_command, runtime) => {
|
|
232
|
+
try {
|
|
233
|
+
const result = await shareSession(runtime.sessionManager, {
|
|
234
|
+
serverUrl: runtime.settings.get("share.serverUrl"),
|
|
235
|
+
store: runtime.settings.get("share.store"),
|
|
236
|
+
state: runtime.session.state,
|
|
237
|
+
obfuscator: runtime.settings.get("share.redactSecrets") ? runtime.session.obfuscator : undefined,
|
|
238
|
+
});
|
|
239
|
+
const lines = [`Share URL: ${result.url}`];
|
|
240
|
+
if (result.gistUrl) lines.push(`Gist: ${result.gistUrl}`);
|
|
241
|
+
if (result.truncated) lines.push("Note: large content was trimmed to fit the share size limit.");
|
|
242
|
+
await runtime.output(lines.join("\n"));
|
|
243
|
+
return commandConsumed();
|
|
244
|
+
} catch (err) {
|
|
245
|
+
return usage(`Failed to share session: ${errorMessage(err)}`, runtime);
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
handleTui: async (_command, runtime) => {
|
|
249
|
+
await runtime.ctx.handleShareCommand();
|
|
250
|
+
runtime.ctx.editor.setText("");
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
name: "collab",
|
|
255
|
+
description: "Share this session live via a relay",
|
|
256
|
+
inlineHint: "[start|view|stop|status] [relayUrl]",
|
|
257
|
+
subcommands: [
|
|
258
|
+
{ name: "view", description: "Share a read-only link (guests can watch, not prompt)" },
|
|
259
|
+
{ name: "status", description: "Show link + participants" },
|
|
260
|
+
{ name: "stop", description: "Stop sharing" },
|
|
261
|
+
],
|
|
262
|
+
allowArgs: true,
|
|
263
|
+
getTuiAutocompleteDescription: runtime => {
|
|
264
|
+
if (runtime.ctx.collabHost) {
|
|
265
|
+
return `Collab: hosting (${Math.max(0, runtime.ctx.collabHost.participants.length - 1)} guests)`;
|
|
266
|
+
}
|
|
267
|
+
if (runtime.ctx.collabGuest?.readOnly) return "Collab: read-only guest";
|
|
268
|
+
if (runtime.ctx.collabGuest) return "Collab: guest";
|
|
269
|
+
return "Collab: off";
|
|
270
|
+
},
|
|
271
|
+
handleTui: async (command, runtime) => {
|
|
272
|
+
const ctx = runtime.ctx;
|
|
273
|
+
ctx.editor.setText("");
|
|
274
|
+
const args = command.args.trim();
|
|
275
|
+
const { verb, rest } = parseSubcommand(args);
|
|
276
|
+
if (verb === "stop") {
|
|
277
|
+
if (!ctx.collabHost) {
|
|
278
|
+
ctx.showStatus("Not hosting a collab session");
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
await ctx.collabHost.stop("host stopped");
|
|
282
|
+
ctx.showStatus("Collab stopped");
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (verb === "status") {
|
|
286
|
+
if (ctx.collabHost) {
|
|
287
|
+
const names = ctx.collabHost.participants.map(p =>
|
|
288
|
+
p.role === "host" ? `${p.name} (host)` : p.readOnly ? `${p.name} (view-only)` : p.name,
|
|
289
|
+
);
|
|
290
|
+
ctx.showStatus(`Collab: ${names.join(", ")} — ${collabWebLinkClickable(ctx.collabHost.webLink)}`);
|
|
291
|
+
} else if (ctx.collabGuest) {
|
|
292
|
+
ctx.showStatus(
|
|
293
|
+
ctx.collabGuest.readOnly
|
|
294
|
+
? "In a collab session as a read-only guest (/leave to exit)"
|
|
295
|
+
: "In a collab session as a guest (/leave to exit)",
|
|
296
|
+
);
|
|
297
|
+
} else {
|
|
298
|
+
ctx.showStatus("Not in a collab session");
|
|
299
|
+
}
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (ctx.collabGuest) {
|
|
303
|
+
ctx.showError("Already in a collab session as a guest (/leave first)");
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const knownStartVerb = verb === "start" || verb === "view";
|
|
307
|
+
const view = verb === "view";
|
|
308
|
+
if (ctx.collabHost) {
|
|
309
|
+
showCollabLink(
|
|
310
|
+
ctx,
|
|
311
|
+
ctx.collabHost,
|
|
312
|
+
view ? "Read-only collab session active" : "Collab session active",
|
|
313
|
+
view,
|
|
314
|
+
);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const explicitUrl = knownStartVerb ? rest : args;
|
|
318
|
+
const relayInput = explicitUrl || ctx.settings.get("collab.relayUrl") || "";
|
|
319
|
+
if (!relayInput) {
|
|
320
|
+
ctx.showError(
|
|
321
|
+
"No relay configured. Set collab.relayUrl in /settings or pass one: /collab relay.example.com",
|
|
322
|
+
);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
// Scheme-less relay args default to wss (ws:// must be spelled out for localhost).
|
|
326
|
+
const relayUrl = relayInput.includes("://") ? relayInput : `wss://${relayInput}`;
|
|
327
|
+
const webUrl = ctx.settings.get("collab.webUrl") || "";
|
|
328
|
+
const host = new CollabHost(ctx);
|
|
329
|
+
try {
|
|
330
|
+
await host.start(relayUrl, webUrl);
|
|
331
|
+
} catch (err) {
|
|
332
|
+
ctx.showError(`Failed to start collab session: ${errorMessage(err)}`);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
ctx.collabHost = host;
|
|
336
|
+
showCollabLink(ctx, host, "Collab session started!", view);
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
name: "join",
|
|
341
|
+
description: "Join a shared collab session",
|
|
342
|
+
inlineHint: "<link>",
|
|
343
|
+
allowArgs: true,
|
|
344
|
+
handleTui: async (command, runtime) => {
|
|
345
|
+
const ctx = runtime.ctx;
|
|
346
|
+
ctx.editor.setText("");
|
|
347
|
+
const link = command.args.trim();
|
|
348
|
+
if (!link) {
|
|
349
|
+
ctx.showError("Usage: /join <link>");
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (ctx.collabHost) {
|
|
353
|
+
ctx.showError("Stop hosting first (/collab stop)");
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (ctx.collabGuest) {
|
|
357
|
+
ctx.showError("Already in a collab session (/leave first)");
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
await new CollabGuestLink(ctx).join(link);
|
|
362
|
+
} catch (err) {
|
|
363
|
+
ctx.showError(`Failed to join collab session: ${errorMessage(err)}`);
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
name: "leave",
|
|
369
|
+
description: "Leave the collab session",
|
|
370
|
+
getTuiAutocompleteDescription: runtime => {
|
|
371
|
+
if (runtime.ctx.collabHost) return "Leave collab: hosting";
|
|
372
|
+
if (runtime.ctx.collabGuest) return "Leave collab: guest";
|
|
373
|
+
return "Leave collab: not in collab";
|
|
374
|
+
},
|
|
375
|
+
handleTui: async (_command, runtime) => {
|
|
376
|
+
const ctx = runtime.ctx;
|
|
377
|
+
ctx.editor.setText("");
|
|
378
|
+
if (ctx.collabGuest) {
|
|
379
|
+
await ctx.collabGuest.leave("left");
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (ctx.collabHost) {
|
|
383
|
+
await ctx.collabHost.stop("host stopped");
|
|
384
|
+
ctx.showStatus("Collab stopped");
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
ctx.showStatus("Not in a collab session");
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
name: "browser",
|
|
392
|
+
description: "Toggle browser headless vs visible mode",
|
|
393
|
+
acpInputHint: "[headless|visible]",
|
|
394
|
+
subcommands: [
|
|
395
|
+
{ name: "headless", description: "Switch to headless mode" },
|
|
396
|
+
{ name: "visible", description: "Switch to visible mode" },
|
|
397
|
+
],
|
|
398
|
+
allowArgs: true,
|
|
399
|
+
getTuiAutocompleteDescription: runtime => {
|
|
400
|
+
if (!runtime.ctx.settings.get("browser.enabled" as SettingPath)) return "Browser: disabled";
|
|
401
|
+
return runtime.ctx.settings.get("browser.headless" as SettingPath) ? "Browser: headless" : "Browser: visible";
|
|
402
|
+
},
|
|
403
|
+
handle: async (command, runtime) => {
|
|
404
|
+
const arg = command.args.toLowerCase();
|
|
405
|
+
const enabled = runtime.settings.get("browser.enabled" as SettingPath) as boolean;
|
|
406
|
+
if (!enabled) return usage("Browser tool is disabled (enable in settings).", runtime);
|
|
407
|
+
const current = runtime.settings.get("browser.headless" as SettingPath) as boolean;
|
|
408
|
+
let next = current;
|
|
409
|
+
if (!arg) next = !current;
|
|
410
|
+
else if (arg === "headless" || arg === "hidden") next = true;
|
|
411
|
+
else if (arg === "visible" || arg === "show" || arg === "headful") next = false;
|
|
412
|
+
else return usage("Usage: /browser [headless|visible]", runtime);
|
|
413
|
+
runtime.settings.set("browser.headless" as SettingPath, next as SettingValue<SettingPath>);
|
|
414
|
+
const tool = runtime.session.getToolByName("browser");
|
|
415
|
+
if (tool && "restartForModeChange" in tool) {
|
|
416
|
+
try {
|
|
417
|
+
await (tool as { restartForModeChange: () => Promise<void> }).restartForModeChange();
|
|
418
|
+
} catch (err) {
|
|
419
|
+
// Setting was already mutated; surface the restart failure so the
|
|
420
|
+
// user knows the browser is in an inconsistent state.
|
|
421
|
+
await runtime.output(
|
|
422
|
+
`Browser mode set to ${next ? "headless" : "visible"}, but restart failed: ${errorMessage(err)}`,
|
|
423
|
+
);
|
|
424
|
+
return commandConsumed();
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
await runtime.output(`Browser mode: ${next ? "headless" : "visible"}`);
|
|
428
|
+
return commandConsumed();
|
|
429
|
+
},
|
|
430
|
+
handleTui: async (command, runtime) => {
|
|
431
|
+
const arg = command.args.toLowerCase();
|
|
432
|
+
const current = settings.get("browser.headless" as SettingPath) as boolean;
|
|
433
|
+
let next = current;
|
|
434
|
+
if (!(settings.get("browser.enabled" as SettingPath) as boolean)) {
|
|
435
|
+
runtime.ctx.showWarning("Browser tool is disabled (enable in settings)");
|
|
436
|
+
runtime.ctx.editor.setText("");
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (!arg) {
|
|
440
|
+
next = !current;
|
|
441
|
+
} else if (arg === "headless" || arg === "hidden") {
|
|
442
|
+
next = true;
|
|
443
|
+
} else if (arg === "visible" || arg === "show" || arg === "headful") {
|
|
444
|
+
next = false;
|
|
445
|
+
} else {
|
|
446
|
+
runtime.ctx.showStatus("Usage: /browser [headless|visible]");
|
|
447
|
+
runtime.ctx.editor.setText("");
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
settings.set("browser.headless" as SettingPath, next as SettingValue<SettingPath>);
|
|
451
|
+
const tool = runtime.ctx.session.getToolByName("browser");
|
|
452
|
+
if (tool && "restartForModeChange" in tool) {
|
|
453
|
+
try {
|
|
454
|
+
await (tool as { restartForModeChange: () => Promise<void> }).restartForModeChange();
|
|
455
|
+
} catch (error) {
|
|
456
|
+
runtime.ctx.showWarning(`Failed to restart browser: ${errorMessage(error)}`);
|
|
457
|
+
runtime.ctx.editor.setText("");
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
runtime.ctx.showStatus(`Browser mode: ${next ? "headless" : "visible"}`);
|
|
462
|
+
runtime.ctx.editor.setText("");
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
name: "copy",
|
|
467
|
+
description: "Pick text or code from the conversation to copy",
|
|
468
|
+
allowArgs: true,
|
|
469
|
+
handleTui: async (command, runtime) => {
|
|
470
|
+
const arg = command.args.trim().toLowerCase();
|
|
471
|
+
if (!arg) {
|
|
472
|
+
runtime.ctx.showCopySelector();
|
|
473
|
+
runtime.ctx.editor.setText("");
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (arg === "code") {
|
|
477
|
+
const block = extractLastCodeBlock(runtime.ctx.session.messages);
|
|
478
|
+
if (!block) {
|
|
479
|
+
runtime.ctx.showStatus("No code block to copy.");
|
|
480
|
+
runtime.ctx.editor.setText("");
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
await copyToClipboard(block.code);
|
|
484
|
+
runtime.ctx.showStatus("Copied code block to clipboard");
|
|
485
|
+
runtime.ctx.editor.setText("");
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (arg === "cmd" || arg === "command") {
|
|
489
|
+
const lastCommand = extractLastCommand(runtime.ctx.session.messages);
|
|
490
|
+
if (!lastCommand) {
|
|
491
|
+
runtime.ctx.showStatus("No command to copy.");
|
|
492
|
+
runtime.ctx.editor.setText("");
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
await copyToClipboard(lastCommand.code);
|
|
496
|
+
runtime.ctx.showStatus(`Copied ${lastCommand.kind === "bash" ? "bash command" : "eval code"} to clipboard`);
|
|
497
|
+
runtime.ctx.editor.setText("");
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
runtime.ctx.showStatus("Usage: /copy [code|cmd]");
|
|
501
|
+
runtime.ctx.editor.setText("");
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
];
|