@oh-my-pi/pi-coding-agent 17.1.7 → 17.2.0
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 +127 -0
- package/dist/{CHANGELOG-5k4dq4g6.md → CHANGELOG-be1f2t8h.md} +127 -0
- package/dist/cli.js +9554 -12379
- package/dist/template-8vdv6xb4.js +1671 -0
- package/dist/template-f8wx9vfn.css +1355 -0
- package/dist/template-qat058wr.html +55 -0
- package/dist/tool-views.generated-jdfmzwmn.js +35 -0
- package/dist/types/advisor/advise-tool.d.ts +0 -7
- package/dist/types/advisor/runtime.d.ts +8 -11
- package/dist/types/advisor/transcript-recorder.d.ts +15 -0
- package/dist/types/async/job-manager.d.ts +11 -0
- package/dist/types/auto-thinking/classifier.d.ts +9 -2
- package/dist/types/cleanse/agent.d.ts +19 -0
- package/dist/types/cleanse/balance.d.ts +7 -0
- package/dist/types/cleanse/checkers.d.ts +13 -0
- package/dist/types/cleanse/index.d.ts +16 -0
- package/dist/types/cleanse/loop.d.ts +16 -0
- package/dist/types/cleanse/parsers.d.ts +13 -0
- package/dist/types/cleanse/progress.d.ts +14 -0
- package/dist/types/cleanse/types.d.ts +64 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/session-picker.d.ts +14 -7
- package/dist/types/collab/guest.d.ts +21 -2
- package/dist/types/commands/cleanse.d.ts +23 -0
- package/dist/types/commands/launch.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +63 -8
- package/dist/types/cursor-bridge-tools.d.ts +54 -0
- package/dist/types/cursor.d.ts +193 -8
- package/dist/types/edit/edit-clipboard.d.ts +19 -0
- package/dist/types/edit/hashline/diff.d.ts +9 -1
- package/dist/types/edit/index.d.ts +8 -1
- package/dist/types/edit/streaming.d.ts +8 -1
- package/dist/types/export/html/index.d.ts +2 -0
- package/dist/types/extensibility/extensions/runner.d.ts +19 -2
- package/dist/types/extensibility/extensions/types.d.ts +25 -1
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +27 -1
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +2 -0
- package/dist/types/internal-urls/local-protocol.d.ts +3 -2
- package/dist/types/launch/terminal-output-worker-client.d.ts +5 -0
- package/dist/types/launch/terminal-output-worker-protocol.d.ts +14 -0
- package/dist/types/launch/terminal-output-worker.d.ts +1 -0
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/main.d.ts +2 -0
- package/dist/types/mcp/manager.d.ts +33 -4
- package/dist/types/modes/components/codex-reset-fireworks.d.ts +51 -0
- package/dist/types/modes/components/read-tool-group.d.ts +12 -0
- package/dist/types/modes/components/session-selector.d.ts +6 -0
- package/dist/types/modes/components/status-line/component.d.ts +23 -0
- package/dist/types/modes/components/tool-execution.d.ts +16 -7
- package/dist/types/modes/components/usage-row.d.ts +2 -0
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +26 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +8 -5
- package/dist/types/modes/rpc/rpc-client.d.ts +7 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +16 -0
- package/dist/types/modes/theme/theme.d.ts +12 -1
- package/dist/types/modes/types.d.ts +2 -1
- package/dist/types/sdk.d.ts +1 -0
- package/dist/types/session/agent-session-events.d.ts +2 -0
- package/dist/types/session/agent-session-types.d.ts +36 -1
- package/dist/types/session/agent-session.d.ts +4 -2
- package/dist/types/session/async-job-delivery.d.ts +8 -0
- package/dist/types/session/claude-session-store.d.ts +13 -0
- package/dist/types/session/codex-session-store.d.ts +14 -0
- package/dist/types/session/credential-pin.d.ts +58 -0
- package/dist/types/session/foreign-session-import.d.ts +15 -0
- package/dist/types/session/foreign-session-jsonl.d.ts +9 -0
- package/dist/types/session/foreign-session-store.d.ts +23 -0
- package/dist/types/session/model-controls.d.ts +3 -3
- package/dist/types/session/session-advisors.d.ts +47 -2
- package/dist/types/session/session-entries.d.ts +19 -1
- package/dist/types/session/session-handoff.d.ts +4 -1
- package/dist/types/session/session-loader.d.ts +2 -0
- package/dist/types/session/session-maintenance.d.ts +1 -1
- package/dist/types/session/session-manager.d.ts +21 -0
- package/dist/types/session/session-tools.d.ts +25 -1
- package/dist/types/session/turn-recovery.d.ts +10 -4
- package/dist/types/system-prompt.d.ts +2 -1
- package/dist/types/task/executor.d.ts +11 -2
- package/dist/types/task/isolation-ownership.d.ts +34 -0
- package/dist/types/task/render.d.ts +7 -0
- package/dist/types/thinking.d.ts +18 -6
- package/dist/types/tools/acp-bridge.d.ts +28 -5
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +1 -2
- package/dist/types/tools/browser/tab-worker.d.ts +1 -4
- package/dist/types/tools/grep.d.ts +17 -1
- package/dist/types/tools/hub/launch.d.ts +5 -1
- package/dist/types/tools/index.d.ts +4 -1
- package/dist/types/tools/path-utils.d.ts +19 -0
- package/dist/types/tools/read.d.ts +2 -0
- package/dist/types/tools/xdev.d.ts +2 -1
- package/dist/types/tui/output-block.d.ts +5 -5
- package/dist/types/utils/changelog.d.ts +19 -0
- package/dist/types/utils/git.d.ts +10 -0
- package/dist/types/utils/jj.d.ts +11 -4
- package/dist/types/web/search/types.d.ts +2 -2
- package/package.json +16 -12
- package/scripts/bundle-dist.ts +3 -1
- package/src/advisor/advise-tool.ts +0 -11
- package/src/advisor/runtime.ts +138 -24
- package/src/advisor/transcript-recorder.ts +56 -0
- package/src/async/job-manager.ts +30 -7
- package/src/auto-thinking/classifier.ts +48 -10
- package/src/cleanse/agent.ts +226 -0
- package/src/cleanse/balance.ts +79 -0
- package/src/cleanse/checkers.ts +996 -0
- package/src/cleanse/index.ts +190 -0
- package/src/cleanse/loop.ts +51 -0
- package/src/cleanse/parsers.ts +726 -0
- package/src/cleanse/progress.ts +50 -0
- package/src/cleanse/prompts/assignment.md +47 -0
- package/src/cleanse/types.ts +72 -0
- package/src/cli/args.ts +6 -0
- package/src/cli/flag-tables.ts +2 -0
- package/src/cli/gallery-fixtures/fs.ts +25 -0
- package/src/cli/session-picker.ts +34 -17
- package/src/cli/ttsr-cli.ts +19 -1
- package/src/cli/update-cli.ts +3 -0
- package/src/cli/usage-cli.ts +29 -4
- package/src/cli/worktree-cli.ts +28 -11
- package/src/cli-commands.ts +1 -0
- package/src/cli.ts +11 -1
- package/src/collab/guest.ts +37 -4
- package/src/collab/host.ts +1 -0
- package/src/commands/cleanse.ts +45 -0
- package/src/commands/launch.ts +6 -0
- package/src/config/model-registry.ts +103 -25
- package/src/config/settings-schema.ts +65 -6
- package/src/config/settings.ts +25 -0
- package/src/cursor-bridge-tools.ts +81 -0
- package/src/cursor.ts +463 -12
- package/src/discovery/claude-plugins.ts +144 -34
- package/src/edit/edit-clipboard.ts +23 -0
- package/src/edit/hashline/diff.ts +49 -10
- package/src/edit/hashline/execute.ts +38 -6
- package/src/edit/hashline/filesystem.ts +27 -3
- package/src/edit/index.ts +12 -2
- package/src/edit/renderer.ts +4 -4
- package/src/edit/streaming.ts +17 -2
- package/src/export/html/index.ts +17 -10
- package/src/export/html/template.js +1 -1
- package/src/extensibility/extensions/runner.ts +70 -0
- package/src/extensibility/extensions/types.ts +30 -0
- package/src/extensibility/legacy-pi-ai-shim.ts +46 -1
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +12 -13
- package/src/internal-urls/local-protocol.ts +3 -2
- package/src/internal-urls/mcp-protocol.ts +4 -1
- package/src/launch/broker.ts +14 -4
- package/src/launch/terminal-output-worker-client.ts +53 -0
- package/src/launch/terminal-output-worker-protocol.ts +11 -0
- package/src/launch/terminal-output-worker.ts +23 -0
- package/src/lsp/client.ts +18 -3
- package/src/lsp/index.ts +4 -0
- package/src/lsp/render.ts +1 -1
- package/src/main.ts +127 -56
- package/src/mcp/manager.ts +120 -18
- package/src/modes/acp/acp-agent.ts +27 -18
- package/src/modes/acp/acp-event-mapper.ts +38 -4
- package/src/modes/components/chat-transcript-builder.ts +30 -11
- package/src/modes/components/codex-reset-fireworks.ts +369 -0
- package/src/modes/components/read-tool-group.ts +159 -17
- package/src/modes/components/session-selector.ts +19 -3
- package/src/modes/components/settings-selector.ts +9 -1
- package/src/modes/components/status-line/component.ts +381 -54
- package/src/modes/components/tool-execution.ts +78 -31
- package/src/modes/components/usage-row.ts +10 -5
- package/src/modes/controllers/event-controller.ts +26 -4
- package/src/modes/controllers/mcp-command-controller.ts +68 -3
- package/src/modes/controllers/selector-controller.ts +94 -36
- package/src/modes/controllers/tan-command-controller.ts +14 -0
- package/src/modes/interactive-mode.ts +67 -64
- package/src/modes/prompt-action-autocomplete.ts +5 -3
- package/src/modes/rpc/rpc-client.ts +18 -0
- package/src/modes/rpc/rpc-mode.ts +16 -1
- package/src/modes/rpc/rpc-types.ts +11 -0
- package/src/modes/theme/theme.ts +151 -6
- package/src/modes/types.ts +2 -1
- package/src/modes/utils/ui-helpers.ts +38 -21
- package/src/prompts/goals/guided-goal-interview.md +41 -6
- package/src/prompts/system/auto-thinking-difficulty.md +4 -2
- package/src/prompts/system/plan-mode-active.md +2 -2
- package/src/prompts/system/system-prompt.md +3 -0
- package/src/prompts/system/vibe-mode-active.md +4 -1
- package/src/prompts/system/xdev-mount-notice.md +1 -1
- package/src/prompts/tools/bash.md +15 -17
- package/src/prompts/tools/browser.md +1 -0
- package/src/prompts/tools/checkpoint.md +1 -1
- package/src/prompts/tools/glob.md +7 -6
- package/src/prompts/tools/goal.md +1 -1
- package/src/prompts/tools/grep.md +7 -6
- package/src/sdk.ts +178 -15
- package/src/session/agent-session-events.ts +1 -0
- package/src/session/agent-session-types.ts +40 -1
- package/src/session/agent-session.ts +330 -179
- package/src/session/async-job-delivery.ts +8 -0
- package/src/session/claude-session-store.ts +426 -0
- package/src/session/codex-session-store.ts +673 -0
- package/src/session/credential-pin.ts +93 -0
- package/src/session/foreign-session-import.ts +52 -0
- package/src/session/foreign-session-jsonl.ts +29 -0
- package/src/session/foreign-session-store.ts +26 -0
- package/src/session/model-controls.ts +15 -7
- package/src/session/session-advisors.ts +142 -23
- package/src/session/session-entries.ts +21 -1
- package/src/session/session-handoff.ts +20 -3
- package/src/session/session-history-format.ts +31 -6
- package/src/session/session-loader.ts +25 -9
- package/src/session/session-maintenance.ts +59 -11
- package/src/session/session-manager.ts +61 -0
- package/src/session/session-tools.ts +107 -5
- package/src/session/settings-stream-fn.ts +1 -0
- package/src/session/turn-recovery.ts +97 -51
- package/src/slash-commands/builtin-registry.ts +136 -5
- package/src/system-prompt.ts +3 -2
- package/src/task/executor.ts +17 -4
- package/src/task/isolation-ownership.ts +106 -0
- package/src/task/render.ts +20 -4
- package/src/task/worktree.ts +8 -0
- package/src/thinking.ts +29 -10
- package/src/tools/acp-bridge.ts +52 -8
- package/src/tools/ask.ts +3 -3
- package/src/tools/ast-edit.ts +9 -2
- package/src/tools/browser/cmux/cmux-tab.ts +9 -14
- package/src/tools/browser/tab-worker.ts +12 -35
- package/src/tools/browser.ts +7 -2
- package/src/tools/checkpoint.ts +0 -13
- package/src/tools/gh-renderer.ts +3 -3
- package/src/tools/grep.ts +64 -8
- package/src/tools/hub/launch.ts +10 -15
- package/src/tools/index.ts +38 -5
- package/src/tools/path-utils.ts +88 -0
- package/src/tools/read.ts +3 -0
- package/src/tools/write.ts +44 -3
- package/src/tools/xdev.ts +39 -14
- package/src/tui/code-cell.ts +4 -4
- package/src/tui/output-block.ts +25 -8
- package/src/utils/changelog.ts +144 -4
- package/src/utils/git.ts +61 -14
- package/src/utils/jj.ts +19 -13
- package/src/utils/shell-snapshot-fn-env.sh +5 -2
- package/src/web/search/providers/codex.ts +34 -0
- package/src/web/search/render.ts +2 -2
- package/src/web/search/types.ts +1 -1
- package/dist/types/goals/guided-setup.d.ts +0 -30
- package/src/goals/guided-setup.ts +0 -171
- package/src/prompts/goals/guided-goal-system.md +0 -33
package/src/tools/grep.ts
CHANGED
|
@@ -884,6 +884,22 @@ export interface GrepToolDetails {
|
|
|
884
884
|
|
|
885
885
|
type SearchParams = typeof searchSchema.infer;
|
|
886
886
|
|
|
887
|
+
/**
|
|
888
|
+
* Construction-time overrides for callers that are not the model.
|
|
889
|
+
*
|
|
890
|
+
* The model-facing schema deliberately does not grow these: they exist for
|
|
891
|
+
* wire bridges (the Cursor `pi_grep` frame) whose protocol carries an explicit
|
|
892
|
+
* context width and total match cap, and which would otherwise have to drop
|
|
893
|
+
* them. Unset means "use the session settings / built-in caps" — the behavior
|
|
894
|
+
* every model-issued call keeps.
|
|
895
|
+
*/
|
|
896
|
+
export interface GrepToolOptions {
|
|
897
|
+
/** Overrides `grep.contextBefore`/`grep.contextAfter` for every call on this instance. */
|
|
898
|
+
context?: number;
|
|
899
|
+
/** Caps total surfaced matches. Applied on top of the built-in per-file and file-window caps, never above them. */
|
|
900
|
+
totalMatchLimit?: number;
|
|
901
|
+
}
|
|
902
|
+
|
|
887
903
|
export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails> {
|
|
888
904
|
readonly name = "grep";
|
|
889
905
|
readonly approval = (args: unknown): ToolTier => {
|
|
@@ -897,7 +913,17 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
897
913
|
readonly parameters = searchSchema;
|
|
898
914
|
readonly strict = true;
|
|
899
915
|
|
|
900
|
-
|
|
916
|
+
readonly #contextOverride?: number;
|
|
917
|
+
readonly #totalMatchLimit?: number;
|
|
918
|
+
|
|
919
|
+
constructor(
|
|
920
|
+
private readonly session: ToolSession,
|
|
921
|
+
options?: GrepToolOptions,
|
|
922
|
+
) {
|
|
923
|
+
const context = options?.context;
|
|
924
|
+
this.#contextOverride = context !== undefined ? Math.max(0, Math.floor(context)) : undefined;
|
|
925
|
+
const total = options?.totalMatchLimit;
|
|
926
|
+
this.#totalMatchLimit = total !== undefined ? Math.max(1, Math.floor(total)) : undefined;
|
|
901
927
|
const displayMode = resolveFileDisplayMode(session);
|
|
902
928
|
this.description = prompt.render(grepDescription, {
|
|
903
929
|
IS_HL_MODE: displayMode.hashLines,
|
|
@@ -978,8 +1004,8 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
978
1004
|
`or pass a UTF-8 text member.`,
|
|
979
1005
|
);
|
|
980
1006
|
}
|
|
981
|
-
const normalizedContextBefore = this.session.settings.get("grep.contextBefore");
|
|
982
|
-
const normalizedContextAfter = this.session.settings.get("grep.contextAfter");
|
|
1007
|
+
const normalizedContextBefore = this.#contextOverride ?? this.session.settings.get("grep.contextBefore");
|
|
1008
|
+
const normalizedContextAfter = this.#contextOverride ?? this.session.settings.get("grep.contextAfter");
|
|
983
1009
|
const ignoreCase = !(caseSensitive ?? true);
|
|
984
1010
|
const useGitignore = gitignore ?? true;
|
|
985
1011
|
const patternHasNewline = normalizedPattern.includes("\n") || normalizedPattern.includes("\\n");
|
|
@@ -1297,9 +1323,22 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
1297
1323
|
// Single-file scopes can't paginate — there is one file by definition.
|
|
1298
1324
|
const canPaginate = isMultiScope;
|
|
1299
1325
|
const skipFiles = canPaginate ? Math.min(normalizedSkip, totalFiles) : 0;
|
|
1300
|
-
|
|
1301
|
-
|
|
1326
|
+
// A caller with a total match cap is not paginating: the cap bounds the
|
|
1327
|
+
// output, and the only consumer that sets one (`pi_grep`) has no `skip`
|
|
1328
|
+
// field to follow a "use skip=N" suggestion with. Windowing it to the
|
|
1329
|
+
// first 20 files would silently return fewer matches than it asked for
|
|
1330
|
+
// while reporting the cap as unreached.
|
|
1331
|
+
//
|
|
1332
|
+
// The window is cap+1 files, not cap: with one match per file, a cap
|
|
1333
|
+
// of N over exactly N files is complete, while over N+1 files it is
|
|
1334
|
+
// clipped — and only reading that extra file distinguishes the two.
|
|
1335
|
+
// The cap below then does the trimming and records that it bit, so
|
|
1336
|
+
// `match_limit_reached` reaches the frame set.
|
|
1337
|
+
const fileWindow = this.#totalMatchLimit !== undefined ? this.#totalMatchLimit + 1 : DEFAULT_FILE_LIMIT;
|
|
1338
|
+
const windowFiles = canPaginate ? fileOrder.slice(skipFiles, skipFiles + fileWindow) : fileOrder;
|
|
1339
|
+
const fileLimitReached = canPaginate && totalFiles > skipFiles + fileWindow;
|
|
1302
1340
|
const selectedMatches: GrepMatch[] = [];
|
|
1341
|
+
let totalMatchLimitReached = false;
|
|
1303
1342
|
if (windowFiles.length > 0) {
|
|
1304
1343
|
const lists = windowFiles.map(file => matchesByPath.get(file) ?? []);
|
|
1305
1344
|
const cursors = new Array<number>(lists.length).fill(0);
|
|
@@ -1313,6 +1352,14 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
1313
1352
|
}
|
|
1314
1353
|
}
|
|
1315
1354
|
}
|
|
1355
|
+
// Round-robin above interleaves files for diversity, so the cap is
|
|
1356
|
+
// applied after selection rather than as a per-list bound: trimming
|
|
1357
|
+
// mid-rotation would silently favour whichever files sort first.
|
|
1358
|
+
const cap = this.#totalMatchLimit;
|
|
1359
|
+
if (cap !== undefined && selectedMatches.length > cap) {
|
|
1360
|
+
selectedMatches.length = cap;
|
|
1361
|
+
totalMatchLimitReached = true;
|
|
1362
|
+
}
|
|
1316
1363
|
}
|
|
1317
1364
|
const nextSkip = skipFiles + windowFiles.length;
|
|
1318
1365
|
const limitMessage = fileLimitReached
|
|
@@ -1503,7 +1550,12 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
1503
1550
|
const output = truncation.content;
|
|
1504
1551
|
const displayText = displayLines.join("\n");
|
|
1505
1552
|
const truncated = Boolean(
|
|
1506
|
-
fileLimitReached ||
|
|
1553
|
+
fileLimitReached ||
|
|
1554
|
+
perFileLimitReached ||
|
|
1555
|
+
totalMatchLimitReached ||
|
|
1556
|
+
result.limitReached ||
|
|
1557
|
+
truncation.truncated ||
|
|
1558
|
+
linesTruncated,
|
|
1507
1559
|
);
|
|
1508
1560
|
const details: GrepToolDetails = {
|
|
1509
1561
|
scopePath,
|
|
@@ -1517,8 +1569,12 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
1517
1569
|
count: fileMatchCounts.get(path) ?? 0,
|
|
1518
1570
|
})),
|
|
1519
1571
|
truncated,
|
|
1520
|
-
fileLimitReached: fileLimitReached ?
|
|
1521
|
-
perFileLimitReached:
|
|
1572
|
+
fileLimitReached: fileLimitReached ? fileWindow : undefined,
|
|
1573
|
+
perFileLimitReached: totalMatchLimitReached
|
|
1574
|
+
? this.#totalMatchLimit
|
|
1575
|
+
: perFileLimitReached
|
|
1576
|
+
? perFileMatchCap
|
|
1577
|
+
: undefined,
|
|
1522
1578
|
displayContent: displayText,
|
|
1523
1579
|
missingPaths: missingPaths.length > 0 ? missingPaths : undefined,
|
|
1524
1580
|
};
|
package/src/tools/hub/launch.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { sanitizeText } from "@oh-my-pi/pi-utils";
|
|
|
12
12
|
import type { RenderResultOptions } from "../../extensibility/custom-tools/types";
|
|
13
13
|
import { daemonClientForProject } from "../../launch/client";
|
|
14
14
|
import type { DaemonOperation, DaemonRpcResult, DaemonSnapshot, DaemonSpec, DaemonState } from "../../launch/protocol";
|
|
15
|
-
import
|
|
15
|
+
import { renderTerminalOutputIsolated } from "../../launch/terminal-output-worker-client";
|
|
16
16
|
import type { Theme, ThemeColor } from "../../modes/theme/theme";
|
|
17
17
|
import { framedBlock, outputBlockContentWidth, renderStatusLine } from "../../tui";
|
|
18
18
|
import type { ToolSession } from "..";
|
|
@@ -271,15 +271,14 @@ function toolContent(result: DaemonRpcResult, params: LaunchParams): string {
|
|
|
271
271
|
}
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
return runtime.renderTerminalOutput(terminalText, {
|
|
274
|
+
/** Resolve display rows while keeping legacy raw replay outside the client process. */
|
|
275
|
+
export async function renderLaunchLogTerminalRows(
|
|
276
|
+
result: Extract<DaemonRpcResult, { op: "logs" }>,
|
|
277
|
+
params: Pick<LaunchParams, "head" | "lines">,
|
|
278
|
+
): Promise<string[] | undefined> {
|
|
279
|
+
if (result.terminalRows !== undefined) return result.terminalRows;
|
|
280
|
+
if (result.terminalText === undefined) return undefined;
|
|
281
|
+
return renderTerminalOutputIsolated(result.terminalText, {
|
|
283
282
|
head: params.head ?? false,
|
|
284
283
|
maxRows: Math.min(1_000, Math.floor(params.lines ?? 100)),
|
|
285
284
|
});
|
|
@@ -297,11 +296,7 @@ async function toolDetails(result: DaemonRpcResult, params: LaunchParams): Promi
|
|
|
297
296
|
cursor: result.cursor,
|
|
298
297
|
timedOut: result.timedOut,
|
|
299
298
|
state: result.state,
|
|
300
|
-
terminalRows:
|
|
301
|
-
result.terminalRows ??
|
|
302
|
-
(result.terminalText === undefined
|
|
303
|
-
? undefined
|
|
304
|
-
: await renderLegacyTerminalOutput(result.terminalText, params)),
|
|
299
|
+
terminalRows: await renderLaunchLogTerminalRows(result, params).catch(() => undefined),
|
|
305
300
|
};
|
|
306
301
|
case "wait":
|
|
307
302
|
return { op: "wait", daemon: result.daemon, timedOut: result.timedOut, matched: result.matched };
|
package/src/tools/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { InMemorySnapshotStore } from "@oh-my-pi/hashline";
|
|
1
|
+
import type { Clipboard, InMemorySnapshotStore } from "@oh-my-pi/hashline";
|
|
2
2
|
import type { AgentTelemetryConfig, AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
3
3
|
import type { FetchImpl, ImageContent, Model, ServiceTierByFamily, ToolChoice } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
@@ -351,6 +351,10 @@ export interface ToolSession {
|
|
|
351
351
|
* file changed out-of-band. Lazily initialized by `getFileSnapshotStore`. */
|
|
352
352
|
fileSnapshotStore?: InMemorySnapshotStore;
|
|
353
353
|
|
|
354
|
+
/** Per-session `CUT`/`PASTE` clipboard register shared across edit
|
|
355
|
+
* calls. Lazily initialized by `getEditClipboard`. */
|
|
356
|
+
editClipboard?: Clipboard;
|
|
357
|
+
|
|
354
358
|
/** Per-session log of unresolved git merge conflict regions surfaced by
|
|
355
359
|
* `read`. Each entry gets a stable id N referenced by `write conflict://N`
|
|
356
360
|
* to splice the recorded region with replacement content. Lazily initialized
|
|
@@ -502,6 +506,18 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
502
506
|
// unreachable, in which case eval dispatches exclusively to the others.
|
|
503
507
|
const allowEval = effectivePythonAllowed || allowJs || effectiveRubyAllowed || effectiveJuliaAllowed;
|
|
504
508
|
|
|
509
|
+
// Checkpoint and rewind are a pair: listing one without the other strands
|
|
510
|
+
// the agent (it can checkpoint but not rewind, or vice versa). Auto-include
|
|
511
|
+
// the sister tool so a one-sided frontmatter `tools:` entry still works.
|
|
512
|
+
// Unlike the AST/auto-learn convenience auto-includes below, this is a
|
|
513
|
+
// safety pairing — it applies to restricted sessions too.
|
|
514
|
+
if (requestedTools && session.settings.get("checkpoint.enabled")) {
|
|
515
|
+
if (requestedTools.includes("checkpoint") && !requestedTools.includes("rewind")) {
|
|
516
|
+
requestedTools.push("rewind");
|
|
517
|
+
} else if (requestedTools.includes("rewind") && !requestedTools.includes("checkpoint")) {
|
|
518
|
+
requestedTools.push("checkpoint");
|
|
519
|
+
}
|
|
520
|
+
}
|
|
505
521
|
// Auto-include AST counterparts when their text-based sibling is present.
|
|
506
522
|
// Restricted callers own the active list and must not have it widened.
|
|
507
523
|
if (requestedTools && !restrictToolNames) {
|
|
@@ -548,7 +564,16 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
548
564
|
}
|
|
549
565
|
const allTools: Record<string, ToolFactory> = { ...BUILTIN_TOOLS, ...HIDDEN_TOOLS };
|
|
550
566
|
const isToolAllowed = (name: string) => {
|
|
551
|
-
|
|
567
|
+
// Never in the default set. Explicitly activatable while goal.enabled and
|
|
568
|
+
// no goal record exists yet — /guided-goal enables it so the agent can
|
|
569
|
+
// finish the interview with `goal create`, which turns goal mode on. Once
|
|
570
|
+
// a goal record exists, only an enabled goal keeps the tool: a completed
|
|
571
|
+
// (exiting) or paused goal must stop advertising it on the next rebuild.
|
|
572
|
+
if (name === "goal") {
|
|
573
|
+
if (!goalEnabled || restrictToolNames) return false;
|
|
574
|
+
const goalState = session.getGoalModeState?.();
|
|
575
|
+
return goalState === undefined || goalState.enabled === true || goalState.goal.status === "dropped";
|
|
576
|
+
}
|
|
552
577
|
if (name === "lsp") return enableLsp && session.settings.get("lsp.enabled");
|
|
553
578
|
if (name === "bash") return session.settings.get("bash.enabled");
|
|
554
579
|
if (name === "eval") return allowEval;
|
|
@@ -565,7 +590,11 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
565
590
|
if (name === "ask") return session.settings.get("ask.enabled");
|
|
566
591
|
if (name === "browser") return session.settings.get("browser.enabled");
|
|
567
592
|
if (name === "computer") return session.settings.get("computer.enabled");
|
|
568
|
-
if (name === "checkpoint" || name === "rewind")
|
|
593
|
+
if (name === "checkpoint" || name === "rewind")
|
|
594
|
+
return (
|
|
595
|
+
session.settings.get("checkpoint.enabled") &&
|
|
596
|
+
((session.taskDepth ?? 0) === 0 || requestedTools !== undefined)
|
|
597
|
+
);
|
|
569
598
|
if (name === "hub") {
|
|
570
599
|
return (
|
|
571
600
|
!restrictToolNames && session.enableIrc !== false && isIrcEnabled(session.settings, session.taskDepth ?? 0)
|
|
@@ -575,11 +604,15 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
575
604
|
return ["hindsight", "mnemopi"].includes(session.settings.get("memory.backend") ?? "");
|
|
576
605
|
}
|
|
577
606
|
if (name === "memory_edit") return session.settings.get("memory.backend") === "mnemopi";
|
|
578
|
-
if (name === "manage_skill")
|
|
607
|
+
if (name === "manage_skill")
|
|
608
|
+
return (
|
|
609
|
+
session.settings.get("autolearn.enabled") &&
|
|
610
|
+
((session.taskDepth ?? 0) === 0 || requestedTools !== undefined)
|
|
611
|
+
);
|
|
579
612
|
if (name === "learn") {
|
|
580
613
|
return (
|
|
581
614
|
session.settings.get("autolearn.enabled") &&
|
|
582
|
-
(session.taskDepth ?? 0) === 0 &&
|
|
615
|
+
((session.taskDepth ?? 0) === 0 || requestedTools !== undefined) &&
|
|
583
616
|
["hindsight", "mnemopi", "local"].includes(session.settings.get("memory.backend") ?? "")
|
|
584
617
|
);
|
|
585
618
|
}
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -520,6 +520,94 @@ export function resolveToCwd(filePath: string, cwd: string): string {
|
|
|
520
520
|
return path.resolve(cwd, expanded);
|
|
521
521
|
}
|
|
522
522
|
|
|
523
|
+
/**
|
|
524
|
+
* Resolve a path that MUST stay inside `cwd`, or `null` when it would escape.
|
|
525
|
+
*
|
|
526
|
+
* {@link resolveToCwd} deliberately honors absolute paths, `~`, and `..` —
|
|
527
|
+
* correct for a path a user typed, wrong for one a remote peer supplied.
|
|
528
|
+
* Callers handling untrusted input (Cursor's `download_path`) use this instead:
|
|
529
|
+
* only a non-empty relative path landing under the live cwd is accepted, so
|
|
530
|
+
* neither `/etc/passwd` nor `../../escape` can be written through.
|
|
531
|
+
*
|
|
532
|
+
* The lexical check alone is not containment: a symlink inside the workspace
|
|
533
|
+
* can point anywhere, so `out/config` under a `ws/out -> /elsewhere` link is
|
|
534
|
+
* relative, `..`-free, and still writes outside. Both the target and its
|
|
535
|
+
* deepest existing ancestor are therefore realpath-resolved — the ancestor
|
|
536
|
+
* because a download names a file that does not exist yet, so the link in its
|
|
537
|
+
* path is the only thing that can be resolved before the write.
|
|
538
|
+
*
|
|
539
|
+
* The cwd itself is rejected: a download names a file, never the directory.
|
|
540
|
+
*/
|
|
541
|
+
export function confineToWorkspace(filePath: string, cwd: string): string | null {
|
|
542
|
+
if (!filePath || path.isAbsolute(filePath)) return null;
|
|
543
|
+
// `~` expands to an absolute path, and an internal URL is not a filesystem
|
|
544
|
+
// target at all; neither is a relative workspace path.
|
|
545
|
+
if (filePath.startsWith("~") || isInternalUrlPath(filePath)) return null;
|
|
546
|
+
const root = path.resolve(cwd);
|
|
547
|
+
const resolved = path.resolve(root, filePath);
|
|
548
|
+
if (!isUnderRootLexical(resolved, root)) return null;
|
|
549
|
+
|
|
550
|
+
// A workspace reached through a link of its own is legitimate (/tmp on
|
|
551
|
+
// macOS), so the real root is the comparison basis. An unresolvable root is
|
|
552
|
+
// not a workspace to contain anything in.
|
|
553
|
+
const realRoot = tryRealpath(root);
|
|
554
|
+
if (!realRoot) return null;
|
|
555
|
+
|
|
556
|
+
// An existing target is authoritative: resolve it outright.
|
|
557
|
+
const realTarget = tryRealpath(resolved);
|
|
558
|
+
if (realTarget) return isUnderRootLexical(realTarget, realRoot) ? resolved : null;
|
|
559
|
+
|
|
560
|
+
// `realpath` also fails on a *dangling* link, and a write follows that link
|
|
561
|
+
// to wherever it points. Chasing the chain to decide would mean
|
|
562
|
+
// reimplementing symlink resolution (multi-hop, relative hops, loops, and
|
|
563
|
+
// a TOCTOU window against a link that can be re-pointed between the check
|
|
564
|
+
// and the write). A download names a file to create, so a path that is
|
|
565
|
+
// already an unresolvable link is refused outright — the one shape where
|
|
566
|
+
// "cannot tell where this lands" is the whole answer.
|
|
567
|
+
if (isSymlink(resolved)) return null;
|
|
568
|
+
|
|
569
|
+
// Otherwise walk up to the deepest ancestor that does exist and check that,
|
|
570
|
+
// then re-apply the segments below it. Those segments are `..`-free by the
|
|
571
|
+
// lexical check above, so they cannot climb back out.
|
|
572
|
+
let ancestor = path.dirname(resolved);
|
|
573
|
+
const tail: string[] = [path.basename(resolved)];
|
|
574
|
+
for (;;) {
|
|
575
|
+
const real = tryRealpath(ancestor);
|
|
576
|
+
if (real) {
|
|
577
|
+
return isUnderRootLexical(path.join(real, ...tail.reverse()), realRoot) ? resolved : null;
|
|
578
|
+
}
|
|
579
|
+
const parent = path.dirname(ancestor);
|
|
580
|
+
// Ran past the root without finding anything real: the workspace itself
|
|
581
|
+
// resolved above, so this cannot happen unless it vanished mid-check.
|
|
582
|
+
if (parent === ancestor || !isUnderRootLexical(ancestor, root)) return null;
|
|
583
|
+
tail.push(path.basename(ancestor));
|
|
584
|
+
ancestor = parent;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/** Whether `target` is a strict descendant of `root`, ignoring symlinks. */
|
|
589
|
+
function isUnderRootLexical(target: string, root: string): boolean {
|
|
590
|
+
const relative = path.relative(root, target);
|
|
591
|
+
return !!relative && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function tryRealpath(target: string): string | null {
|
|
595
|
+
try {
|
|
596
|
+
return fs.realpathSync.native(target);
|
|
597
|
+
} catch {
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/** Whether the path itself is a symlink, without following it. */
|
|
603
|
+
function isSymlink(target: string): boolean {
|
|
604
|
+
try {
|
|
605
|
+
return fs.lstatSync(target).isSymbolicLink();
|
|
606
|
+
} catch {
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
523
611
|
export function formatPathRelativeToCwd(
|
|
524
612
|
filePath: string,
|
|
525
613
|
cwd: string,
|
package/src/tools/read.ts
CHANGED
|
@@ -735,6 +735,8 @@ export interface ReadToolDetails {
|
|
|
735
735
|
method?: string;
|
|
736
736
|
notes?: string[];
|
|
737
737
|
meta?: OutputMeta;
|
|
738
|
+
/** Full on-disk byte size recorded before applying a file range. */
|
|
739
|
+
fileSize?: number;
|
|
738
740
|
/** Raw text + start line for user-visible TUI rendering, set when content is text-like.
|
|
739
741
|
* Mirrors the same lines the model receives but without hashline/line-number prefixes,
|
|
740
742
|
* so the TUI can render the file content with its own gutter without re-parsing the formatted text. */
|
|
@@ -2959,6 +2961,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2959
2961
|
}
|
|
2960
2962
|
}
|
|
2961
2963
|
|
|
2964
|
+
details.fileSize = fileSize;
|
|
2962
2965
|
this.#markMarkdownContentType(details, absolutePath);
|
|
2963
2966
|
if (suffixResolution) {
|
|
2964
2967
|
details.suffixResolution = suffixResolution;
|
package/src/tools/write.ts
CHANGED
|
@@ -160,7 +160,42 @@ function throwReadSelectorMisfire(target: string, sel: string): never {
|
|
|
160
160
|
);
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Recognize a semicolon-joined list of read-tool selectors mis-dispatched as a
|
|
165
|
+
* single write target — the multi-file read expression the scout emitted in
|
|
166
|
+
* issue #6809 (`a.txt:1-2;b/c.txt:3-4`). Every `;`-segment must be non-empty and
|
|
167
|
+
* carry its own read selector ({@link splitPathAndSel} peels a `:N-M`, `:raw`,
|
|
168
|
+
* or `:conflicts` tail). No real call targets such a list: `read` accepts one
|
|
169
|
+
* path, `write` writes one file. Unlike {@link readSelectorForEmptyWrite} this
|
|
170
|
+
* fires regardless of `content` — the non-empty-content escape hatch exists for
|
|
171
|
+
* a lone selector-shaped *filename*, never a `;`-list, and honoring it here
|
|
172
|
+
* silently creates a nested directory tree (`a.txt:1-2;b/`) in the workspace.
|
|
173
|
+
* The caller still probes the literal target first, so an existing POSIX file
|
|
174
|
+
* by that exact name stays writable (same escape as the single-selector guard).
|
|
175
|
+
*/
|
|
176
|
+
function readSelectorListMisfire(target: string): number | undefined {
|
|
177
|
+
if (!target.includes(";")) return undefined;
|
|
178
|
+
const segments = target.split(";");
|
|
179
|
+
if (segments.length < 2) return undefined;
|
|
180
|
+
for (const segment of segments) {
|
|
181
|
+
const trimmed = segment.trim();
|
|
182
|
+
if (trimmed.length === 0 || splitPathAndSel(trimmed).sel === undefined) return undefined;
|
|
183
|
+
}
|
|
184
|
+
return segments.length;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function throwReadSelectorListMisfire(target: string, count: number): never {
|
|
188
|
+
throw new ToolError(
|
|
189
|
+
`write target '${target}' is a semicolon-joined list of ${count} read-tool selectors, not a filesystem path — refusing to create it. ` +
|
|
190
|
+
`write creates a single file; issue one read() per path to read these ranges (e.g. read({ path: "<one path>:<range>" })).`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
163
194
|
async function assertNotReadSelectorMisfire(target: string, content: string, cwd: string): Promise<void> {
|
|
195
|
+
const listCount = readSelectorListMisfire(target);
|
|
196
|
+
if (listCount !== undefined && (await probeLiteralPathExists(target, cwd)) === "missing") {
|
|
197
|
+
throwReadSelectorListMisfire(target, listCount);
|
|
198
|
+
}
|
|
164
199
|
const sel = readSelectorForEmptyWrite(target, content);
|
|
165
200
|
if (sel === undefined) return;
|
|
166
201
|
if ((await probeLiteralPathExists(target, cwd)) !== "missing") return;
|
|
@@ -1232,9 +1267,15 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
1232
1267
|
|
|
1233
1268
|
// Try ACP bridge first for editor-visible filesystem paths. Internal
|
|
1234
1269
|
// artifacts such as local:// plans are owned by OMP, not the editor.
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1270
|
+
const bridgeWrite = await routeWriteThroughBridge(this.session, path, absolutePath, cleanContent, signal);
|
|
1271
|
+
if (bridgeWrite) {
|
|
1272
|
+
// `write` always replaces the whole file, so (unlike hashline's
|
|
1273
|
+
// hunk-scoped diff) there's no size cost to keying the header/
|
|
1274
|
+
// executable-bit check on the verified post-write content —
|
|
1275
|
+
// use it so a drifted write (e.g. client format-on-save) still
|
|
1276
|
+
// hands back a tag that matches what's actually on disk.
|
|
1277
|
+
const madeExecutable = await maybeMarkExecutableForShebang(absolutePath, bridgeWrite.text);
|
|
1278
|
+
const header = maybeWriteSnapshotHeader(this.session, absolutePath, bridgeWrite.text);
|
|
1238
1279
|
const writeLine = `Successfully wrote ${cleanContent.length} bytes to ${displayPath}`;
|
|
1239
1280
|
let resultText = header ? `${header}\n${writeLine}` : writeLine;
|
|
1240
1281
|
if (stripped) {
|
package/src/tools/xdev.ts
CHANGED
|
@@ -35,6 +35,7 @@ import { parseStreamingJson } from "@oh-my-pi/pi-utils";
|
|
|
35
35
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
36
36
|
import { XD_URL_PREFIX } from "../internal-urls/xd-protocol";
|
|
37
37
|
import type { Theme } from "../modes/theme/theme";
|
|
38
|
+
import { truncateHeadBytes } from "../session/streaming-output";
|
|
38
39
|
import { renderDefaultToolExecution } from "./default-renderer";
|
|
39
40
|
import type { Tool } from "./index";
|
|
40
41
|
import { replaceTabs } from "./render-utils";
|
|
@@ -178,14 +179,34 @@ function toolSummary(inst: Tool): string {
|
|
|
178
179
|
return firstLine?.trim() ?? inst.label ?? inst.name;
|
|
179
180
|
}
|
|
180
181
|
|
|
181
|
-
|
|
182
|
+
/** C0/C1 controls and Unicode line/paragraph separators; summaries must remain one line. */
|
|
183
|
+
const SUMMARY_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]+/g;
|
|
184
|
+
const SUMMARY_ELLIPSIS = "…";
|
|
185
|
+
const SUMMARY_ELLIPSIS_BYTES = Buffer.byteLength(SUMMARY_ELLIPSIS, "utf-8");
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Bound a catalog summary for prompt rendering. External summaries are
|
|
189
|
+
* third-party metadata inlined verbatim, so control characters are stripped
|
|
190
|
+
* first, then the result is bounded in UTF-8 BYTES rather than characters (a
|
|
191
|
+
* character bound is not a byte bound for multi-byte scripts). The cut lands
|
|
192
|
+
* on a code point boundary, so the prompt never carries a partial code point.
|
|
193
|
+
*/
|
|
194
|
+
function sanitizeCatalogSummary(summary: string, maxBytes?: number): string {
|
|
195
|
+
const cleaned = summary.replace(SUMMARY_CONTROL_CHARS, " ").trim();
|
|
196
|
+
if (maxBytes === undefined || Buffer.byteLength(cleaned, "utf-8") <= maxBytes) return cleaned;
|
|
197
|
+
if (maxBytes <= 0) return "";
|
|
198
|
+
if (maxBytes < SUMMARY_ELLIPSIS_BYTES) return truncateHeadBytes(cleaned, maxBytes).text;
|
|
199
|
+
const body = truncateHeadBytes(cleaned, maxBytes - SUMMARY_ELLIPSIS_BYTES).text.trimEnd();
|
|
200
|
+
return `${body}${SUMMARY_ELLIPSIS}`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function promptCatalogSummary(inst: Tool, maxBytes?: number): string {
|
|
182
204
|
const summary =
|
|
183
205
|
toolSummary(inst)
|
|
184
206
|
.split("\n")
|
|
185
207
|
.find(line => line.trim().length > 0)
|
|
186
208
|
?.trim() ?? inst.name;
|
|
187
|
-
|
|
188
|
-
return `${summary.slice(0, maxLength).trimEnd()}…`;
|
|
209
|
+
return sanitizeCatalogSummary(summary, maxBytes) || inst.name;
|
|
189
210
|
}
|
|
190
211
|
|
|
191
212
|
/** Compile the `tools.xdevInlineDevices` allowlist once per render, dropping
|
|
@@ -257,15 +278,19 @@ export function listXdevTools(state: XdevState): Tool[] {
|
|
|
257
278
|
});
|
|
258
279
|
}
|
|
259
280
|
|
|
260
|
-
/** `{name, summary}`
|
|
261
|
-
export function xdevEntries(state: XdevState): Array<{ name: string; summary: string }> {
|
|
262
|
-
return listXdevTools(state).map(tool =>
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
281
|
+
/** `{name, summary, dynamic}` triples for prompt templates and `/tools` display. */
|
|
282
|
+
export function xdevEntries(state: XdevState): Array<{ name: string; summary: string; dynamic: boolean }> {
|
|
283
|
+
return listXdevTools(state).map(tool => {
|
|
284
|
+
// Built-ins are first-party; anything else carries third-party metadata. One
|
|
285
|
+
// boolean drives both the description cap and the flag callers present, so
|
|
286
|
+
// the two can never disagree about which summaries are untrusted.
|
|
287
|
+
const dynamic = !state.builtInNames.has(tool.name);
|
|
288
|
+
return {
|
|
289
|
+
name: tool.name,
|
|
290
|
+
summary: promptCatalogSummary(tool, dynamic ? XDEV_EXTERNAL_DESCRIPTION_CAP : undefined),
|
|
291
|
+
dynamic,
|
|
292
|
+
};
|
|
293
|
+
});
|
|
269
294
|
}
|
|
270
295
|
|
|
271
296
|
/** `read xd://` listing with one device per line. */
|
|
@@ -313,8 +338,8 @@ export function xdevDocsAll(
|
|
|
313
338
|
[
|
|
314
339
|
"## Additional devices (docs on demand)",
|
|
315
340
|
...overflow.map(tool => {
|
|
316
|
-
const
|
|
317
|
-
return `- ${XD_URL_PREFIX}${tool.name} — ${promptCatalogSummary(tool,
|
|
341
|
+
const maxBytes = state.builtInNames.has(tool.name) ? undefined : XDEV_EXTERNAL_DESCRIPTION_CAP;
|
|
342
|
+
return `- ${XD_URL_PREFIX}${tool.name} — ${promptCatalogSummary(tool, maxBytes)}`;
|
|
318
343
|
}),
|
|
319
344
|
"",
|
|
320
345
|
`Read ${XD_URL_PREFIX}<tool> for full docs + JSON schema before first use.`,
|
package/src/tui/code-cell.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
formatStatusIcon,
|
|
11
11
|
replaceTabs,
|
|
12
12
|
} from "../tools/render-utils";
|
|
13
|
-
import { renderOutputBlock } from "./output-block";
|
|
13
|
+
import { outputBlockContentWidth, renderOutputBlock } from "./output-block";
|
|
14
14
|
import type { State } from "./types";
|
|
15
15
|
|
|
16
16
|
export interface CodeCellOptions {
|
|
@@ -230,9 +230,9 @@ export function renderMarkdownCell(options: MarkdownCellOptions, theme: Theme):
|
|
|
230
230
|
const { title, meta } = formatHeader(codeOptions, theme);
|
|
231
231
|
const state = getState(options.status);
|
|
232
232
|
|
|
233
|
-
// Markdown component manages its own wrapping at the inner
|
|
234
|
-
// `renderOutputBlock
|
|
235
|
-
const innerWidth = Math.max(20, width
|
|
233
|
+
// Markdown component manages its own wrapping at the same inner width as
|
|
234
|
+
// `renderOutputBlock`, so collapsed row caps are applied after final wrapping.
|
|
235
|
+
const innerWidth = Math.max(20, outputBlockContentWidth(width));
|
|
236
236
|
const allLines = content.trim() ? new Markdown(content, 0, 0, getMarkdownTheme()).render(innerWidth) : [];
|
|
237
237
|
const maxContentLines = expanded ? allLines.length : Math.min(allLines.length, contentMaxLines);
|
|
238
238
|
const contentLines = allLines.slice(0, maxContentLines);
|
package/src/tui/output-block.ts
CHANGED
|
@@ -17,6 +17,7 @@ export interface OutputBlockOptions {
|
|
|
17
17
|
width: number;
|
|
18
18
|
applyBg?: boolean;
|
|
19
19
|
contentPaddingLeft?: number;
|
|
20
|
+
contentPaddingRight?: number;
|
|
20
21
|
/** Override the state-derived border color. Used for muted "legacy" tool
|
|
21
22
|
* frames that should not visually compete with framed-output tools. */
|
|
22
23
|
borderColor?: ThemeColor;
|
|
@@ -48,13 +49,18 @@ function normalizeContentPaddingLeft(value: number | undefined): number {
|
|
|
48
49
|
|
|
49
50
|
/**
|
|
50
51
|
* Inner content width that {@link renderOutputBlock} wraps its body to, for a
|
|
51
|
-
* given outer `width`: both vertical borders
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* into more rows than they counted and the box overflows its intended height.
|
|
52
|
+
* given outer `width`: both vertical borders plus symmetric content padding.
|
|
53
|
+
* An explicit left padding of zero keeps legacy flush blocks flush on both
|
|
54
|
+
* sides unless a right padding is provided separately.
|
|
55
55
|
*/
|
|
56
|
-
export function outputBlockContentWidth(
|
|
57
|
-
|
|
56
|
+
export function outputBlockContentWidth(
|
|
57
|
+
width: number,
|
|
58
|
+
contentPaddingLeft?: number,
|
|
59
|
+
contentPaddingRight?: number,
|
|
60
|
+
): number {
|
|
61
|
+
const left = normalizeContentPaddingLeft(contentPaddingLeft);
|
|
62
|
+
const right = normalizeContentPaddingLeft(contentPaddingRight ?? left);
|
|
63
|
+
return Math.max(1, width - 2 - left - right);
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): string[] {
|
|
@@ -88,8 +94,13 @@ export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): st
|
|
|
88
94
|
})();
|
|
89
95
|
|
|
90
96
|
const contentPaddingLeft = normalizeContentPaddingLeft(options.contentPaddingLeft);
|
|
91
|
-
const
|
|
97
|
+
const contentPaddingRight = normalizeContentPaddingLeft(options.contentPaddingRight ?? contentPaddingLeft);
|
|
98
|
+
const contentWidth = Math.max(
|
|
99
|
+
0,
|
|
100
|
+
lineWidth - visibleWidth(v) - contentPaddingLeft - contentPaddingRight - visibleWidth(v),
|
|
101
|
+
);
|
|
92
102
|
const contentLeftPadding = contentPaddingLeft > 0 ? padding(contentPaddingLeft) : "";
|
|
103
|
+
const contentRightPadding = contentPaddingRight > 0 ? padding(contentPaddingRight) : "";
|
|
93
104
|
|
|
94
105
|
// ── Layout pass: collect row descriptors before emitting the bordered lines. ──
|
|
95
106
|
const rows: BlockRow[] = [];
|
|
@@ -170,7 +181,8 @@ export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): st
|
|
|
170
181
|
return `${border(leftGlyphs)}${border(fillGlyphs)}${border(rightGlyph)}`;
|
|
171
182
|
};
|
|
172
183
|
|
|
173
|
-
const renderContent = (inner: string): string =>
|
|
184
|
+
const renderContent = (inner: string): string =>
|
|
185
|
+
`${border(v)}${contentLeftPadding}${inner}${contentRightPadding}${border(v)}`;
|
|
174
186
|
|
|
175
187
|
const lines: string[] = [];
|
|
176
188
|
for (let r = 0; r < H; r++) {
|
|
@@ -215,6 +227,11 @@ export class CachedOutputBlock {
|
|
|
215
227
|
const h = new Hasher();
|
|
216
228
|
h.u32(options.width);
|
|
217
229
|
h.u32(normalizeContentPaddingLeft(options.contentPaddingLeft));
|
|
230
|
+
h.u32(
|
|
231
|
+
normalizeContentPaddingLeft(
|
|
232
|
+
options.contentPaddingRight ?? normalizeContentPaddingLeft(options.contentPaddingLeft),
|
|
233
|
+
),
|
|
234
|
+
);
|
|
218
235
|
h.optional(options.header);
|
|
219
236
|
h.optional(options.headerMeta);
|
|
220
237
|
h.optional(options.state);
|