@oh-my-pi/pi-coding-agent 17.1.2 → 17.1.4
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 +63 -0
- package/dist/cli.js +3806 -3798
- package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +1 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +8 -0
- package/dist/types/cli/update-cli.d.ts +41 -0
- package/dist/types/cli/usage-cli.d.ts +4 -2
- package/dist/types/commands/update.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +19 -0
- package/dist/types/config/settings-schema.d.ts +30 -7
- package/dist/types/cursor.d.ts +47 -1
- package/dist/types/eval/js/process-entry.d.ts +2 -1
- package/dist/types/eval/js/worker-core.d.ts +4 -1
- package/dist/types/extensibility/extensions/runner.d.ts +1 -0
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +4 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +4 -0
- package/dist/types/extensibility/legacy-pi-tui-shim.d.ts +15 -7
- package/dist/types/extensibility/shared-events.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +9 -0
- package/dist/types/modes/components/custom-editor.d.ts +12 -8
- package/dist/types/plan-mode/approved-plan.d.ts +3 -2
- package/dist/types/secrets/obfuscator.d.ts +7 -31
- package/dist/types/session/agent-session.d.ts +19 -4
- package/dist/types/session/prewalk.d.ts +2 -1
- package/dist/types/session/session-advisors.d.ts +8 -0
- package/dist/types/session/session-metadata.d.ts +29 -0
- package/dist/types/session/turn-recovery.d.ts +5 -5
- package/dist/types/stt/sherpa-runtime.d.ts +38 -0
- package/dist/types/thinking.d.ts +24 -0
- package/dist/types/tools/auto-generated-guard.d.ts +5 -2
- package/dist/types/tools/bash.d.ts +5 -4
- package/dist/types/tools/computer/exposure.d.ts +8 -0
- package/dist/types/tools/computer/supervisor.d.ts +1 -1
- package/dist/types/tools/computer/worker-entry.d.ts +1 -1
- package/dist/types/tools/computer/worker.d.ts +1 -1
- package/dist/types/tools/computer.d.ts +6 -0
- package/dist/types/tools/memory-render.d.ts +1 -4
- package/dist/types/tools/shell-tokenize.d.ts +14 -0
- package/dist/types/tools/todo.d.ts +7 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +80 -10
- package/src/advisor/runtime.ts +27 -1
- package/src/cli/__tests__/auth-gateway-catalog.test.ts +111 -0
- package/src/cli/auth-gateway-cli.ts +63 -16
- package/src/cli/config-cli.ts +25 -7
- package/src/cli/update-cli.ts +229 -29
- package/src/cli/usage-cli.ts +144 -15
- package/src/cli.ts +21 -15
- package/src/commands/update.ts +6 -0
- package/src/config/__tests__/model-registry.test.ts +42 -7
- package/src/config/model-registry.ts +67 -4
- package/src/config/settings-schema.ts +39 -8
- package/src/config/settings.ts +24 -2
- package/src/cursor.ts +153 -0
- package/src/dap/session.ts +70 -11
- package/src/edit/hashline/filesystem.ts +1 -1
- package/src/edit/modes/patch.ts +1 -1
- package/src/eval/__tests__/js-context-manager.test.ts +9 -1
- package/src/eval/__tests__/process-entry-import.test.ts +111 -1
- package/src/eval/js/process-entry.ts +9 -5
- package/src/eval/js/worker-core.ts +6 -2
- package/src/exec/bash-executor.ts +4 -2
- package/src/extensibility/extensions/runner.ts +23 -8
- package/src/extensibility/legacy-pi-ai-shim.ts +9 -0
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +14 -2
- package/src/extensibility/legacy-pi-tui-shim.ts +33 -0
- package/src/extensibility/shared-events.ts +2 -0
- package/src/main.ts +22 -1
- package/src/modes/acp/acp-agent.ts +6 -0
- package/src/modes/components/assistant-message.ts +88 -11
- package/src/modes/components/chat-transcript-builder.ts +2 -0
- package/src/modes/components/custom-editor.test.ts +170 -0
- package/src/modes/components/custom-editor.ts +79 -29
- package/src/modes/components/settings-defs.ts +5 -1
- package/src/modes/controllers/event-controller.ts +96 -4
- package/src/modes/controllers/selector-controller.ts +14 -0
- package/src/modes/interactive-mode.ts +34 -8
- package/src/modes/utils/context-usage.ts +19 -2
- package/src/modes/utils/interactive-context-helpers.ts +1 -0
- package/src/plan-mode/approved-plan.ts +18 -10
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/system-prompt.md +10 -1
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/sdk.ts +4 -0
- package/src/secrets/obfuscator.ts +36 -126
- package/src/session/agent-session.ts +69 -60
- package/src/session/prewalk.ts +8 -3
- package/src/session/session-advisors.ts +67 -3
- package/src/session/session-metadata.ts +53 -0
- package/src/session/session-provider-boundary.ts +0 -1
- package/src/session/session-tools.ts +35 -1
- package/src/session/stream-guards.ts +1 -1
- package/src/session/turn-recovery.ts +36 -19
- package/src/slash-commands/builtin-registry.ts +49 -7
- package/src/stt/asr-worker.ts +2 -37
- package/src/stt/sherpa-runtime.ts +71 -0
- package/src/task/executor.ts +6 -5
- package/src/thinking.ts +39 -0
- package/src/tools/auto-generated-guard.ts +18 -5
- package/src/tools/bash.ts +43 -15
- package/src/tools/computer/exposure.ts +38 -0
- package/src/tools/computer/supervisor.ts +61 -13
- package/src/tools/computer/worker-entry.ts +28 -19
- package/src/tools/computer/worker.ts +3 -7
- package/src/tools/computer.ts +65 -10
- package/src/tools/gh-cache-invalidation.ts +2 -82
- package/src/tools/memory-render.ts +11 -2
- package/src/tools/render-utils.ts +8 -3
- package/src/tools/shell-tokenize.ts +83 -0
- package/src/tools/todo.ts +44 -17
- package/src/tools/write.ts +1 -1
|
@@ -1,25 +1,34 @@
|
|
|
1
1
|
import { parentPort } from "node:worker_threads";
|
|
2
|
-
import { consumeWorkerInbox } from "@oh-my-pi/pi-utils/worker-host";
|
|
2
|
+
import { consumeWorkerInbox, isWorkerHostSelector } from "@oh-my-pi/pi-utils/worker-host";
|
|
3
3
|
import type { ComputerWorkerInbound, ComputerWorkerTransport } from "./protocol";
|
|
4
4
|
import { ComputerWorkerCore } from "./worker";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
export function startComputerWorker(): void {
|
|
7
|
+
if (!parentPort) throw new Error("computer-worker-entry: missing parentPort");
|
|
7
8
|
|
|
8
|
-
const port = parentPort;
|
|
9
|
-
const inbox = consumeWorkerInbox();
|
|
10
|
-
const transport: ComputerWorkerTransport = {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
};
|
|
9
|
+
const port = parentPort;
|
|
10
|
+
const inbox = consumeWorkerInbox();
|
|
11
|
+
const transport: ComputerWorkerTransport = {
|
|
12
|
+
send(message, transfer) {
|
|
13
|
+
port.postMessage(message, transfer ?? []);
|
|
14
|
+
},
|
|
15
|
+
onMessage(handler) {
|
|
16
|
+
if (inbox) return inbox.bind(message => handler(message as ComputerWorkerInbound));
|
|
17
|
+
const listener = (message: unknown): void => handler(message as ComputerWorkerInbound);
|
|
18
|
+
port.on("message", listener);
|
|
19
|
+
return () => port.off("message", listener);
|
|
20
|
+
},
|
|
21
|
+
close() {
|
|
22
|
+
port.close();
|
|
23
|
+
},
|
|
24
|
+
};
|
|
24
25
|
|
|
25
|
-
new ComputerWorkerCore(transport);
|
|
26
|
+
new ComputerWorkerCore(transport);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Bun workers report `import.meta.main === false`. The source fallback still
|
|
30
|
+
// enters this file directly, while packaged CLI workers carry the selector and
|
|
31
|
+
// start the named entry only after installing its inbox.
|
|
32
|
+
if (!Bun.isMainThread && !process.argv.some(isWorkerHostSelector) && import.meta.path === Bun.main) {
|
|
33
|
+
startComputerWorker();
|
|
34
|
+
}
|
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
type DesktopCapture,
|
|
4
|
-
DesktopSession,
|
|
5
|
-
type DesktopSessionOptions,
|
|
6
|
-
} from "@oh-my-pi/pi-natives";
|
|
1
|
+
import type { DesktopAction, DesktopCapture, DesktopSession, DesktopSessionOptions } from "@oh-my-pi/pi-natives";
|
|
2
|
+
import { createDesktopSession } from "@oh-my-pi/pi-natives/desktop";
|
|
7
3
|
import type { ComputerWorkerError, ComputerWorkerInbound, ComputerWorkerTransport } from "./protocol";
|
|
8
4
|
|
|
9
5
|
export interface NativeDesktopSession {
|
|
@@ -44,7 +40,7 @@ export class ComputerWorkerCore {
|
|
|
44
40
|
|
|
45
41
|
constructor(
|
|
46
42
|
private readonly transport: ComputerWorkerTransport,
|
|
47
|
-
private readonly createSession: NativeDesktopSessionFactory =
|
|
43
|
+
private readonly createSession: NativeDesktopSessionFactory = createDesktopSession,
|
|
48
44
|
) {
|
|
49
45
|
this.#unsubscribe = transport.onMessage(message => this.#onMessage(message));
|
|
50
46
|
}
|
package/src/tools/computer.ts
CHANGED
|
@@ -5,7 +5,8 @@ import type {
|
|
|
5
5
|
AgentToolUpdateCallback,
|
|
6
6
|
ToolApprovalDecision,
|
|
7
7
|
} from "@oh-my-pi/pi-agent-core";
|
|
8
|
-
import type { ComputerAction, ComputerSafetyCheck, ComputerToolCallMetadata } from "@oh-my-pi/pi-ai";
|
|
8
|
+
import type { ComputerAction, ComputerSafetyCheck, ComputerToolCallMetadata, Model } from "@oh-my-pi/pi-ai";
|
|
9
|
+
import { isClaudeModelId } from "@oh-my-pi/pi-catalog/identity";
|
|
9
10
|
import type {
|
|
10
11
|
DesktopAction,
|
|
11
12
|
DesktopCapabilities,
|
|
@@ -21,6 +22,36 @@ import { type ComputerController, ComputerSupervisor, registerComputerController
|
|
|
21
22
|
import type { ToolSession } from "./index";
|
|
22
23
|
import { ToolError, throwIfAborted } from "./tool-errors";
|
|
23
24
|
|
|
25
|
+
// Image transports that cannot preserve native screenshot detail resize frames
|
|
26
|
+
// without returning transformed dimensions. Keep their native coordinate frames
|
|
27
|
+
// below the empirically verified threshold so pointer actions match what the
|
|
28
|
+
// model sees. Claude paths predate the resolved transport capability and retain
|
|
29
|
+
// their established model-family fallback.
|
|
30
|
+
const COORDINATE_SAFE_MAX_CAPTURE_WIDTH = 1280;
|
|
31
|
+
const COORDINATE_SAFE_MAX_CAPTURE_HEIGHT = 896;
|
|
32
|
+
|
|
33
|
+
function usesCoordinateSafeImageSizing(model: Model | undefined): boolean {
|
|
34
|
+
if (!model) return false;
|
|
35
|
+
const compat = model.compat;
|
|
36
|
+
return (
|
|
37
|
+
(!!compat && "supportsImageDetailOriginal" in compat && compat.supportsImageDetailOriginal === false) ||
|
|
38
|
+
isClaudeModelId(model.id) ||
|
|
39
|
+
(model.requestModelId !== undefined && isClaudeModelId(model.requestModelId)) ||
|
|
40
|
+
(typeof model.name === "string" && /^claude(?:\s|$)/i.test(model.name))
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function captureOptions(session: ToolSession, coordinateSafeImageSizing: boolean): DesktopSessionOptions {
|
|
45
|
+
const maxWidth = session.settings.get("computer.maxWidth");
|
|
46
|
+
const maxHeight = session.settings.get("computer.maxHeight");
|
|
47
|
+
return {
|
|
48
|
+
backend: session.settings.get("computer.backend"),
|
|
49
|
+
display: session.settings.get("computer.display"),
|
|
50
|
+
maxWidth: coordinateSafeImageSizing ? Math.min(maxWidth, COORDINATE_SAFE_MAX_CAPTURE_WIDTH) : maxWidth,
|
|
51
|
+
maxHeight: coordinateSafeImageSizing ? Math.min(maxHeight, COORDINATE_SAFE_MAX_CAPTURE_HEIGHT) : maxHeight,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
24
55
|
// Desktop actions cross the N-API boundary as i32; out-of-range JS numbers
|
|
25
56
|
// must fail closed here instead of truncating in the napi conversion.
|
|
26
57
|
const INT32_MIN = -2_147_483_648;
|
|
@@ -230,7 +261,7 @@ function isComputerAction(value: unknown): value is ComputerAction {
|
|
|
230
261
|
function parseActions(value: unknown): ComputerAction[] {
|
|
231
262
|
// Missing or empty action batches degrade to a plain screenshot so a
|
|
232
263
|
// function-calling model can observe the screen before acting.
|
|
233
|
-
if (value
|
|
264
|
+
if (value == null) return [{ type: "screenshot" }];
|
|
234
265
|
if (!Array.isArray(value)) throw new ToolError("Computer call requires an array of actions");
|
|
235
266
|
if (value.length === 0) return [{ type: "screenshot" }];
|
|
236
267
|
if (!value.every(isComputerAction)) throw new ToolError("Computer call contains an invalid action");
|
|
@@ -286,6 +317,7 @@ function callMetadata(context: AgentToolContext | undefined): ComputerToolCallMe
|
|
|
286
317
|
export function computerApproval(args: unknown): ToolApprovalDecision {
|
|
287
318
|
const actions =
|
|
288
319
|
args && typeof args === "object" && "actions" in args ? (args as { actions?: unknown }).actions : undefined;
|
|
320
|
+
if (actions == null) return "read";
|
|
289
321
|
if (!Array.isArray(actions)) return "exec";
|
|
290
322
|
return actions.every(action => {
|
|
291
323
|
if (!action || typeof action !== "object") return false;
|
|
@@ -356,8 +388,16 @@ export class ComputerTool implements AgentTool<typeof computerSchema, ComputerTo
|
|
|
356
388
|
const actions = args && typeof args === "object" ? (args as { actions?: unknown }).actions : undefined;
|
|
357
389
|
return approvalActionSummary(actions);
|
|
358
390
|
};
|
|
359
|
-
|
|
360
|
-
|
|
391
|
+
/**
|
|
392
|
+
* Settings snapshot used to create the tool's current controller; refreshed
|
|
393
|
+
* when a model switch crosses the coordinate-safe sizing boundary. Surfaced
|
|
394
|
+
* by `/computer status`.
|
|
395
|
+
*/
|
|
396
|
+
effectiveConfiguration: Readonly<DesktopSessionOptions>;
|
|
397
|
+
readonly #createController: ComputerControllerFactory;
|
|
398
|
+
#controller: ComputerController;
|
|
399
|
+
#unregisterOwner: () => void;
|
|
400
|
+
#usesCoordinateSafeImageSizing: boolean;
|
|
361
401
|
#closed = false;
|
|
362
402
|
#description?: string;
|
|
363
403
|
|
|
@@ -365,12 +405,10 @@ export class ComputerTool implements AgentTool<typeof computerSchema, ComputerTo
|
|
|
365
405
|
readonly session: ToolSession,
|
|
366
406
|
createController: ComputerControllerFactory = options => new ComputerSupervisor(options),
|
|
367
407
|
) {
|
|
368
|
-
this.#
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
maxHeight: session.settings.get("computer.maxHeight"),
|
|
373
|
-
});
|
|
408
|
+
this.#createController = createController;
|
|
409
|
+
this.#usesCoordinateSafeImageSizing = usesCoordinateSafeImageSizing(session.getActiveModel?.());
|
|
410
|
+
this.effectiveConfiguration = Object.freeze(captureOptions(session, this.#usesCoordinateSafeImageSizing));
|
|
411
|
+
this.#controller = createController(this.effectiveConfiguration);
|
|
374
412
|
this.#unregisterOwner = registerComputerController(
|
|
375
413
|
session.getEvalKernelOwnerId?.() ?? undefined,
|
|
376
414
|
this.#controller,
|
|
@@ -381,6 +419,21 @@ export class ComputerTool implements AgentTool<typeof computerSchema, ComputerTo
|
|
|
381
419
|
return this.#description;
|
|
382
420
|
}
|
|
383
421
|
|
|
422
|
+
async #refreshControllerForModel(): Promise<void> {
|
|
423
|
+
const nextUsesCoordinateSafeImageSizing = usesCoordinateSafeImageSizing(this.session.getActiveModel?.());
|
|
424
|
+
if (nextUsesCoordinateSafeImageSizing === this.#usesCoordinateSafeImageSizing) return;
|
|
425
|
+
|
|
426
|
+
const previous = this.#controller;
|
|
427
|
+
const nextOptions = Object.freeze(captureOptions(this.session, nextUsesCoordinateSafeImageSizing));
|
|
428
|
+
const next = this.#createController(nextOptions);
|
|
429
|
+
this.#unregisterOwner();
|
|
430
|
+
this.#controller = next;
|
|
431
|
+
this.#unregisterOwner = registerComputerController(this.session.getEvalKernelOwnerId?.() ?? undefined, next);
|
|
432
|
+
this.#usesCoordinateSafeImageSizing = nextUsesCoordinateSafeImageSizing;
|
|
433
|
+
this.effectiveConfiguration = nextOptions;
|
|
434
|
+
await previous.close();
|
|
435
|
+
}
|
|
436
|
+
|
|
384
437
|
async execute(
|
|
385
438
|
_toolCallId: string,
|
|
386
439
|
params: ComputerParams,
|
|
@@ -396,6 +449,8 @@ export class ComputerTool implements AgentTool<typeof computerSchema, ComputerTo
|
|
|
396
449
|
if (pendingSafetyChecks.length > 0 && context?.providerSafetyApproved !== true) {
|
|
397
450
|
throw new ToolError("Provider safety checks require interactive approval before computer input");
|
|
398
451
|
}
|
|
452
|
+
await this.#refreshControllerForModel();
|
|
453
|
+
throwIfAborted(signal);
|
|
399
454
|
const capture = await this.#controller.execute(actions.map(toDesktopAction), signal);
|
|
400
455
|
throwIfAborted(signal);
|
|
401
456
|
const data = Buffer.from(capture.data).toBase64();
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* dwarfs the cost of one cache miss.
|
|
19
19
|
*/
|
|
20
20
|
import { invalidateAllForNumber, invalidateAllForRepo } from "./github-cache";
|
|
21
|
+
import { tokenizeShellSegments } from "./shell-tokenize";
|
|
21
22
|
|
|
22
23
|
const PR_URL_PATTERN = /^https:\/\/github\.com\/([^/\s]+\/[^/\s]+)\/pull\/(\d+)(?:[/?#].*)?$/i;
|
|
23
24
|
const ISSUE_URL_PATTERN = /^https:\/\/github\.com\/([^/\s]+\/[^/\s]+)\/issues\/(\d+)(?:[/?#].*)?$/i;
|
|
@@ -154,87 +155,6 @@ function detectGhMutation(tokens: readonly string[]): { number?: number; repo?:
|
|
|
154
155
|
return repo !== undefined ? { repo } : {};
|
|
155
156
|
}
|
|
156
157
|
|
|
157
|
-
/**
|
|
158
|
-
* Conservative tokenizer that splits a bash command into individual word
|
|
159
|
-
* tokens. Handles single/double-quoted strings, backslash escapes, and
|
|
160
|
-
* standard operators (`;`, `&&`, `||`, `|`, `&`, newlines) as token
|
|
161
|
-
* boundaries that emit a sentinel `";"` so the caller treats the segments
|
|
162
|
-
* as independent command sequences. We do not attempt full POSIX shell
|
|
163
|
-
* parsing — heredocs, command substitution, and arithmetic expansion are
|
|
164
|
-
* out of scope; the detector simply falls through when it cannot find a
|
|
165
|
-
* clean `gh issue|pr <subcmd>` triple.
|
|
166
|
-
*/
|
|
167
|
-
function tokenize(command: string): string[][] {
|
|
168
|
-
const segments: string[][] = [];
|
|
169
|
-
let current: string[] = [];
|
|
170
|
-
let buffer = "";
|
|
171
|
-
let inSingle = false;
|
|
172
|
-
let inDouble = false;
|
|
173
|
-
const pushBuffer = () => {
|
|
174
|
-
if (buffer.length > 0) {
|
|
175
|
-
current.push(buffer);
|
|
176
|
-
buffer = "";
|
|
177
|
-
}
|
|
178
|
-
};
|
|
179
|
-
const pushSegment = () => {
|
|
180
|
-
pushBuffer();
|
|
181
|
-
if (current.length > 0) segments.push(current);
|
|
182
|
-
current = [];
|
|
183
|
-
};
|
|
184
|
-
for (let i = 0; i < command.length; i++) {
|
|
185
|
-
const ch = command[i];
|
|
186
|
-
if (inSingle) {
|
|
187
|
-
if (ch === "'") {
|
|
188
|
-
inSingle = false;
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
buffer += ch;
|
|
192
|
-
continue;
|
|
193
|
-
}
|
|
194
|
-
if (inDouble) {
|
|
195
|
-
if (ch === "\\" && i + 1 < command.length) {
|
|
196
|
-
const next = command[i + 1];
|
|
197
|
-
if (next === '"' || next === "\\" || next === "$" || next === "`") {
|
|
198
|
-
buffer += next;
|
|
199
|
-
i++;
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
if (ch === '"') {
|
|
204
|
-
inDouble = false;
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
buffer += ch;
|
|
208
|
-
continue;
|
|
209
|
-
}
|
|
210
|
-
if (ch === "'") {
|
|
211
|
-
inSingle = true;
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
if (ch === '"') {
|
|
215
|
-
inDouble = true;
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
if (ch === "\\" && i + 1 < command.length) {
|
|
219
|
-
buffer += command[i + 1];
|
|
220
|
-
i++;
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
if (ch === " " || ch === "\t") {
|
|
224
|
-
pushBuffer();
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
if (ch === "\n" || ch === ";" || ch === "&" || ch === "|" || ch === "(" || ch === ")") {
|
|
228
|
-
pushSegment();
|
|
229
|
-
// `&&`, `||` already collapsed by the segment break above.
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
|
-
buffer += ch;
|
|
233
|
-
}
|
|
234
|
-
pushSegment();
|
|
235
|
-
return segments;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
158
|
/**
|
|
239
159
|
* Drop `github-cache` rows for any `gh issue|pr <mutating-subcmd>` call
|
|
240
160
|
* embedded in `command`. Safe to invoke unconditionally; no-op when the
|
|
@@ -242,7 +162,7 @@ function tokenize(command: string): string[][] {
|
|
|
242
162
|
*/
|
|
243
163
|
export function invalidateGithubCacheForBashCommand(command: string): void {
|
|
244
164
|
if (!command?.includes("gh")) return;
|
|
245
|
-
const segments =
|
|
165
|
+
const segments = tokenizeShellSegments(command);
|
|
246
166
|
for (const segment of segments) {
|
|
247
167
|
const hit = detectGhMutation(segment);
|
|
248
168
|
if (!hit) continue;
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
// from the active theme (`•` by default, a nerd-font dot under nerd themes).
|
|
28
28
|
|
|
29
29
|
interface RetainRenderArgs {
|
|
30
|
-
items?:
|
|
30
|
+
items?: unknown;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
interface QueryRenderArgs {
|
|
@@ -35,7 +35,16 @@ interface QueryRenderArgs {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
function retainContents(args: RetainRenderArgs | undefined): string[] {
|
|
38
|
-
|
|
38
|
+
const items = args?.items;
|
|
39
|
+
if (!Array.isArray(items)) return [];
|
|
40
|
+
|
|
41
|
+
const contents: string[] = [];
|
|
42
|
+
for (const item of items) {
|
|
43
|
+
if (!item || typeof item !== "object" || !("content" in item) || typeof item.content !== "string") continue;
|
|
44
|
+
const content = replaceTabs(item.content.trim());
|
|
45
|
+
if (content.length > 0) contents.push(content);
|
|
46
|
+
}
|
|
47
|
+
return contents;
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
function resultText(result: { content?: Array<{ type: string; text?: string }> }): string {
|
|
@@ -13,7 +13,8 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
13
13
|
import { getKeybindings, replaceTabs, truncateToWidth } from "@oh-my-pi/pi-tui";
|
|
14
14
|
import { pluralize } from "@oh-my-pi/pi-utils";
|
|
15
15
|
import { formatKeyHints, type KeyId } from "../config/keybindings";
|
|
16
|
-
import { settings } from "../config/settings";
|
|
16
|
+
import { isSettingsInitialized, settings } from "../config/settings";
|
|
17
|
+
import { getDefault } from "../config/settings-schema";
|
|
17
18
|
import type { Theme } from "../modes/theme/theme";
|
|
18
19
|
import { Hasher } from "../tui/utils";
|
|
19
20
|
import { formatDimensionNote, type ResizedImage } from "../utils/image-resize";
|
|
@@ -27,8 +28,12 @@ export { replaceTabs, truncateToWidth, wrapTextWithAnsi } from "@oh-my-pi/pi-tui
|
|
|
27
28
|
|
|
28
29
|
/** Resolve inline image dimension caps from settings and viewport. */
|
|
29
30
|
export function resolveImageOptions(): { maxWidthCells: number; maxHeightCells?: number } {
|
|
30
|
-
const
|
|
31
|
-
const
|
|
31
|
+
const activeSettings = isSettingsInitialized() ? settings : undefined;
|
|
32
|
+
const maxWidthCells = activeSettings?.get("tui.maxInlineImageColumns") ?? getDefault("tui.maxInlineImageColumns");
|
|
33
|
+
const rowSetting = Math.max(
|
|
34
|
+
0,
|
|
35
|
+
activeSettings?.get("tui.maxInlineImageRows") ?? getDefault("tui.maxInlineImageRows"),
|
|
36
|
+
);
|
|
32
37
|
const viewportRows = process.stdout.rows;
|
|
33
38
|
const viewportFraction = viewportRows ? Math.floor(viewportRows * 0.6) : 0;
|
|
34
39
|
let maxHeightCells: number | undefined;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conservative shell command tokenizer shared by the bash approval-pattern
|
|
3
|
+
* matcher and the gh-cache invalidator.
|
|
4
|
+
*
|
|
5
|
+
* Splits a bash command into independent command segments, each a list of word
|
|
6
|
+
* tokens. Handles single/double-quoted strings, backslash escapes, and the
|
|
7
|
+
* standard operators (`;`, `&&`, `||`, `|`, `&`, `(`, `)`, newlines) as segment
|
|
8
|
+
* boundaries so callers treat the pieces as independent command sequences.
|
|
9
|
+
*
|
|
10
|
+
* It is deliberately not a full POSIX parser — heredocs, command substitution,
|
|
11
|
+
* and arithmetic expansion are out of scope; callers fall through when they
|
|
12
|
+
* cannot find the structure they need.
|
|
13
|
+
*/
|
|
14
|
+
export function tokenizeShellSegments(command: string): string[][] {
|
|
15
|
+
const segments: string[][] = [];
|
|
16
|
+
let current: string[] = [];
|
|
17
|
+
let buffer = "";
|
|
18
|
+
let inSingle = false;
|
|
19
|
+
let inDouble = false;
|
|
20
|
+
const pushBuffer = () => {
|
|
21
|
+
if (buffer.length > 0) {
|
|
22
|
+
current.push(buffer);
|
|
23
|
+
buffer = "";
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const pushSegment = () => {
|
|
27
|
+
pushBuffer();
|
|
28
|
+
if (current.length > 0) segments.push(current);
|
|
29
|
+
current = [];
|
|
30
|
+
};
|
|
31
|
+
for (let i = 0; i < command.length; i++) {
|
|
32
|
+
const ch = command[i];
|
|
33
|
+
if (inSingle) {
|
|
34
|
+
if (ch === "'") {
|
|
35
|
+
inSingle = false;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
buffer += ch;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (inDouble) {
|
|
42
|
+
if (ch === "\\" && i + 1 < command.length) {
|
|
43
|
+
const next = command[i + 1];
|
|
44
|
+
if (next === '"' || next === "\\" || next === "$" || next === "`") {
|
|
45
|
+
buffer += next;
|
|
46
|
+
i++;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (ch === '"') {
|
|
51
|
+
inDouble = false;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
buffer += ch;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (ch === "'") {
|
|
58
|
+
inSingle = true;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (ch === '"') {
|
|
62
|
+
inDouble = true;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (ch === "\\" && i + 1 < command.length) {
|
|
66
|
+
buffer += command[i + 1];
|
|
67
|
+
i++;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (ch === " " || ch === "\t") {
|
|
71
|
+
pushBuffer();
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (ch === "\n" || ch === ";" || ch === "&" || ch === "|" || ch === "(" || ch === ")") {
|
|
75
|
+
pushSegment();
|
|
76
|
+
// `&&`, `||` already collapsed by the segment break above.
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
buffer += ch;
|
|
80
|
+
}
|
|
81
|
+
pushSegment();
|
|
82
|
+
return segments;
|
|
83
|
+
}
|
package/src/tools/todo.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
|
|
|
2
2
|
import type { ToolExample } from "@oh-my-pi/pi-ai";
|
|
3
3
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
4
4
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
5
|
-
import { isRecord, prompt } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { isRecord, prompt, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
6
6
|
import { type } from "arktype";
|
|
7
7
|
import chalk from "chalk";
|
|
8
8
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
@@ -12,7 +12,7 @@ import type { ToolSession } from "../sdk";
|
|
|
12
12
|
import type { SessionEntry } from "../session/session-entries";
|
|
13
13
|
import { framedBlock, renderStatusLine, renderTreeList } from "../tui";
|
|
14
14
|
import { normalizePathLikeInput, resolveToCwd } from "./path-utils";
|
|
15
|
-
import { formatErrorDetail, formatMoreItems, PREVIEW_LIMITS, pluralize } from "./render-utils";
|
|
15
|
+
import { formatErrorDetail, formatMoreItems, PREVIEW_LIMITS, pluralize, replaceTabs } from "./render-utils";
|
|
16
16
|
|
|
17
17
|
// =============================================================================
|
|
18
18
|
// Types
|
|
@@ -929,9 +929,27 @@ export function phaseRomanNumeral(oneBasedIndex: number): string {
|
|
|
929
929
|
return out;
|
|
930
930
|
}
|
|
931
931
|
|
|
932
|
-
/**
|
|
932
|
+
/**
|
|
933
|
+
* Every render boundary in this file funnels display text through here.
|
|
934
|
+
*
|
|
935
|
+
* `sanitizeText` strips ANSI/C0 sequences but deliberately preserves tabs, and
|
|
936
|
+
* a raw tab punches holes in bordered TUI output, so both are needed. The raw
|
|
937
|
+
* value stays untouched everywhere else: task content and phase names are the
|
|
938
|
+
* identity keys the local list is looked up by, and what gets persisted.
|
|
939
|
+
*/
|
|
940
|
+
function forDisplay(text: string): string {
|
|
941
|
+
return replaceTabs(sanitizeText(text));
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* Display-only phase header: `I. Foundation`. State and prompts never see this.
|
|
946
|
+
*
|
|
947
|
+
* Sanitized for the same reason task labels are: this is a render boundary and
|
|
948
|
+
* the name may carry provider or session text holding control sequences. The
|
|
949
|
+
* raw `phase.name` stays the lookup key everywhere else.
|
|
950
|
+
*/
|
|
933
951
|
export function formatPhaseDisplayName(name: string, oneBasedIndex: number): string {
|
|
934
|
-
return `${phaseRomanNumeral(oneBasedIndex)}. ${name}`;
|
|
952
|
+
return `${phaseRomanNumeral(oneBasedIndex)}. ${forDisplay(name)}`;
|
|
935
953
|
}
|
|
936
954
|
|
|
937
955
|
export const TODO_STRIKE_HOLD_FRAMES = 2;
|
|
@@ -970,27 +988,31 @@ function formatTodoLine(
|
|
|
970
988
|
matched = false,
|
|
971
989
|
): string {
|
|
972
990
|
const checkbox = uiTheme.checkbox;
|
|
991
|
+
// Sanitize only for display. A mirrored Cursor snapshot carries provider text
|
|
992
|
+
// verbatim, and a label holding ANSI/C0 sequences would otherwise rewrite the
|
|
993
|
+
// terminal every time the list renders or replays. `item.content` stays raw
|
|
994
|
+
// everywhere else: it is the identity key the local list is looked up by
|
|
995
|
+
// (`findTaskByContent`) and what gets persisted.
|
|
996
|
+
const label = forDisplay(item.content);
|
|
973
997
|
switch (item.status) {
|
|
974
998
|
case "completed": {
|
|
975
|
-
const revealCount = completionKeys.has(item.content) ? strikeRevealCount(
|
|
999
|
+
const revealCount = completionKeys.has(item.content) ? strikeRevealCount(label, frame) : undefined;
|
|
976
1000
|
const content =
|
|
977
|
-
revealCount === undefined
|
|
978
|
-
? strikethroughText(item.content)
|
|
979
|
-
: partialStrikethrough(item.content, revealCount);
|
|
1001
|
+
revealCount === undefined ? strikethroughText(label) : partialStrikethrough(label, revealCount);
|
|
980
1002
|
return uiTheme.fg("success", `${prefix}${checkbox.checked} ${content}`);
|
|
981
1003
|
}
|
|
982
1004
|
case "in_progress":
|
|
983
|
-
return uiTheme.fg("accent", `${prefix}${checkbox.unchecked} ${
|
|
1005
|
+
return uiTheme.fg("accent", `${prefix}${checkbox.unchecked} ${label}`);
|
|
984
1006
|
case "abandoned":
|
|
985
|
-
return uiTheme.fg("error", `${prefix}${checkbox.unchecked} ${strikethroughText(
|
|
1007
|
+
return uiTheme.fg("error", `${prefix}${checkbox.unchecked} ${strikethroughText(label)}`);
|
|
986
1008
|
case "blocked": {
|
|
987
|
-
const note = item.blocker ? `blocked: ${item.blocker}` : "blocked";
|
|
988
|
-
return uiTheme.fg("warning", `${prefix}${checkbox.unchecked} ${
|
|
1009
|
+
const note = item.blocker ? `blocked: ${forDisplay(item.blocker)}` : "blocked";
|
|
1010
|
+
return uiTheme.fg("warning", `${prefix}${checkbox.unchecked} ${label} (${note})`);
|
|
989
1011
|
}
|
|
990
1012
|
default:
|
|
991
1013
|
// A pending todo lit by a live subagent match renders accent, matching
|
|
992
1014
|
// the sticky HUD's convention (#5873).
|
|
993
|
-
return uiTheme.fg(matched ? "accent" : "dim", `${prefix}${checkbox.unchecked} ${
|
|
1015
|
+
return uiTheme.fg(matched ? "accent" : "dim", `${prefix}${checkbox.unchecked} ${label}`);
|
|
994
1016
|
}
|
|
995
1017
|
}
|
|
996
1018
|
|
|
@@ -1065,13 +1087,15 @@ export const todoToolRenderer = {
|
|
|
1065
1087
|
// both the new single-op and legacy batch shapes so a malformed delta
|
|
1066
1088
|
// never breaks the TUI render loop (#2005).
|
|
1067
1089
|
const opsList = normalizeTodoArg(args);
|
|
1090
|
+
// Model-authored, partially-streamed strings going straight into a header:
|
|
1091
|
+
// `renderStatusLine` only flattens CR/LF and leaves the rest to the caller.
|
|
1068
1092
|
const ops =
|
|
1069
1093
|
opsList.length === 0
|
|
1070
1094
|
? ["update"]
|
|
1071
1095
|
: opsList.map(e => {
|
|
1072
|
-
const parts = [e.op ?? "update"];
|
|
1073
|
-
if (e.task) parts.push(e.task);
|
|
1074
|
-
if (e.phase) parts.push(e.phase);
|
|
1096
|
+
const parts = [forDisplay(e.op ?? "update")];
|
|
1097
|
+
if (e.task) parts.push(forDisplay(e.task));
|
|
1098
|
+
if (e.phase) parts.push(forDisplay(e.phase));
|
|
1075
1099
|
if (Array.isArray(e.items) && e.items.length) {
|
|
1076
1100
|
parts.push(`${e.items.length} item${e.items.length === 1 ? "" : "s"}`);
|
|
1077
1101
|
}
|
|
@@ -1125,7 +1149,10 @@ export const todoToolRenderer = {
|
|
|
1125
1149
|
uiTheme,
|
|
1126
1150
|
);
|
|
1127
1151
|
if (allTasks.length === 0) {
|
|
1128
|
-
|
|
1152
|
+
// Provider text on the Cursor path (the todo summary or a refusal note),
|
|
1153
|
+
// so sanitize like every other label. The error branch above already
|
|
1154
|
+
// goes through `formatErrorDetail`.
|
|
1155
|
+
const fallback = forDisplay(result.content?.find(content => content.type === "text")?.text ?? "No todos");
|
|
1129
1156
|
return new Text(`${header}\n ${uiTheme.fg("dim", fallback)}`, 0, 0);
|
|
1130
1157
|
}
|
|
1131
1158
|
|
package/src/tools/write.ts
CHANGED
|
@@ -1215,7 +1215,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
1215
1215
|
|
|
1216
1216
|
// Check if file exists and is auto-generated before overwriting
|
|
1217
1217
|
if (await fs.exists(absolutePath)) {
|
|
1218
|
-
await assertEditableFile(absolutePath, path);
|
|
1218
|
+
await assertEditableFile(absolutePath, path, this.session.settings);
|
|
1219
1219
|
}
|
|
1220
1220
|
|
|
1221
1221
|
const displayPath = formatPathRelativeToCwd(absolutePath, this.session.cwd);
|