gsd-pi 2.67.0-dev.1cd1e0f → 2.67.0-dev.2367d7e
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/README.md +1 -1
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +155 -70
- package/dist/resources/extensions/gsd/auto/phases.js +17 -0
- package/dist/resources/extensions/gsd/auto/session.js +10 -0
- package/dist/resources/extensions/gsd/auto-direct-dispatch.js +12 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +1 -1
- package/dist/resources/extensions/gsd/auto-start.js +16 -30
- package/dist/resources/extensions/gsd/auto-worktree.js +62 -15
- package/dist/resources/extensions/gsd/auto.js +121 -59
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +11 -435
- package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +1 -4
- package/dist/resources/extensions/gsd/bootstrap/query-tools.js +7 -64
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +7 -2
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +88 -8
- package/dist/resources/extensions/gsd/commands/catalog.js +2 -1
- package/dist/resources/extensions/gsd/commands/handlers/core.js +39 -25
- package/dist/resources/extensions/gsd/commands/index.js +8 -1
- package/dist/resources/extensions/gsd/commands-mcp-status.js +43 -7
- package/dist/resources/extensions/gsd/doctor-git-checks.js +4 -4
- package/dist/resources/extensions/gsd/doctor-proactive.js +3 -3
- package/dist/resources/extensions/gsd/doctor.js +8 -4
- package/dist/resources/extensions/gsd/gsd-db.js +11 -0
- package/dist/resources/extensions/gsd/guided-flow.js +56 -31
- package/dist/resources/extensions/gsd/init-wizard.js +37 -0
- package/dist/resources/extensions/gsd/interrupted-session.js +146 -0
- package/dist/resources/extensions/gsd/mcp-project-config.js +83 -0
- package/dist/resources/extensions/gsd/state.js +7 -2
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +508 -0
- package/dist/resources/extensions/gsd/workflow-logger.js +18 -3
- package/dist/resources/extensions/gsd/workflow-mcp.js +261 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +8 -8
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/react-loadable-manifest.json +2 -2
- package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +8 -8
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/2826.821e01b07d92e948.js +9 -0
- package/dist/web/standalone/.next/static/chunks/app/{page-0c485498795110d6.js → page-f1e30ab6bb269149.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/{webpack-b49b09f97429b5d0.js → webpack-6e4d7e9a4f57bed4.js} +1 -1
- package/package.json +4 -2
- package/packages/mcp-server/README.md +38 -0
- package/packages/mcp-server/dist/cli.d.ts +9 -0
- package/packages/mcp-server/dist/cli.d.ts.map +1 -0
- package/packages/mcp-server/dist/cli.js +58 -0
- package/packages/mcp-server/dist/cli.js.map +1 -0
- package/packages/mcp-server/dist/index.d.ts +20 -0
- package/packages/mcp-server/dist/index.d.ts.map +1 -0
- package/packages/mcp-server/dist/index.js +14 -0
- package/packages/mcp-server/dist/index.js.map +1 -0
- package/packages/mcp-server/dist/readers/captures.d.ts +25 -0
- package/packages/mcp-server/dist/readers/captures.d.ts.map +1 -0
- package/packages/mcp-server/dist/readers/captures.js +67 -0
- package/packages/mcp-server/dist/readers/captures.js.map +1 -0
- package/packages/mcp-server/dist/readers/doctor-lite.d.ts +20 -0
- package/packages/mcp-server/dist/readers/doctor-lite.d.ts.map +1 -0
- package/packages/mcp-server/dist/readers/doctor-lite.js +173 -0
- package/packages/mcp-server/dist/readers/doctor-lite.js.map +1 -0
- package/packages/mcp-server/dist/readers/index.d.ts +14 -0
- package/packages/mcp-server/dist/readers/index.d.ts.map +1 -0
- package/packages/mcp-server/dist/readers/index.js +10 -0
- package/packages/mcp-server/dist/readers/index.js.map +1 -0
- package/packages/mcp-server/dist/readers/knowledge.d.ts +18 -0
- package/packages/mcp-server/dist/readers/knowledge.d.ts.map +1 -0
- package/packages/mcp-server/dist/readers/knowledge.js +82 -0
- package/packages/mcp-server/dist/readers/knowledge.js.map +1 -0
- package/packages/mcp-server/dist/readers/metrics.d.ts +32 -0
- package/packages/mcp-server/dist/readers/metrics.d.ts.map +1 -0
- package/packages/mcp-server/dist/readers/metrics.js +74 -0
- package/packages/mcp-server/dist/readers/metrics.js.map +1 -0
- package/packages/mcp-server/dist/readers/paths.d.ts +42 -0
- package/packages/mcp-server/dist/readers/paths.d.ts.map +1 -0
- package/packages/mcp-server/dist/readers/paths.js +199 -0
- package/packages/mcp-server/dist/readers/paths.js.map +1 -0
- package/packages/mcp-server/dist/readers/roadmap.d.ts +26 -0
- package/packages/mcp-server/dist/readers/roadmap.d.ts.map +1 -0
- package/packages/mcp-server/dist/readers/roadmap.js +194 -0
- package/packages/mcp-server/dist/readers/roadmap.js.map +1 -0
- package/packages/mcp-server/dist/readers/state.d.ts +43 -0
- package/packages/mcp-server/dist/readers/state.d.ts.map +1 -0
- package/packages/mcp-server/dist/readers/state.js +184 -0
- package/packages/mcp-server/dist/readers/state.js.map +1 -0
- package/packages/mcp-server/dist/server.d.ts +28 -0
- package/packages/mcp-server/dist/server.d.ts.map +1 -0
- package/packages/mcp-server/dist/server.js +319 -0
- package/packages/mcp-server/dist/server.js.map +1 -0
- package/packages/mcp-server/dist/session-manager.d.ts +54 -0
- package/packages/mcp-server/dist/session-manager.d.ts.map +1 -0
- package/packages/mcp-server/dist/session-manager.js +284 -0
- package/packages/mcp-server/dist/session-manager.js.map +1 -0
- package/packages/mcp-server/dist/types.d.ts +61 -0
- package/packages/mcp-server/dist/types.d.ts.map +1 -0
- package/packages/mcp-server/dist/types.js +11 -0
- package/packages/mcp-server/dist/types.js.map +1 -0
- package/packages/mcp-server/dist/workflow-tools.d.ts +9 -0
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -0
- package/packages/mcp-server/dist/workflow-tools.js +532 -0
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -0
- package/packages/mcp-server/src/server.ts +6 -2
- package/packages/mcp-server/src/workflow-tools.test.ts +976 -0
- package/packages/mcp-server/src/workflow-tools.ts +997 -0
- package/packages/mcp-server/tsconfig.json +1 -1
- package/packages/pi-agent-core/dist/agent-loop.js +14 -6
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/src/agent-loop.test.ts +53 -0
- package/packages/pi-agent-core/src/agent-loop.ts +20 -6
- package/packages/pi-coding-agent/dist/core/contextual-tips.d.ts +43 -0
- package/packages/pi-coding-agent/dist/core/contextual-tips.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/contextual-tips.js +208 -0
- package/packages/pi-coding-agent/dist/core/contextual-tips.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/contextual-tips.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/contextual-tips.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/contextual-tips.test.js +227 -0
- package/packages/pi-coding-agent/dist/core/contextual-tips.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/index.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/index.js +1 -0
- package/packages/pi-coding-agent/dist/core/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js +28 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +17 -12
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +19 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts +4 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +14 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +3 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +15 -12
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/core/contextual-tips.test.ts +259 -0
- package/packages/pi-coding-agent/src/core/contextual-tips.ts +232 -0
- package/packages/pi-coding-agent/src/core/index.ts +2 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts +54 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +18 -12
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +21 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +19 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +19 -15
- package/packages/rpc-client/dist/index.d.ts +10 -0
- package/packages/rpc-client/dist/index.d.ts.map +1 -0
- package/packages/rpc-client/dist/index.js +9 -0
- package/packages/rpc-client/dist/index.js.map +1 -0
- package/packages/rpc-client/dist/jsonl.d.ts +17 -0
- package/packages/rpc-client/dist/jsonl.d.ts.map +1 -0
- package/packages/rpc-client/dist/jsonl.js +54 -0
- package/packages/rpc-client/dist/jsonl.js.map +1 -0
- package/packages/rpc-client/dist/rpc-client.d.ts +259 -0
- package/packages/rpc-client/dist/rpc-client.d.ts.map +1 -0
- package/packages/rpc-client/dist/rpc-client.js +541 -0
- package/packages/rpc-client/dist/rpc-client.js.map +1 -0
- package/packages/rpc-client/dist/rpc-client.test.d.ts +2 -0
- package/packages/rpc-client/dist/rpc-client.test.d.ts.map +1 -0
- package/packages/rpc-client/dist/rpc-client.test.js +477 -0
- package/packages/rpc-client/dist/rpc-client.test.js.map +1 -0
- package/packages/rpc-client/dist/rpc-types.d.ts +566 -0
- package/packages/rpc-client/dist/rpc-types.d.ts.map +1 -0
- package/packages/rpc-client/dist/rpc-types.js +12 -0
- package/packages/rpc-client/dist/rpc-types.js.map +1 -0
- package/scripts/ensure-workspace-builds.cjs +2 -0
- package/scripts/link-workspace-packages.cjs +21 -14
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +193 -93
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +173 -79
- package/src/resources/extensions/gsd/auto/phases.ts +25 -0
- package/src/resources/extensions/gsd/auto/session.ts +10 -0
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +20 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +1 -1
- package/src/resources/extensions/gsd/auto-start.ts +23 -55
- package/src/resources/extensions/gsd/auto-worktree.ts +59 -15
- package/src/resources/extensions/gsd/auto.ts +133 -64
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +22 -435
- package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +1 -5
- package/src/resources/extensions/gsd/bootstrap/query-tools.ts +7 -72
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +8 -2
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +122 -6
- package/src/resources/extensions/gsd/commands/catalog.ts +2 -1
- package/src/resources/extensions/gsd/commands/handlers/core.ts +53 -26
- package/src/resources/extensions/gsd/commands/index.ts +7 -1
- package/src/resources/extensions/gsd/commands-mcp-status.ts +53 -7
- package/src/resources/extensions/gsd/doctor-git-checks.ts +4 -4
- package/src/resources/extensions/gsd/doctor-proactive.ts +3 -3
- package/src/resources/extensions/gsd/doctor.ts +9 -5
- package/src/resources/extensions/gsd/gsd-db.ts +12 -0
- package/src/resources/extensions/gsd/guided-flow.ts +66 -36
- package/src/resources/extensions/gsd/init-wizard.ts +40 -0
- package/src/resources/extensions/gsd/interrupted-session.ts +224 -0
- package/src/resources/extensions/gsd/mcp-project-config.ts +128 -0
- package/src/resources/extensions/gsd/state.ts +7 -1
- package/src/resources/extensions/gsd/tests/auto-project-root-env.test.ts +29 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +668 -2
- package/src/resources/extensions/gsd/tests/cold-resume-db-reopen.test.ts +14 -4
- package/src/resources/extensions/gsd/tests/copy-planning-artifacts-samepath.test.ts +21 -0
- package/src/resources/extensions/gsd/tests/core-overlay-fallback.test.ts +101 -0
- package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +380 -2
- package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/forensics-context-persist.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +12 -0
- package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/integration/doctor-fixlevel.test.ts +52 -1
- package/src/resources/extensions/gsd/tests/integration/doctor-git.test.ts +2 -9
- package/src/resources/extensions/gsd/tests/integration/doctor-proactive.test.ts +0 -33
- package/src/resources/extensions/gsd/tests/integration/merge-cwd-restore.test.ts +169 -0
- package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +146 -0
- package/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts +136 -0
- package/src/resources/extensions/gsd/tests/mcp-project-config.test.ts +85 -0
- package/src/resources/extensions/gsd/tests/mcp-status.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/verification-operational-gate.test.ts +11 -0
- package/src/resources/extensions/gsd/tests/workflow-logger.test.ts +16 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +500 -0
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +625 -0
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +629 -0
- package/src/resources/extensions/gsd/workflow-logger.ts +19 -3
- package/src/resources/extensions/gsd/workflow-mcp.ts +320 -0
- package/dist/web/standalone/.next/static/chunks/6502.b804e48b7919f55e.js +0 -9
- package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.d.ts +0 -13
- package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.d.ts.map +0 -1
- package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.js +0 -27
- package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.js.map +0 -1
- package/packages/pi-coding-agent/src/modes/interactive/provider-auth-setup.ts +0 -40
- /package/dist/web/standalone/.next/static/{PHqEommYRR8CRn3i84CGM → WMDT_0C0XDkBKtsAI_AX4}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{PHqEommYRR8CRn3i84CGM → WMDT_0C0XDkBKtsAI_AX4}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { ContextualTips } from "./contextual-tips.js";
|
|
4
|
+
|
|
5
|
+
const baseCtx = {
|
|
6
|
+
input: "hello world",
|
|
7
|
+
isStreaming: false,
|
|
8
|
+
thinkingLevel: "off" as string,
|
|
9
|
+
contextPercent: undefined as number | undefined,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe("ContextualTips", () => {
|
|
13
|
+
describe("shell-command-prefix tip", () => {
|
|
14
|
+
it("fires for bare shell commands", () => {
|
|
15
|
+
const tips = new ContextualTips();
|
|
16
|
+
const result = tips.evaluate({ ...baseCtx, input: "ls -la" });
|
|
17
|
+
assert.ok(result);
|
|
18
|
+
assert.ok(result.includes("looks like a shell command"));
|
|
19
|
+
assert.ok(result.includes("!"));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("fires for various known commands", () => {
|
|
23
|
+
for (const cmd of ["pwd", "cd src", "cat file.txt", "grep foo bar", "git status", "npm install", "docker ps"]) {
|
|
24
|
+
const tips = new ContextualTips();
|
|
25
|
+
const result = tips.evaluate({ ...baseCtx, input: cmd });
|
|
26
|
+
assert.ok(result, `Expected tip for "${cmd}"`);
|
|
27
|
+
assert.ok(result.includes("looks like a shell command"));
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("does not fire for commands already prefixed with !", () => {
|
|
32
|
+
const tips = new ContextualTips();
|
|
33
|
+
const result = tips.evaluate({ ...baseCtx, input: "!ls -la" });
|
|
34
|
+
assert.equal(result, null);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("does not fire for commands prefixed with !!", () => {
|
|
38
|
+
const tips = new ContextualTips();
|
|
39
|
+
const result = tips.evaluate({ ...baseCtx, input: "!!ls -la" });
|
|
40
|
+
assert.equal(result, null);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("does not fire for slash commands", () => {
|
|
44
|
+
const tips = new ContextualTips();
|
|
45
|
+
const result = tips.evaluate({ ...baseCtx, input: "/clear" });
|
|
46
|
+
assert.equal(result, null);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("does not fire for unknown commands", () => {
|
|
50
|
+
const tips = new ContextualTips();
|
|
51
|
+
const result = tips.evaluate({ ...baseCtx, input: "please help me fix this bug" });
|
|
52
|
+
assert.equal(result, null);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("does not fire for very long inputs", () => {
|
|
56
|
+
const tips = new ContextualTips();
|
|
57
|
+
const longInput = "ls " + "a".repeat(200);
|
|
58
|
+
const result = tips.evaluate({ ...baseCtx, input: longInput });
|
|
59
|
+
assert.equal(result, null);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("respects maxShows (2)", () => {
|
|
63
|
+
const tips = new ContextualTips();
|
|
64
|
+
tips.evaluate({ ...baseCtx, input: "ls" });
|
|
65
|
+
tips.evaluate({ ...baseCtx, input: "pwd" });
|
|
66
|
+
const third = tips.evaluate({ ...baseCtx, input: "cat foo" });
|
|
67
|
+
assert.equal(third, null);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("large-paste tip", () => {
|
|
72
|
+
it("fires for large inputs", () => {
|
|
73
|
+
const tips = new ContextualTips();
|
|
74
|
+
const largeInput = "a".repeat(2500);
|
|
75
|
+
const result = tips.evaluate({ ...baseCtx, input: largeInput });
|
|
76
|
+
assert.ok(result);
|
|
77
|
+
assert.ok(result.includes("Large inputs"));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("does not fire for normal-length inputs", () => {
|
|
81
|
+
const tips = new ContextualTips();
|
|
82
|
+
const result = tips.evaluate({ ...baseCtx, input: "fix the login bug" });
|
|
83
|
+
assert.equal(result, null);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("does not fire for large bash commands", () => {
|
|
87
|
+
const tips = new ContextualTips();
|
|
88
|
+
const result = tips.evaluate({ ...baseCtx, input: "!" + "a".repeat(2500) });
|
|
89
|
+
assert.equal(result, null);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("respects maxShows (2)", () => {
|
|
93
|
+
const tips = new ContextualTips();
|
|
94
|
+
const large = "x".repeat(3000);
|
|
95
|
+
tips.evaluate({ ...baseCtx, input: large });
|
|
96
|
+
tips.evaluate({ ...baseCtx, input: large });
|
|
97
|
+
const third = tips.evaluate({ ...baseCtx, input: large });
|
|
98
|
+
assert.equal(third, null);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("thinking-level-high tip", () => {
|
|
103
|
+
it("fires for short inputs with high thinking", () => {
|
|
104
|
+
const tips = new ContextualTips();
|
|
105
|
+
const result = tips.evaluate({ ...baseCtx, input: "what is 2+2?", thinkingLevel: "high" });
|
|
106
|
+
assert.ok(result);
|
|
107
|
+
assert.ok(result.includes("Thinking is set to high"));
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("fires for xhigh thinking", () => {
|
|
111
|
+
const tips = new ContextualTips();
|
|
112
|
+
const result = tips.evaluate({ ...baseCtx, input: "what time is it?", thinkingLevel: "xhigh" });
|
|
113
|
+
assert.ok(result);
|
|
114
|
+
assert.ok(result.includes("Thinking is set to xhigh"));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("does not fire for low/medium thinking", () => {
|
|
118
|
+
const tips = new ContextualTips();
|
|
119
|
+
const result = tips.evaluate({ ...baseCtx, input: "what is 2+2?", thinkingLevel: "medium" });
|
|
120
|
+
assert.equal(result, null);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("does not fire for long inputs", () => {
|
|
124
|
+
const tips = new ContextualTips();
|
|
125
|
+
const longInput = "Please help me refactor this entire authentication module to use JWT tokens instead of session cookies. " +
|
|
126
|
+
"I need to update the middleware, the login handler, and the user model.";
|
|
127
|
+
const result = tips.evaluate({ ...baseCtx, input: longInput, thinkingLevel: "high" });
|
|
128
|
+
assert.equal(result, null);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("does not fire for slash commands", () => {
|
|
132
|
+
const tips = new ContextualTips();
|
|
133
|
+
const result = tips.evaluate({ ...baseCtx, input: "/model", thinkingLevel: "high" });
|
|
134
|
+
assert.equal(result, null);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("respects maxShows (1)", () => {
|
|
138
|
+
const tips = new ContextualTips();
|
|
139
|
+
tips.evaluate({ ...baseCtx, input: "hi", thinkingLevel: "high" });
|
|
140
|
+
const second = tips.evaluate({ ...baseCtx, input: "hello", thinkingLevel: "high" });
|
|
141
|
+
assert.equal(second, null);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("double-bang-reminder tip", () => {
|
|
146
|
+
it("fires after 3+ included bash commands", () => {
|
|
147
|
+
const tips = new ContextualTips();
|
|
148
|
+
tips.recordBashIncluded();
|
|
149
|
+
tips.recordBashIncluded();
|
|
150
|
+
tips.recordBashIncluded();
|
|
151
|
+
const result = tips.evaluate({ ...baseCtx, input: "!ls" });
|
|
152
|
+
assert.ok(result);
|
|
153
|
+
assert.ok(result.includes("!!"));
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("does not fire with fewer than 3 included commands", () => {
|
|
157
|
+
const tips = new ContextualTips();
|
|
158
|
+
tips.recordBashIncluded();
|
|
159
|
+
tips.recordBashIncluded();
|
|
160
|
+
const result = tips.evaluate({ ...baseCtx, input: "!ls" });
|
|
161
|
+
assert.equal(result, null);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("does not fire for !! commands", () => {
|
|
165
|
+
const tips = new ContextualTips();
|
|
166
|
+
tips.recordBashIncluded();
|
|
167
|
+
tips.recordBashIncluded();
|
|
168
|
+
tips.recordBashIncluded();
|
|
169
|
+
const result = tips.evaluate({ ...baseCtx, input: "!!ls" });
|
|
170
|
+
assert.equal(result, null);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("respects maxShows (2)", () => {
|
|
174
|
+
const tips = new ContextualTips();
|
|
175
|
+
for (let i = 0; i < 5; i++) tips.recordBashIncluded();
|
|
176
|
+
tips.evaluate({ ...baseCtx, input: "!ls" });
|
|
177
|
+
tips.evaluate({ ...baseCtx, input: "!pwd" });
|
|
178
|
+
const third = tips.evaluate({ ...baseCtx, input: "!cat foo" });
|
|
179
|
+
assert.equal(third, null);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("compaction-nudge tip", () => {
|
|
184
|
+
it("fires when context is >= 70%", () => {
|
|
185
|
+
const tips = new ContextualTips();
|
|
186
|
+
const result = tips.evaluate({ ...baseCtx, input: "fix the bug", contextPercent: 75 });
|
|
187
|
+
assert.ok(result);
|
|
188
|
+
assert.ok(result.includes("/compact"));
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("does not fire when context is < 70%", () => {
|
|
192
|
+
const tips = new ContextualTips();
|
|
193
|
+
const result = tips.evaluate({ ...baseCtx, input: "fix the bug", contextPercent: 50 });
|
|
194
|
+
assert.equal(result, null);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("does not fire when contextPercent is undefined", () => {
|
|
198
|
+
const tips = new ContextualTips();
|
|
199
|
+
const result = tips.evaluate({ ...baseCtx, input: "fix the bug", contextPercent: undefined });
|
|
200
|
+
assert.equal(result, null);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("does not fire for slash commands", () => {
|
|
204
|
+
const tips = new ContextualTips();
|
|
205
|
+
const result = tips.evaluate({ ...baseCtx, input: "/model", contextPercent: 90 });
|
|
206
|
+
assert.equal(result, null);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("respects maxShows (1)", () => {
|
|
210
|
+
const tips = new ContextualTips();
|
|
211
|
+
tips.evaluate({ ...baseCtx, input: "hello", contextPercent: 80 });
|
|
212
|
+
const second = tips.evaluate({ ...baseCtx, input: "world", contextPercent: 85 });
|
|
213
|
+
assert.equal(second, null);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("reset", () => {
|
|
218
|
+
it("resets all show counters", () => {
|
|
219
|
+
const tips = new ContextualTips();
|
|
220
|
+
// Exhaust shell-command-prefix tip
|
|
221
|
+
tips.evaluate({ ...baseCtx, input: "ls" });
|
|
222
|
+
tips.evaluate({ ...baseCtx, input: "pwd" });
|
|
223
|
+
assert.equal(tips.evaluate({ ...baseCtx, input: "cat foo" }), null);
|
|
224
|
+
|
|
225
|
+
tips.reset();
|
|
226
|
+
|
|
227
|
+
// Should fire again after reset
|
|
228
|
+
const result = tips.evaluate({ ...baseCtx, input: "ls" });
|
|
229
|
+
assert.ok(result);
|
|
230
|
+
assert.ok(result.includes("looks like a shell command"));
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("resets bash included count", () => {
|
|
234
|
+
const tips = new ContextualTips();
|
|
235
|
+
for (let i = 0; i < 5; i++) tips.recordBashIncluded();
|
|
236
|
+
assert.equal(tips.bashIncludedCount, 5);
|
|
237
|
+
|
|
238
|
+
tips.reset();
|
|
239
|
+
assert.equal(tips.bashIncludedCount, 0);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe("priority — first match wins", () => {
|
|
244
|
+
it("shell-command-prefix takes priority over compaction nudge", () => {
|
|
245
|
+
const tips = new ContextualTips();
|
|
246
|
+
const result = tips.evaluate({ ...baseCtx, input: "ls", contextPercent: 80 });
|
|
247
|
+
assert.ok(result);
|
|
248
|
+
assert.ok(result.includes("looks like a shell command"));
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("large-paste takes priority over compaction nudge", () => {
|
|
252
|
+
const tips = new ContextualTips();
|
|
253
|
+
const largeInput = "x".repeat(3000);
|
|
254
|
+
const result = tips.evaluate({ ...baseCtx, input: largeInput, contextPercent: 80 });
|
|
255
|
+
assert.ok(result);
|
|
256
|
+
assert.ok(result.includes("Large inputs"));
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
});
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contextual tips system — shows non-intrusive, session-scoped hints
|
|
3
|
+
* when user behavior suggests they'd benefit from knowing a feature.
|
|
4
|
+
*
|
|
5
|
+
* Each tip fires at most `maxShows` times per session. Tips are
|
|
6
|
+
* evaluated in order; the first match wins per input event.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ─── Tip definitions ─────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
export interface TipContext {
|
|
12
|
+
/** The raw input text the user submitted */
|
|
13
|
+
input: string;
|
|
14
|
+
/** Whether the agent is currently streaming */
|
|
15
|
+
isStreaming: boolean;
|
|
16
|
+
/** Current thinking level (e.g. "off", "low", "high", "xhigh") */
|
|
17
|
+
thinkingLevel?: string;
|
|
18
|
+
/** Number of `!` (included) bash commands run this session */
|
|
19
|
+
bashIncludedCount: number;
|
|
20
|
+
/** Approximate context usage percentage (0–100), if known */
|
|
21
|
+
contextPercent?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface Tip {
|
|
25
|
+
id: string;
|
|
26
|
+
/** Maximum times this tip is shown per session */
|
|
27
|
+
maxShows: number;
|
|
28
|
+
/** Returns the tip message if the tip should fire, or null to skip */
|
|
29
|
+
evaluate: (ctx: TipContext) => string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Shell commands that obviously run locally and don't need the LLM.
|
|
33
|
+
// Intentionally conservative — these are unambiguous filesystem/info commands.
|
|
34
|
+
const LOCAL_SHELL_COMMANDS = new Set([
|
|
35
|
+
"ls",
|
|
36
|
+
"ll",
|
|
37
|
+
"la",
|
|
38
|
+
"pwd",
|
|
39
|
+
"cd",
|
|
40
|
+
"dir",
|
|
41
|
+
"cat",
|
|
42
|
+
"head",
|
|
43
|
+
"tail",
|
|
44
|
+
"wc",
|
|
45
|
+
"file",
|
|
46
|
+
"which",
|
|
47
|
+
"whoami",
|
|
48
|
+
"echo",
|
|
49
|
+
"date",
|
|
50
|
+
"tree",
|
|
51
|
+
"find",
|
|
52
|
+
"grep",
|
|
53
|
+
"rg",
|
|
54
|
+
"clear",
|
|
55
|
+
"env",
|
|
56
|
+
"df",
|
|
57
|
+
"du",
|
|
58
|
+
"uname",
|
|
59
|
+
"hostname",
|
|
60
|
+
"mkdir",
|
|
61
|
+
"rm",
|
|
62
|
+
"cp",
|
|
63
|
+
"mv",
|
|
64
|
+
"touch",
|
|
65
|
+
"chmod",
|
|
66
|
+
"less",
|
|
67
|
+
"more",
|
|
68
|
+
"sort",
|
|
69
|
+
"uniq",
|
|
70
|
+
"sed",
|
|
71
|
+
"awk",
|
|
72
|
+
"curl",
|
|
73
|
+
"wget",
|
|
74
|
+
"tar",
|
|
75
|
+
"zip",
|
|
76
|
+
"unzip",
|
|
77
|
+
"git",
|
|
78
|
+
"docker",
|
|
79
|
+
"npm",
|
|
80
|
+
"npx",
|
|
81
|
+
"yarn",
|
|
82
|
+
"pnpm",
|
|
83
|
+
"node",
|
|
84
|
+
"python",
|
|
85
|
+
"python3",
|
|
86
|
+
"pip",
|
|
87
|
+
"pip3",
|
|
88
|
+
"make",
|
|
89
|
+
"cargo",
|
|
90
|
+
"go",
|
|
91
|
+
"ruby",
|
|
92
|
+
"brew",
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Extract the first token from input, ignoring leading whitespace.
|
|
97
|
+
* Returns lowercase for case-insensitive matching.
|
|
98
|
+
*/
|
|
99
|
+
function firstToken(input: string): string {
|
|
100
|
+
const trimmed = input.trimStart();
|
|
101
|
+
const spaceIdx = trimmed.search(/\s/);
|
|
102
|
+
const token = spaceIdx === -1 ? trimmed : trimmed.slice(0, spaceIdx);
|
|
103
|
+
return token.toLowerCase();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check if input looks like a bare shell command (no !, //, or slash prefix).
|
|
108
|
+
*/
|
|
109
|
+
function looksLikeShellCommand(input: string): boolean {
|
|
110
|
+
const trimmed = input.trimStart();
|
|
111
|
+
// Already prefixed — user knows what they're doing
|
|
112
|
+
if (trimmed.startsWith("!") || trimmed.startsWith("/")) return false;
|
|
113
|
+
// Multi-line or very long inputs are probably prompts
|
|
114
|
+
if (trimmed.includes("\n") || trimmed.length > 120) return false;
|
|
115
|
+
return LOCAL_SHELL_COMMANDS.has(firstToken(trimmed));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const TIPS: Tip[] = [
|
|
119
|
+
// 1. Shell command reminder
|
|
120
|
+
{
|
|
121
|
+
id: "shell-command-prefix",
|
|
122
|
+
maxShows: 2,
|
|
123
|
+
evaluate(ctx) {
|
|
124
|
+
if (!looksLikeShellCommand(ctx.input)) return null;
|
|
125
|
+
const cmd = firstToken(ctx.input);
|
|
126
|
+
return `Tip: "${cmd}" looks like a shell command. Prefix with ! to run locally, or !! to run without using tokens.`;
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
// 2. Large paste warning
|
|
131
|
+
{
|
|
132
|
+
id: "large-paste",
|
|
133
|
+
maxShows: 2,
|
|
134
|
+
evaluate(ctx) {
|
|
135
|
+
if (ctx.input.length < 2000) return null;
|
|
136
|
+
// Slash commands and bash prefixes are intentional
|
|
137
|
+
if (ctx.input.trimStart().startsWith("/") || ctx.input.trimStart().startsWith("!")) return null;
|
|
138
|
+
return "Tip: Large inputs consume many tokens. Consider saving to a file and asking the agent to read it.";
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
// 3. Thinking level awareness
|
|
143
|
+
{
|
|
144
|
+
id: "thinking-level-high",
|
|
145
|
+
maxShows: 1,
|
|
146
|
+
evaluate(ctx) {
|
|
147
|
+
const level = ctx.thinkingLevel?.toLowerCase();
|
|
148
|
+
if (level !== "high" && level !== "xhigh") return null;
|
|
149
|
+
// Only fire for short, simple-looking inputs (likely simple questions)
|
|
150
|
+
const trimmed = ctx.input.trim();
|
|
151
|
+
if (trimmed.length > 80 || trimmed.includes("\n")) return null;
|
|
152
|
+
// Don't fire on slash or bash commands
|
|
153
|
+
if (trimmed.startsWith("/") || trimmed.startsWith("!")) return null;
|
|
154
|
+
return `Tip: Thinking is set to ${level}. Use Ctrl+T to lower it for simple questions — saves tokens.`;
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
// 4. Double-bang reminder
|
|
159
|
+
{
|
|
160
|
+
id: "double-bang-reminder",
|
|
161
|
+
maxShows: 2,
|
|
162
|
+
evaluate(ctx) {
|
|
163
|
+
// Fire after user has run 3+ included (!) bash commands
|
|
164
|
+
if (ctx.bashIncludedCount < 3) return null;
|
|
165
|
+
// Only trigger on a ! command (not !!)
|
|
166
|
+
const trimmed = ctx.input.trimStart();
|
|
167
|
+
if (!trimmed.startsWith("!") || trimmed.startsWith("!!")) return null;
|
|
168
|
+
return "Tip: Use !! instead of ! to keep command output out of agent context and save tokens.";
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
// 5. Compaction nudge
|
|
173
|
+
{
|
|
174
|
+
id: "compaction-nudge",
|
|
175
|
+
maxShows: 1,
|
|
176
|
+
evaluate(ctx) {
|
|
177
|
+
if (ctx.contextPercent === undefined || ctx.contextPercent < 70) return null;
|
|
178
|
+
// Don't nag on slash/bash
|
|
179
|
+
const trimmed = ctx.input.trimStart();
|
|
180
|
+
if (trimmed.startsWith("/") || trimmed.startsWith("!")) return null;
|
|
181
|
+
return "Tip: Context is getting full. Use /compact to summarize the conversation and free up space.";
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
// ─── Session-scoped tracker ──────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
export class ContextualTips {
|
|
189
|
+
/** Map of tip ID → number of times shown this session */
|
|
190
|
+
private showCounts = new Map<string, number>();
|
|
191
|
+
/** Track ! bash commands for double-bang reminder */
|
|
192
|
+
private _bashIncludedCount = 0;
|
|
193
|
+
|
|
194
|
+
/** Increment the bash-included counter. Call when user runs ! (not !!) command. */
|
|
195
|
+
recordBashIncluded(): void {
|
|
196
|
+
this._bashIncludedCount++;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
get bashIncludedCount(): number {
|
|
200
|
+
return this._bashIncludedCount;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Evaluate all tips against the current input context.
|
|
205
|
+
* Returns the first matching tip message, or null if none apply.
|
|
206
|
+
*/
|
|
207
|
+
evaluate(ctx: Omit<TipContext, "bashIncludedCount">): string | null {
|
|
208
|
+
const fullCtx: TipContext = {
|
|
209
|
+
...ctx,
|
|
210
|
+
bashIncludedCount: this._bashIncludedCount,
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
for (const tip of TIPS) {
|
|
214
|
+
const shown = this.showCounts.get(tip.id) ?? 0;
|
|
215
|
+
if (shown >= tip.maxShows) continue;
|
|
216
|
+
|
|
217
|
+
const message = tip.evaluate(fullCtx);
|
|
218
|
+
if (message) {
|
|
219
|
+
this.showCounts.set(tip.id, shown + 1);
|
|
220
|
+
return message;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Reset all counters (e.g. on new session). */
|
|
228
|
+
reset(): void {
|
|
229
|
+
this.showCounts.clear();
|
|
230
|
+
this._bashIncludedCount = 0;
|
|
231
|
+
}
|
|
232
|
+
}
|
package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import stripAnsi from "strip-ansi";
|
|
4
|
+
import { ToolExecutionComponent } from "../tool-execution.js";
|
|
5
|
+
import { initTheme } from "../../theme/theme.js";
|
|
6
|
+
|
|
7
|
+
initTheme("dark", false);
|
|
8
|
+
|
|
9
|
+
function renderTool(
|
|
10
|
+
toolName: string,
|
|
11
|
+
args: Record<string, unknown>,
|
|
12
|
+
result?: {
|
|
13
|
+
content: Array<{ type: string; text?: string }>;
|
|
14
|
+
isError: boolean;
|
|
15
|
+
details?: Record<string, unknown>;
|
|
16
|
+
},
|
|
17
|
+
): string {
|
|
18
|
+
const component = new ToolExecutionComponent(
|
|
19
|
+
toolName,
|
|
20
|
+
args,
|
|
21
|
+
{},
|
|
22
|
+
undefined,
|
|
23
|
+
{ requestRender() {} } as any,
|
|
24
|
+
);
|
|
25
|
+
component.setExpanded(true);
|
|
26
|
+
if (result) component.updateResult(result);
|
|
27
|
+
return stripAnsi(component.render(120).join("\n"));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("ToolExecutionComponent", () => {
|
|
31
|
+
test("renders capitalized Claude Code Bash tool names with bash output instead of generic args JSON", () => {
|
|
32
|
+
const rendered = renderTool(
|
|
33
|
+
"Bash",
|
|
34
|
+
{ command: "pwd" },
|
|
35
|
+
{ content: [{ type: "text", text: "/tmp/gsd-pr-fix" }], isError: false },
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
assert.match(rendered, /\$ pwd/);
|
|
39
|
+
assert.match(rendered, /\/tmp\/gsd-pr-fix/);
|
|
40
|
+
assert.doesNotMatch(rendered, /^\{\s*\}$/m);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("renders capitalized Claude Code Read tool names with read output", () => {
|
|
44
|
+
const rendered = renderTool(
|
|
45
|
+
"Read",
|
|
46
|
+
{ path: "/tmp/demo.txt" },
|
|
47
|
+
{ content: [{ type: "text", text: "hello\nworld" }], isError: false },
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
assert.match(rendered, /read .*demo\.txt/);
|
|
51
|
+
assert.match(rendered, /hello/);
|
|
52
|
+
assert.match(rendered, /world/);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -97,6 +97,10 @@ export class ToolExecutionComponent extends Container {
|
|
|
97
97
|
// When true, this component intentionally renders no lines
|
|
98
98
|
private hideComponent = false;
|
|
99
99
|
|
|
100
|
+
private get normalizedToolName(): string {
|
|
101
|
+
return typeof this.toolName === "string" ? this.toolName.toLowerCase() : "";
|
|
102
|
+
}
|
|
103
|
+
|
|
100
104
|
constructor(
|
|
101
105
|
toolName: string,
|
|
102
106
|
args: any,
|
|
@@ -121,7 +125,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
121
125
|
|
|
122
126
|
// Use contentBox for bash (visual truncation) or custom tools with custom renderers
|
|
123
127
|
// Use contentText for built-in tools (including overrides without custom renderers)
|
|
124
|
-
if (
|
|
128
|
+
if (this.normalizedToolName === "bash" || (toolDefinition && !this.shouldUseBuiltInRenderer())) {
|
|
125
129
|
this.addChild(this.contentBox);
|
|
126
130
|
} else {
|
|
127
131
|
this.addChild(this.contentText);
|
|
@@ -136,7 +140,8 @@ export class ToolExecutionComponent extends Container {
|
|
|
136
140
|
* or the toolDefinition doesn't provide custom renderers.
|
|
137
141
|
*/
|
|
138
142
|
private shouldUseBuiltInRenderer(): boolean {
|
|
139
|
-
const
|
|
143
|
+
const normalizedToolName = this.normalizedToolName;
|
|
144
|
+
const isBuiltInName = normalizedToolName in allTools;
|
|
140
145
|
const hasCustomRenderers = this.toolDefinition?.renderCall || this.toolDefinition?.renderResult;
|
|
141
146
|
return isBuiltInName && !hasCustomRenderers;
|
|
142
147
|
}
|
|
@@ -152,7 +157,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
152
157
|
|
|
153
158
|
updateArgs(args: any): void {
|
|
154
159
|
this.args = args;
|
|
155
|
-
if (this.
|
|
160
|
+
if (this.normalizedToolName === "write" && this.isPartial) {
|
|
156
161
|
this.updateWriteHighlightCacheIncremental();
|
|
157
162
|
}
|
|
158
163
|
this.updateDisplay();
|
|
@@ -308,7 +313,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
308
313
|
): void {
|
|
309
314
|
this.result = result;
|
|
310
315
|
this.isPartial = isPartial;
|
|
311
|
-
if (this.
|
|
316
|
+
if (this.normalizedToolName === "write" && !isPartial) {
|
|
312
317
|
const rawPath = str(this.args?.file_path ?? this.args?.path);
|
|
313
318
|
const fileContent = str(this.args?.content);
|
|
314
319
|
if (rawPath !== null && fileContent !== null) {
|
|
@@ -387,7 +392,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
387
392
|
|
|
388
393
|
// Use built-in rendering for built-in tools (or overrides without custom renderers)
|
|
389
394
|
if (useBuiltInRenderer) {
|
|
390
|
-
if (this.
|
|
395
|
+
if (this.normalizedToolName === "bash") {
|
|
391
396
|
// Bash uses Box with visual line truncation
|
|
392
397
|
this.contentBox.setBgFn(bgFn);
|
|
393
398
|
this.contentBox.clear();
|
|
@@ -629,8 +634,9 @@ export class ToolExecutionComponent extends Container {
|
|
|
629
634
|
private formatToolExecution(): string {
|
|
630
635
|
let text = "";
|
|
631
636
|
const invalidArg = theme.fg("error", "[invalid arg]");
|
|
637
|
+
const normalizedToolName = this.normalizedToolName;
|
|
632
638
|
|
|
633
|
-
if (
|
|
639
|
+
if (normalizedToolName === "read") {
|
|
634
640
|
const rawPath = str(this.args?.file_path ?? this.args?.path);
|
|
635
641
|
const path = rawPath !== null ? shortenPath(rawPath) : null;
|
|
636
642
|
const offset = this.args?.offset;
|
|
@@ -692,7 +698,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
692
698
|
}
|
|
693
699
|
}
|
|
694
700
|
}
|
|
695
|
-
} else if (
|
|
701
|
+
} else if (normalizedToolName === "write") {
|
|
696
702
|
const rawPath = str(this.args?.file_path ?? this.args?.path);
|
|
697
703
|
const fileContent = str(this.args?.content);
|
|
698
704
|
const path = rawPath !== null ? shortenPath(rawPath) : null;
|
|
@@ -751,7 +757,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
751
757
|
text += `\n\n${theme.fg("error", errorText)}`;
|
|
752
758
|
}
|
|
753
759
|
}
|
|
754
|
-
} else if (
|
|
760
|
+
} else if (normalizedToolName === "edit") {
|
|
755
761
|
const rawPath = str(this.args?.file_path ?? this.args?.path);
|
|
756
762
|
const path = rawPath !== null ? shortenPath(rawPath) : null;
|
|
757
763
|
|
|
@@ -787,7 +793,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
787
793
|
text += `\n\n${renderDiff(this.editDiffPreview.diff, { filePath: rawPath ?? undefined })}`;
|
|
788
794
|
}
|
|
789
795
|
}
|
|
790
|
-
} else if (
|
|
796
|
+
} else if (normalizedToolName === "ls") {
|
|
791
797
|
const rawPath = str(this.args?.path);
|
|
792
798
|
const path = rawPath !== null ? shortenPath(rawPath || ".") : null;
|
|
793
799
|
const limit = this.args?.limit;
|
|
@@ -824,7 +830,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
824
830
|
text += `\n${theme.fg("warning", `[Truncated: ${warnings.join(", ")}]`)}`;
|
|
825
831
|
}
|
|
826
832
|
}
|
|
827
|
-
} else if (
|
|
833
|
+
} else if (normalizedToolName === "find") {
|
|
828
834
|
const pattern = str(this.args?.pattern);
|
|
829
835
|
const rawPath = str(this.args?.path);
|
|
830
836
|
const path = rawPath !== null ? shortenPath(rawPath || ".") : null;
|
|
@@ -866,7 +872,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
866
872
|
text += `\n${theme.fg("warning", `[Truncated: ${warnings.join(", ")}]`)}`;
|
|
867
873
|
}
|
|
868
874
|
}
|
|
869
|
-
} else if (
|
|
875
|
+
} else if (normalizedToolName === "grep") {
|
|
870
876
|
const pattern = str(this.args?.pattern);
|
|
871
877
|
const rawPath = str(this.args?.path);
|
|
872
878
|
const path = rawPath !== null ? shortenPath(rawPath || ".") : null;
|
|
@@ -916,7 +922,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
916
922
|
text += `\n${theme.fg("warning", `[Truncated: ${warnings.join(", ")}]`)}`;
|
|
917
923
|
}
|
|
918
924
|
}
|
|
919
|
-
} else if (
|
|
925
|
+
} else if (normalizedToolName === "web_search") {
|
|
920
926
|
// Server-side Anthropic web search
|
|
921
927
|
text = theme.fg("toolTitle", theme.bold("web search"));
|
|
922
928
|
|
|
@@ -121,6 +121,27 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
121
121
|
if (host.streamingComponent && event.message.role === "assistant") {
|
|
122
122
|
host.streamingMessage = event.message;
|
|
123
123
|
host.streamingComponent.updateContent(host.streamingMessage);
|
|
124
|
+
|
|
125
|
+
// When the stream adapter signals a completed tool call with an
|
|
126
|
+
// external result (from Claude Code SDK), update the pending
|
|
127
|
+
// ToolExecutionComponent immediately so output is visible in
|
|
128
|
+
// real-time instead of waiting for the session to end.
|
|
129
|
+
const innerEvent = event.assistantMessageEvent;
|
|
130
|
+
if (innerEvent.type === "toolcall_end" && innerEvent.toolCall) {
|
|
131
|
+
const tc = innerEvent.toolCall as any;
|
|
132
|
+
const externalResult = tc.externalResult;
|
|
133
|
+
if (externalResult) {
|
|
134
|
+
const component = host.pendingTools.get(tc.id);
|
|
135
|
+
if (component) {
|
|
136
|
+
component.updateResult({
|
|
137
|
+
content: externalResult.content ?? [{ type: "text", text: "" }],
|
|
138
|
+
details: externalResult.details ?? {},
|
|
139
|
+
isError: externalResult.isError ?? false,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
124
145
|
const contentBlocks = host.streamingMessage.content;
|
|
125
146
|
for (let i = lastProcessedContentIndex; i < contentBlocks.length; i++) {
|
|
126
147
|
const content = contentBlocks[i];
|