gsd-pi 2.70.1 → 2.71.0-dev.06b86c6
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 +24 -17
- package/dist/cli.js +12 -3
- package/dist/mcp-server.js +6 -6
- package/dist/provider-migrations.d.ts +10 -0
- package/dist/provider-migrations.js +12 -0
- package/dist/resource-loader.js +136 -13
- package/dist/resources/GSD-WORKFLOW.md +1 -1
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +129 -30
- package/dist/resources/extensions/get-secrets-from-user.js +17 -1
- package/dist/resources/extensions/gsd/auto-start.js +4 -12
- package/dist/resources/extensions/gsd/auto-tool-tracking.js +1 -1
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +6 -0
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +6 -0
- package/dist/resources/extensions/gsd/commands/context.js +15 -6
- package/dist/resources/extensions/gsd/commands/dispatcher.js +12 -2
- package/dist/resources/extensions/gsd/custom-workflow-engine.js +16 -12
- package/dist/resources/extensions/gsd/dispatch-guard.js +18 -1
- package/dist/resources/extensions/gsd/error-classifier.js +1 -1
- package/dist/resources/extensions/gsd/file-lock.js +60 -0
- package/dist/resources/extensions/gsd/guided-flow.js +12 -10
- package/dist/resources/extensions/gsd/init-wizard.js +3 -11
- package/dist/resources/extensions/gsd/notification-store.js +21 -1
- package/dist/resources/extensions/gsd/notification-widget.js +1 -1
- package/dist/resources/extensions/gsd/pre-execution-checks.js +35 -2
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +2 -2
- package/dist/resources/extensions/gsd/prompts/discuss.md +33 -13
- package/dist/resources/extensions/gsd/prompts/execute-task.md +20 -19
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +2 -0
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +2 -0
- package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/queue.md +3 -2
- package/dist/resources/extensions/gsd/prompts/system.md +1 -0
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -1
- package/dist/resources/extensions/gsd/state.js +234 -332
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +34 -0
- package/dist/resources/extensions/gsd/workflow-events.js +25 -13
- package/dist/resources/extensions/gsd/workflow-mcp-auto-prep.js +56 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +1 -1
- package/dist/resources/skills/create-skill/SKILL.md +2 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +15 -15
- package/dist/web/standalone/.next/build-manifest.json +4 -4
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
- package/dist/web/standalone/.next/required-server-files.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
- 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.js +2 -2
- 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 +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- 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 +3 -3
- 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/api/boot/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/notifications/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/notifications/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +4 -4
- 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 +4 -4
- 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 +3 -3
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/page.js +2 -2
- package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +15 -15
- package/dist/web/standalone/.next/server/chunks/63.js +3 -3
- package/dist/web/standalone/.next/server/chunks/6897.js +1 -1
- 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/middleware.js +2 -2
- package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
- package/dist/web/standalone/.next/server/next-font-manifest.json +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.dd3dc8bbd3025fa5.js +9 -0
- package/dist/web/standalone/.next/static/chunks/app/_not-found/{page-2f24283c162b6ab3.js → page-f2a7482d42a5614b.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/{layout-9ecfd95f343793f0.js → layout-a16c7a7ecdf0c2cf.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/page-f1e30ab6bb269149.js +1 -0
- package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +1 -0
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +1 -0
- package/dist/web/standalone/.next/static/chunks/{webpack-6e4d7e9a4f57bed4.js → webpack-b868033a5834586d.js} +1 -1
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
- package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
- package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/dist/env-writer.d.ts +39 -0
- package/packages/mcp-server/dist/env-writer.d.ts.map +1 -0
- package/packages/mcp-server/dist/env-writer.js +158 -0
- package/packages/mcp-server/dist/env-writer.js.map +1 -0
- package/packages/mcp-server/dist/server.d.ts +11 -2
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +102 -2
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +21 -11
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/env-writer.test.ts +280 -0
- package/packages/mcp-server/src/env-writer.ts +183 -0
- package/packages/mcp-server/src/secure-env-collect.test.ts +265 -0
- package/packages/mcp-server/src/server.ts +137 -3
- package/packages/mcp-server/src/workflow-tools.test.ts +110 -0
- package/packages/mcp-server/src/workflow-tools.ts +31 -11
- package/packages/pi-ai/dist/providers/amazon-bedrock.js +11 -2
- package/packages/pi-ai/dist/providers/amazon-bedrock.js.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +4 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.js +8 -3
- package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.test.js +44 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.test.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js +11 -0
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/src/providers/amazon-bedrock.ts +13 -1
- package/packages/pi-ai/src/providers/anthropic-shared.test.ts +55 -1
- package/packages/pi-ai/src/providers/anthropic-shared.ts +14 -3
- package/packages/pi-ai/src/providers/openai-completions.ts +14 -0
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +388 -0
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts +19 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js +50 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.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 +168 -23
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +6 -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 +58 -2
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +468 -0
- package/packages/pi-coding-agent/src/core/extensions/types.ts +2 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts +58 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/extension-input.ts +2 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +198 -29
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +66 -2
- package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +1 -1
- package/packages/pi-coding-agent/src/modes/rpc/rpc-types.ts +1 -0
- package/packages/pi-tui/dist/components/__tests__/input.test.js +9 -0
- package/packages/pi-tui/dist/components/__tests__/input.test.js.map +1 -1
- package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.d.ts +2 -0
- package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.d.ts.map +1 -0
- package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.js +66 -0
- package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.js.map +1 -0
- package/packages/pi-tui/dist/components/input.d.ts +2 -0
- package/packages/pi-tui/dist/components/input.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/input.js +7 -4
- package/packages/pi-tui/dist/components/input.js.map +1 -1
- package/packages/pi-tui/dist/components/markdown.d.ts +3 -0
- package/packages/pi-tui/dist/components/markdown.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/markdown.js +17 -1
- package/packages/pi-tui/dist/components/markdown.js.map +1 -1
- package/packages/pi-tui/src/components/__tests__/input.test.ts +11 -0
- package/packages/pi-tui/src/components/__tests__/markdown-maxlines.test.ts +75 -0
- package/packages/pi-tui/src/components/input.ts +7 -4
- package/packages/pi-tui/src/components/markdown.ts +22 -1
- package/pkg/package.json +1 -1
- package/src/resources/GSD-WORKFLOW.md +1 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +166 -31
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +145 -0
- package/src/resources/extensions/get-secrets-from-user.ts +24 -1
- package/src/resources/extensions/gsd/auto-start.ts +4 -14
- package/src/resources/extensions/gsd/auto-tool-tracking.ts +1 -1
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +6 -0
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +7 -0
- package/src/resources/extensions/gsd/commands/context.ts +16 -5
- package/src/resources/extensions/gsd/commands/dispatcher.ts +14 -2
- package/src/resources/extensions/gsd/custom-workflow-engine.ts +19 -14
- package/src/resources/extensions/gsd/dispatch-guard.ts +18 -1
- package/src/resources/extensions/gsd/error-classifier.ts +1 -1
- package/src/resources/extensions/gsd/file-lock.ts +59 -0
- package/src/resources/extensions/gsd/guided-flow.ts +12 -9
- package/src/resources/extensions/gsd/init-wizard.ts +3 -13
- package/src/resources/extensions/gsd/notification-store.ts +19 -1
- package/src/resources/extensions/gsd/notification-widget.ts +1 -1
- package/src/resources/extensions/gsd/pre-execution-checks.ts +39 -2
- package/src/resources/extensions/gsd/prompts/complete-slice.md +2 -2
- package/src/resources/extensions/gsd/prompts/discuss.md +33 -13
- package/src/resources/extensions/gsd/prompts/execute-task.md +20 -19
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +2 -0
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +2 -0
- package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/queue.md +3 -2
- package/src/resources/extensions/gsd/prompts/system.md +1 -0
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -1
- package/src/resources/extensions/gsd/state.ts +274 -344
- package/src/resources/extensions/gsd/tests/auto-start-worktree-db-path.test.ts +28 -0
- package/src/resources/extensions/gsd/tests/bootstrap-derive-state-db-open.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/complete-slice-prompt-task-summary-layout.test.ts +18 -0
- package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +436 -0
- package/src/resources/extensions/gsd/tests/discuss-incremental-persistence.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/execute-task-prompt-existing-artifact-guard.test.ts +33 -0
- package/src/resources/extensions/gsd/tests/file-lock.test.ts +103 -0
- package/src/resources/extensions/gsd/tests/gsd-no-project-error.test.ts +73 -0
- package/src/resources/extensions/gsd/tests/notification-store.test.ts +17 -0
- package/src/resources/extensions/gsd/tests/notification-widget.test.ts +25 -0
- package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +49 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +19 -0
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +7 -0
- package/src/resources/extensions/gsd/tests/secure-env-collect.test.ts +45 -0
- package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +7 -0
- package/src/resources/extensions/gsd/tests/validate-milestone-prompt-verification-classes.test.ts +18 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp-auto-prep.test.ts +76 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +155 -1
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +22 -0
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +60 -25
- package/src/resources/extensions/gsd/workflow-events.ts +34 -25
- package/src/resources/extensions/gsd/workflow-mcp-auto-prep.ts +76 -0
- package/src/resources/extensions/gsd/workflow-mcp.ts +1 -1
- package/src/resources/skills/create-skill/SKILL.md +2 -0
- package/dist/web/standalone/.next/static/chunks/2826.821e01b07d92e948.js +0 -9
- package/dist/web/standalone/.next/static/chunks/app/page-7115e62689b5fd84.js +0 -1
- package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +0 -1
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +0 -1
- /package/dist/web/standalone/.next/static/{9pw9EXtXjdM7EFrCXUEPf → dYVdRaunb2ZSEA8fjkT-V}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{9pw9EXtXjdM7EFrCXUEPf → dYVdRaunb2ZSEA8fjkT-V}/_ssgManifest.js +0 -0
|
@@ -1,14 +1,36 @@
|
|
|
1
|
-
import { Loader, Spacer, Text } from "@gsd/pi-tui";
|
|
1
|
+
import { Loader, Markdown, Spacer, Text } from "@gsd/pi-tui";
|
|
2
2
|
|
|
3
3
|
import type { InteractiveModeEvent, InteractiveModeStateHost } from "../interactive-mode-state.js";
|
|
4
4
|
import { theme } from "../theme/theme.js";
|
|
5
5
|
import { AssistantMessageComponent } from "../components/assistant-message.js";
|
|
6
6
|
import { ToolExecutionComponent } from "../components/tool-execution.js";
|
|
7
|
+
import { DynamicBorder } from "../components/dynamic-border.js";
|
|
7
8
|
import { appKey } from "../components/keybinding-hints.js";
|
|
8
9
|
|
|
9
10
|
// Tracks the last processed content index to avoid re-scanning all blocks on every message_update
|
|
10
11
|
let lastProcessedContentIndex = 0;
|
|
11
12
|
|
|
13
|
+
function hasVisibleAssistantContent(message: { content: Array<any> }): boolean {
|
|
14
|
+
return message.content.some(
|
|
15
|
+
(c) =>
|
|
16
|
+
(c.type === "text" && typeof c.text === "string" && c.text.trim().length > 0)
|
|
17
|
+
|| (c.type === "thinking" && typeof c.thinking === "string" && c.thinking.trim().length > 0),
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function hasAssistantToolBlocks(message: { content: Array<any> }): boolean {
|
|
22
|
+
return message.content.some((c) => c.type === "toolCall" || c.type === "serverToolUse");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Tracks the latest assistant text for the pinned message zone
|
|
26
|
+
let lastPinnedText = "";
|
|
27
|
+
// Whether any tool execution has been added in this assistant turn (triggers pinned display)
|
|
28
|
+
let hasToolsInTurn = false;
|
|
29
|
+
// Reference to the pinned border so we can toggle its label between working/idle
|
|
30
|
+
let pinnedBorder: DynamicBorder | undefined;
|
|
31
|
+
// Reference to the pinned markdown component below the border
|
|
32
|
+
let pinnedTextComponent: Markdown | undefined;
|
|
33
|
+
|
|
12
34
|
export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
13
35
|
init: () => Promise<void>;
|
|
14
36
|
getMarkdownThemeWithSettings: () => any;
|
|
@@ -31,9 +53,15 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
31
53
|
|
|
32
54
|
host.footer.invalidate();
|
|
33
55
|
|
|
34
|
-
// Reset content index tracker when a new assistant message starts
|
|
56
|
+
// Reset content index tracker and pinned state when a new assistant message starts
|
|
35
57
|
if (event.type === "message_start" && event.message.role === "assistant") {
|
|
36
58
|
lastProcessedContentIndex = 0;
|
|
59
|
+
lastPinnedText = "";
|
|
60
|
+
hasToolsInTurn = false;
|
|
61
|
+
if (pinnedBorder) pinnedBorder.stopSpinner();
|
|
62
|
+
pinnedBorder = undefined;
|
|
63
|
+
pinnedTextComponent = undefined;
|
|
64
|
+
host.pinnedMessageContainer.clear();
|
|
37
65
|
}
|
|
38
66
|
|
|
39
67
|
switch (event.type) {
|
|
@@ -46,6 +74,12 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
46
74
|
host.streamingMessage = undefined;
|
|
47
75
|
host.pendingTools.clear();
|
|
48
76
|
host.pendingMessagesContainer.clear();
|
|
77
|
+
host.pinnedMessageContainer.clear();
|
|
78
|
+
lastPinnedText = "";
|
|
79
|
+
hasToolsInTurn = false;
|
|
80
|
+
if (pinnedBorder) pinnedBorder.stopSpinner();
|
|
81
|
+
pinnedBorder = undefined;
|
|
82
|
+
pinnedTextComponent = undefined;
|
|
49
83
|
host.compactionQueuedMessages = [];
|
|
50
84
|
host.rebuildChatFromMessages();
|
|
51
85
|
host.updatePendingMessagesDisplay();
|
|
@@ -104,45 +138,54 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
104
138
|
host.updatePendingMessagesDisplay();
|
|
105
139
|
host.ui.requestRender();
|
|
106
140
|
} else if (event.message.role === "assistant") {
|
|
107
|
-
host.streamingComponent = new AssistantMessageComponent(
|
|
108
|
-
undefined,
|
|
109
|
-
host.hideThinkingBlock,
|
|
110
|
-
host.getMarkdownThemeWithSettings(),
|
|
111
|
-
host.settingsManager.getTimestampFormat(),
|
|
112
|
-
);
|
|
113
141
|
host.streamingMessage = event.message;
|
|
114
|
-
|
|
115
|
-
|
|
142
|
+
// External-tool providers can stream multiple assistant turns through
|
|
143
|
+
// one response. Delay component creation until visible assistant text
|
|
144
|
+
// arrives so tool outputs keep chronological ordering.
|
|
116
145
|
host.ui.requestRender();
|
|
117
146
|
}
|
|
118
147
|
break;
|
|
119
148
|
|
|
120
149
|
case "message_update":
|
|
121
|
-
if (
|
|
150
|
+
if (event.message.role === "assistant") {
|
|
122
151
|
host.streamingMessage = event.message;
|
|
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
152
|
const innerEvent = event.assistantMessageEvent;
|
|
153
|
+
|
|
154
|
+
let externalToolResult:
|
|
155
|
+
| { toolCallId: string; content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>; details: Record<string, unknown>; isError: boolean }
|
|
156
|
+
| undefined;
|
|
130
157
|
if (innerEvent.type === "toolcall_end" && innerEvent.toolCall) {
|
|
131
158
|
const tc = innerEvent.toolCall as any;
|
|
132
|
-
const
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
159
|
+
const ext = tc.externalResult;
|
|
160
|
+
if (ext) {
|
|
161
|
+
externalToolResult = {
|
|
162
|
+
toolCallId: tc.id,
|
|
163
|
+
content: ext.content ?? [{ type: "text", text: "" }],
|
|
164
|
+
details: ext.details ?? {},
|
|
165
|
+
isError: ext.isError ?? false,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
} else if (innerEvent.type === "server_tool_use") {
|
|
169
|
+
const idx = typeof innerEvent.contentIndex === "number" ? innerEvent.contentIndex : -1;
|
|
170
|
+
const block = idx >= 0 ? (host.streamingMessage.content[idx] as any) : undefined;
|
|
171
|
+
const ext = block?.externalResult;
|
|
172
|
+
if (block?.id && ext) {
|
|
173
|
+
externalToolResult = {
|
|
174
|
+
toolCallId: block.id,
|
|
175
|
+
content: ext.content ?? [{ type: "text", text: "" }],
|
|
176
|
+
details: ext.details ?? {},
|
|
177
|
+
isError: ext.isError ?? false,
|
|
178
|
+
};
|
|
142
179
|
}
|
|
143
180
|
}
|
|
144
181
|
|
|
145
182
|
const contentBlocks = host.streamingMessage.content;
|
|
183
|
+
// Some adapters reuse a single assistant lifecycle while internally
|
|
184
|
+
// spanning multiple provider turns. When a new turn starts, content
|
|
185
|
+
// length can shrink back to 0/1; reset scan index to avoid skipping.
|
|
186
|
+
if (lastProcessedContentIndex >= contentBlocks.length) {
|
|
187
|
+
lastProcessedContentIndex = 0;
|
|
188
|
+
}
|
|
146
189
|
for (let i = lastProcessedContentIndex; i < contentBlocks.length; i++) {
|
|
147
190
|
const content = contentBlocks[i];
|
|
148
191
|
if (content.type === "toolCall") {
|
|
@@ -192,19 +235,108 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
192
235
|
}
|
|
193
236
|
}
|
|
194
237
|
}
|
|
238
|
+
|
|
239
|
+
// When the stream adapter signals a completed tool call with an
|
|
240
|
+
// external result (from Claude Code SDK), update the pending
|
|
241
|
+
// ToolExecutionComponent immediately so output is visible in
|
|
242
|
+
// real-time instead of waiting for the session to end.
|
|
243
|
+
if (externalToolResult) {
|
|
244
|
+
const component = host.pendingTools.get(externalToolResult.toolCallId);
|
|
245
|
+
if (component) {
|
|
246
|
+
component.updateResult({
|
|
247
|
+
content: externalToolResult.content,
|
|
248
|
+
details: externalToolResult.details,
|
|
249
|
+
isError: externalToolResult.isError,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Render assistant text/thinking after tool components so mixed
|
|
255
|
+
// streams keep chronological ordering in the chat container.
|
|
256
|
+
const hasToolBlocks = hasAssistantToolBlocks(host.streamingMessage);
|
|
257
|
+
if (!host.streamingComponent && hasVisibleAssistantContent(host.streamingMessage)) {
|
|
258
|
+
host.streamingComponent = new AssistantMessageComponent(
|
|
259
|
+
undefined,
|
|
260
|
+
host.hideThinkingBlock,
|
|
261
|
+
host.getMarkdownThemeWithSettings(),
|
|
262
|
+
host.settingsManager.getTimestampFormat(),
|
|
263
|
+
);
|
|
264
|
+
host.chatContainer.addChild(host.streamingComponent);
|
|
265
|
+
}
|
|
266
|
+
if (host.streamingComponent) {
|
|
267
|
+
if (hasToolBlocks) {
|
|
268
|
+
host.chatContainer.removeChild(host.streamingComponent);
|
|
269
|
+
host.chatContainer.addChild(host.streamingComponent);
|
|
270
|
+
}
|
|
271
|
+
host.streamingComponent.updateContent(host.streamingMessage);
|
|
272
|
+
}
|
|
273
|
+
|
|
195
274
|
// Update index: fully processed blocks won't need re-scanning.
|
|
196
275
|
// Keep the last block's index (it may still be accumulating data),
|
|
197
276
|
// so we re-check it next time but skip all earlier ones.
|
|
198
277
|
if (contentBlocks.length > 0) {
|
|
199
278
|
lastProcessedContentIndex = Math.max(0, contentBlocks.length - 1);
|
|
200
279
|
}
|
|
280
|
+
|
|
281
|
+
// Pinned message: mirror the latest assistant text above the editor
|
|
282
|
+
// when tool executions push it out of the viewport.
|
|
283
|
+
const hasTools = contentBlocks.some(
|
|
284
|
+
(c: any) => c.type === "toolCall" || c.type === "serverToolUse",
|
|
285
|
+
);
|
|
286
|
+
if (hasTools) hasToolsInTurn = true;
|
|
287
|
+
|
|
288
|
+
if (hasToolsInTurn) {
|
|
289
|
+
// Collect the latest text block(s) from the assistant message
|
|
290
|
+
let latestText = "";
|
|
291
|
+
for (let i = contentBlocks.length - 1; i >= 0; i--) {
|
|
292
|
+
const c = contentBlocks[i] as any;
|
|
293
|
+
if (c.type === "text" && c.text?.trim()) {
|
|
294
|
+
latestText = c.text.trim();
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (latestText && latestText !== lastPinnedText) {
|
|
300
|
+
lastPinnedText = latestText;
|
|
301
|
+
|
|
302
|
+
if (!pinnedBorder) {
|
|
303
|
+
// First time: create border + text component
|
|
304
|
+
host.pinnedMessageContainer.clear();
|
|
305
|
+
pinnedBorder = new DynamicBorder(
|
|
306
|
+
(str: string) => theme.fg("dim", str),
|
|
307
|
+
"Working · Latest Output",
|
|
308
|
+
);
|
|
309
|
+
pinnedBorder.startSpinner(host.ui, (str: string) => theme.fg("accent", str));
|
|
310
|
+
host.pinnedMessageContainer.addChild(pinnedBorder);
|
|
311
|
+
pinnedTextComponent = new Markdown(latestText, 1, 0, host.getMarkdownThemeWithSettings());
|
|
312
|
+
// Cap pinned content to ~40% of terminal height so tall output
|
|
313
|
+
// doesn't exceed the viewport and cause render flashing.
|
|
314
|
+
pinnedTextComponent.maxLines = Math.max(3, Math.floor(host.ui.terminal.rows * 0.4));
|
|
315
|
+
host.pinnedMessageContainer.addChild(pinnedTextComponent);
|
|
316
|
+
// Hide the separate status loader — the pinned zone replaces it
|
|
317
|
+
if (host.loadingAnimation) {
|
|
318
|
+
host.loadingAnimation.stop();
|
|
319
|
+
host.loadingAnimation = undefined;
|
|
320
|
+
}
|
|
321
|
+
host.statusContainer.clear();
|
|
322
|
+
} else {
|
|
323
|
+
// Update existing markdown component in-place
|
|
324
|
+
pinnedTextComponent?.setText(latestText);
|
|
325
|
+
// Refresh maxLines in case terminal was resized
|
|
326
|
+
if (pinnedTextComponent) {
|
|
327
|
+
pinnedTextComponent.maxLines = Math.max(3, Math.floor(host.ui.terminal.rows * 0.4));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
201
333
|
host.ui.requestRender();
|
|
202
334
|
}
|
|
203
335
|
break;
|
|
204
336
|
|
|
205
337
|
case "message_end":
|
|
206
338
|
if (event.message.role === "user") break;
|
|
207
|
-
if (
|
|
339
|
+
if (event.message.role === "assistant") {
|
|
208
340
|
host.streamingMessage = event.message;
|
|
209
341
|
let errorMessage: string | undefined;
|
|
210
342
|
if (host.streamingMessage.stopReason === "aborted") {
|
|
@@ -214,7 +346,25 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
214
346
|
: "Operation aborted";
|
|
215
347
|
host.streamingMessage.errorMessage = errorMessage;
|
|
216
348
|
}
|
|
217
|
-
|
|
349
|
+
|
|
350
|
+
const shouldRenderAssistant = hasVisibleAssistantContent(host.streamingMessage)
|
|
351
|
+
|| (
|
|
352
|
+
(host.streamingMessage.stopReason === "aborted" || host.streamingMessage.stopReason === "error")
|
|
353
|
+
&& !hasAssistantToolBlocks(host.streamingMessage)
|
|
354
|
+
);
|
|
355
|
+
if (!host.streamingComponent && shouldRenderAssistant) {
|
|
356
|
+
host.streamingComponent = new AssistantMessageComponent(
|
|
357
|
+
undefined,
|
|
358
|
+
host.hideThinkingBlock,
|
|
359
|
+
host.getMarkdownThemeWithSettings(),
|
|
360
|
+
host.settingsManager.getTimestampFormat(),
|
|
361
|
+
);
|
|
362
|
+
host.chatContainer.addChild(host.streamingComponent);
|
|
363
|
+
}
|
|
364
|
+
if (host.streamingComponent) {
|
|
365
|
+
host.streamingComponent.updateContent(host.streamingMessage);
|
|
366
|
+
}
|
|
367
|
+
|
|
218
368
|
if (host.streamingMessage.stopReason === "aborted" || host.streamingMessage.stopReason === "error") {
|
|
219
369
|
if (!errorMessage) {
|
|
220
370
|
errorMessage = host.streamingMessage.errorMessage || "Error";
|
|
@@ -230,6 +380,15 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
230
380
|
}
|
|
231
381
|
host.streamingComponent = undefined;
|
|
232
382
|
host.streamingMessage = undefined;
|
|
383
|
+
// Clear pinned output once the message is finalized in the chat
|
|
384
|
+
// container — prevents duplicate display when the agent continues
|
|
385
|
+
// (e.g. form elicitation) after the assistant message ends.
|
|
386
|
+
if (pinnedBorder) pinnedBorder.stopSpinner();
|
|
387
|
+
host.pinnedMessageContainer.clear();
|
|
388
|
+
lastPinnedText = "";
|
|
389
|
+
hasToolsInTurn = false;
|
|
390
|
+
pinnedBorder = undefined;
|
|
391
|
+
pinnedTextComponent = undefined;
|
|
233
392
|
host.footer.invalidate();
|
|
234
393
|
}
|
|
235
394
|
host.ui.requestRender();
|
|
@@ -282,6 +441,16 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
282
441
|
host.streamingMessage = undefined;
|
|
283
442
|
}
|
|
284
443
|
host.pendingTools.clear();
|
|
444
|
+
// Pinned output is only useful while work is actively streaming.
|
|
445
|
+
// Keep chat history as the single source after completion.
|
|
446
|
+
if (pinnedBorder) {
|
|
447
|
+
pinnedBorder.stopSpinner();
|
|
448
|
+
}
|
|
449
|
+
host.pinnedMessageContainer.clear();
|
|
450
|
+
lastPinnedText = "";
|
|
451
|
+
hasToolsInTurn = false;
|
|
452
|
+
pinnedBorder = undefined;
|
|
453
|
+
pinnedTextComponent = undefined;
|
|
285
454
|
await host.checkShutdownRequested();
|
|
286
455
|
host.ui.requestRender();
|
|
287
456
|
break;
|
|
@@ -168,6 +168,7 @@ export class InteractiveMode {
|
|
|
168
168
|
private chatContainer: Container;
|
|
169
169
|
private pendingMessagesContainer: Container;
|
|
170
170
|
private statusContainer: Container;
|
|
171
|
+
private pinnedMessageContainer: Container;
|
|
171
172
|
private defaultEditor: CustomEditor;
|
|
172
173
|
private editor: EditorComponent;
|
|
173
174
|
private autocompleteProvider: CombinedAutocompleteProvider | undefined;
|
|
@@ -285,6 +286,7 @@ export class InteractiveMode {
|
|
|
285
286
|
this.chatContainer = new Container();
|
|
286
287
|
this.pendingMessagesContainer = new Container();
|
|
287
288
|
this.statusContainer = new Container();
|
|
289
|
+
this.pinnedMessageContainer = new Container();
|
|
288
290
|
this.widgetContainerAbove = new Container();
|
|
289
291
|
this.widgetContainerBelow = new Container();
|
|
290
292
|
this.keybindings = KeybindingsManager.create();
|
|
@@ -490,6 +492,7 @@ export class InteractiveMode {
|
|
|
490
492
|
this.ui.addChild(this.chatContainer);
|
|
491
493
|
this.ui.addChild(this.pendingMessagesContainer);
|
|
492
494
|
this.ui.addChild(this.statusContainer);
|
|
495
|
+
this.ui.addChild(this.pinnedMessageContainer);
|
|
493
496
|
this.renderWidgets(); // Initialize with default spacer
|
|
494
497
|
this.ui.addChild(this.widgetContainerAbove);
|
|
495
498
|
this.ui.addChild(this.editorContainer);
|
|
@@ -1396,7 +1399,19 @@ export class InteractiveMode {
|
|
|
1396
1399
|
*/
|
|
1397
1400
|
private renderWidgets(): void {
|
|
1398
1401
|
if (!this.widgetContainerAbove || !this.widgetContainerBelow) return;
|
|
1399
|
-
|
|
1402
|
+
|
|
1403
|
+
// widgetContainerAbove: spacer collapses when pinned content is visible
|
|
1404
|
+
// so there's no extra blank line between pinned output and the editor border.
|
|
1405
|
+
this.widgetContainerAbove.clear();
|
|
1406
|
+
const pinned = this.pinnedMessageContainer;
|
|
1407
|
+
this.widgetContainerAbove.addChild({
|
|
1408
|
+
render: () => pinned.children.length > 0 ? [] : [""],
|
|
1409
|
+
invalidate: () => {},
|
|
1410
|
+
});
|
|
1411
|
+
for (const component of this.extensionWidgetsAbove.values()) {
|
|
1412
|
+
this.widgetContainerAbove.addChild(component);
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1400
1415
|
this.renderWidgetContainer(this.widgetContainerBelow, this.extensionWidgetsBelow, false, false);
|
|
1401
1416
|
this.ui.requestRender();
|
|
1402
1417
|
}
|
|
@@ -1631,7 +1646,7 @@ export class InteractiveMode {
|
|
|
1631
1646
|
this.hideExtensionInput();
|
|
1632
1647
|
resolve(undefined);
|
|
1633
1648
|
},
|
|
1634
|
-
{ tui: this.ui, timeout: opts?.timeout },
|
|
1649
|
+
{ tui: this.ui, timeout: opts?.timeout, secure: opts?.secure },
|
|
1635
1650
|
);
|
|
1636
1651
|
|
|
1637
1652
|
this.editorContainer.clear();
|
|
@@ -2264,6 +2279,7 @@ export class InteractiveMode {
|
|
|
2264
2279
|
updateFooter: true,
|
|
2265
2280
|
populateHistory: true,
|
|
2266
2281
|
});
|
|
2282
|
+
this.populatePinnedFromMessages(context.messages);
|
|
2267
2283
|
|
|
2268
2284
|
// Show compaction info if session was compacted
|
|
2269
2285
|
const allEntries = this.sessionManager.getEntries();
|
|
@@ -2287,6 +2303,54 @@ export class InteractiveMode {
|
|
|
2287
2303
|
this.chatContainer.clear();
|
|
2288
2304
|
const context = this.sessionManager.buildSessionContext();
|
|
2289
2305
|
this.renderSessionContext(context);
|
|
2306
|
+
this.populatePinnedFromMessages(context.messages);
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
/**
|
|
2310
|
+
* After rebuilding chat from messages, pin the last assistant text above the
|
|
2311
|
+
* editor if tool results would otherwise push it out of the viewport.
|
|
2312
|
+
*/
|
|
2313
|
+
private populatePinnedFromMessages(messages: AgentMessage[]): void {
|
|
2314
|
+
this.pinnedMessageContainer.clear();
|
|
2315
|
+
|
|
2316
|
+
// Walk backwards to find the last assistant message
|
|
2317
|
+
let lastAssistant: AssistantMessage | undefined;
|
|
2318
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2319
|
+
const msg = messages[i];
|
|
2320
|
+
if (msg && "role" in msg && msg.role === "assistant") {
|
|
2321
|
+
lastAssistant = msg as AssistantMessage;
|
|
2322
|
+
break;
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
if (!lastAssistant) return;
|
|
2326
|
+
|
|
2327
|
+
// Check if any tool calls follow the last text block
|
|
2328
|
+
const content = lastAssistant.content;
|
|
2329
|
+
let lastTextIndex = -1;
|
|
2330
|
+
let hasToolAfterText = false;
|
|
2331
|
+
for (let i = 0; i < content.length; i++) {
|
|
2332
|
+
if (content[i].type === "text") lastTextIndex = i;
|
|
2333
|
+
}
|
|
2334
|
+
if (lastTextIndex >= 0) {
|
|
2335
|
+
for (let i = lastTextIndex + 1; i < content.length; i++) {
|
|
2336
|
+
if (content[i].type === "toolCall" || content[i].type === "serverToolUse") {
|
|
2337
|
+
hasToolAfterText = true;
|
|
2338
|
+
break;
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
if (!hasToolAfterText || lastTextIndex < 0) return;
|
|
2343
|
+
|
|
2344
|
+
const textBlock = content[lastTextIndex] as { type: "text"; text: string };
|
|
2345
|
+
const text = textBlock.text?.trim();
|
|
2346
|
+
if (!text) return;
|
|
2347
|
+
|
|
2348
|
+
this.pinnedMessageContainer.addChild(
|
|
2349
|
+
new DynamicBorder((str: string) => theme.fg("dim", str), "Latest Output"),
|
|
2350
|
+
);
|
|
2351
|
+
this.pinnedMessageContainer.addChild(
|
|
2352
|
+
new Markdown(text, 1, 0, this.getMarkdownThemeWithSettings()),
|
|
2353
|
+
);
|
|
2290
2354
|
}
|
|
2291
2355
|
|
|
2292
2356
|
// =========================================================================
|
|
@@ -224,7 +224,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
224
224
|
),
|
|
225
225
|
|
|
226
226
|
input: (title, placeholder, opts) =>
|
|
227
|
-
createDialogPromise(opts, undefined, { method: "input", title, placeholder, timeout: opts?.timeout }, (r) =>
|
|
227
|
+
createDialogPromise(opts, undefined, { method: "input", title, placeholder, timeout: opts?.timeout, secure: opts?.secure }, (r) =>
|
|
228
228
|
"cancelled" in r && r.cancelled ? undefined : "value" in r ? r.value : undefined,
|
|
229
229
|
),
|
|
230
230
|
|
|
@@ -25,5 +25,14 @@ describe("Input", () => {
|
|
|
25
25
|
input.focused = false;
|
|
26
26
|
assert.equal(input.focused, false);
|
|
27
27
|
});
|
|
28
|
+
it("secure mode obscures typed characters in render output", () => {
|
|
29
|
+
const input = new Input();
|
|
30
|
+
input.secure = true;
|
|
31
|
+
input.focused = true;
|
|
32
|
+
input.handleInput("secret123");
|
|
33
|
+
const line = input.render(40)[0] ?? "";
|
|
34
|
+
assert.ok(!line.includes("secret123"), "rendered line must not expose raw secret text");
|
|
35
|
+
assert.ok(line.includes("*********"), "rendered line should include masked characters");
|
|
36
|
+
});
|
|
28
37
|
});
|
|
29
38
|
//# sourceMappingURL=input.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"input.test.js","sourceRoot":"","sources":["../../../src/components/__tests__/input.test.ts"],"names":[],"mappings":"AAAA,0CAA0C;AAC1C,4DAA4D;AAE5D,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEpC,QAAQ,CAAC,OAAO,EAAE,GAAG,EAAE;IACtB,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;QACrD,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAC1B,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC;QAErB,yDAAyD;QACzD,KAAK,CAAC,WAAW,CAAC,kBAAkB,CAAC,CAAC;QAEtC,2BAA2B;QAC3B,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC;QAEtB,mDAAmD;QACnD,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC;QAErB,iEAAiE;QACjE,KAAK,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QAC3B,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,OAAO,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAChD,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAC1B,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACnC,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC;QACrB,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QAClC,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC;QACtB,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC","sourcesContent":["// pi-tui Input component regression tests\n// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>\n\nimport { describe, it } from \"node:test\";\nimport assert from \"node:assert/strict\";\nimport { Input } from \"../input.js\";\n\ndescribe(\"Input\", () => {\n\tit(\"paste buffer is cleared when focus is lost\", () => {\n\t\tconst input = new Input();\n\t\tinput.focused = true;\n\n\t\t// Simulate starting a paste (bracket paste start marker)\n\t\tinput.handleInput(\"\\x1b[200~partial\");\n\n\t\t// Now lose focus mid-paste\n\t\tinput.focused = false;\n\n\t\t// Regain focus — should not have stale paste state\n\t\tinput.focused = true;\n\n\t\t// Typing normal text should work without paste buffer corruption\n\t\tinput.handleInput(\"hello\");\n\t\tassert.equal(input.getValue(), \"hello\");\n\t});\n\n\tit(\"focused getter/setter works correctly\", () => {\n\t\tconst input = new Input();\n\t\tassert.equal(input.focused, false);\n\t\tinput.focused = true;\n\t\tassert.equal(input.focused, true);\n\t\tinput.focused = false;\n\t\tassert.equal(input.focused, false);\n\t});\n});\n"]}
|
|
1
|
+
{"version":3,"file":"input.test.js","sourceRoot":"","sources":["../../../src/components/__tests__/input.test.ts"],"names":[],"mappings":"AAAA,0CAA0C;AAC1C,4DAA4D;AAE5D,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEpC,QAAQ,CAAC,OAAO,EAAE,GAAG,EAAE;IACtB,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;QACrD,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAC1B,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC;QAErB,yDAAyD;QACzD,KAAK,CAAC,WAAW,CAAC,kBAAkB,CAAC,CAAC;QAEtC,2BAA2B;QAC3B,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC;QAEtB,mDAAmD;QACnD,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC;QAErB,iEAAiE;QACjE,KAAK,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QAC3B,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,OAAO,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAChD,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAC1B,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACnC,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC;QACrB,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QAClC,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC;QACtB,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wDAAwD,EAAE,GAAG,EAAE;QACjE,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAC1B,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC;QACpB,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC;QACrB,KAAK,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC;QAE/B,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACvC,MAAM,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,+CAA+C,CAAC,CAAC;QACxF,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,gDAAgD,CAAC,CAAC;IACzF,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC","sourcesContent":["// pi-tui Input component regression tests\n// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>\n\nimport { describe, it } from \"node:test\";\nimport assert from \"node:assert/strict\";\nimport { Input } from \"../input.js\";\n\ndescribe(\"Input\", () => {\n\tit(\"paste buffer is cleared when focus is lost\", () => {\n\t\tconst input = new Input();\n\t\tinput.focused = true;\n\n\t\t// Simulate starting a paste (bracket paste start marker)\n\t\tinput.handleInput(\"\\x1b[200~partial\");\n\n\t\t// Now lose focus mid-paste\n\t\tinput.focused = false;\n\n\t\t// Regain focus — should not have stale paste state\n\t\tinput.focused = true;\n\n\t\t// Typing normal text should work without paste buffer corruption\n\t\tinput.handleInput(\"hello\");\n\t\tassert.equal(input.getValue(), \"hello\");\n\t});\n\n\tit(\"focused getter/setter works correctly\", () => {\n\t\tconst input = new Input();\n\t\tassert.equal(input.focused, false);\n\t\tinput.focused = true;\n\t\tassert.equal(input.focused, true);\n\t\tinput.focused = false;\n\t\tassert.equal(input.focused, false);\n\t});\n\n\tit(\"secure mode obscures typed characters in render output\", () => {\n\t\tconst input = new Input();\n\t\tinput.secure = true;\n\t\tinput.focused = true;\n\t\tinput.handleInput(\"secret123\");\n\n\t\tconst line = input.render(40)[0] ?? \"\";\n\t\tassert.ok(!line.includes(\"secret123\"), \"rendered line must not expose raw secret text\");\n\t\tassert.ok(line.includes(\"*********\"), \"rendered line should include masked characters\");\n\t});\n});\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown-maxlines.test.d.ts","sourceRoot":"","sources":["../../../src/components/__tests__/markdown-maxlines.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { Markdown } from "../markdown.js";
|
|
4
|
+
function noopTheme() {
|
|
5
|
+
const identity = (text) => text;
|
|
6
|
+
return {
|
|
7
|
+
heading: identity,
|
|
8
|
+
link: identity,
|
|
9
|
+
linkUrl: identity,
|
|
10
|
+
code: identity,
|
|
11
|
+
codeBlock: identity,
|
|
12
|
+
codeBlockBorder: identity,
|
|
13
|
+
quote: identity,
|
|
14
|
+
quoteBorder: identity,
|
|
15
|
+
hr: identity,
|
|
16
|
+
listBullet: identity,
|
|
17
|
+
bold: identity,
|
|
18
|
+
italic: identity,
|
|
19
|
+
strikethrough: identity,
|
|
20
|
+
underline: identity,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
test("Markdown renders all lines when maxLines is not set", () => {
|
|
24
|
+
const text = "Line 1\n\nLine 2\n\nLine 3\n\nLine 4\n\nLine 5";
|
|
25
|
+
const md = new Markdown(text, 0, 0, noopTheme());
|
|
26
|
+
const lines = md.render(80);
|
|
27
|
+
// Each paragraph produces a line + an inter-paragraph blank line
|
|
28
|
+
const contentLines = lines.filter((l) => l.trim().length > 0);
|
|
29
|
+
assert.ok(contentLines.length >= 5, `expected at least 5 content lines, got ${contentLines.length}`);
|
|
30
|
+
});
|
|
31
|
+
test("Markdown truncates from the top when maxLines is exceeded", () => {
|
|
32
|
+
const text = "Line 1\n\nLine 2\n\nLine 3\n\nLine 4\n\nLine 5";
|
|
33
|
+
const md = new Markdown(text, 0, 0, noopTheme());
|
|
34
|
+
md.maxLines = 3;
|
|
35
|
+
const lines = md.render(80);
|
|
36
|
+
assert.ok(lines.length <= 3, `expected at most 3 lines, got ${lines.length}`);
|
|
37
|
+
// First line should be the ellipsis indicator
|
|
38
|
+
assert.ok(lines[0].includes("…"), "first line should contain ellipsis indicator");
|
|
39
|
+
assert.ok(lines[0].includes("above"), "first line should mention lines above");
|
|
40
|
+
});
|
|
41
|
+
test("Markdown preserves most recent content when truncating", () => {
|
|
42
|
+
const text = "First paragraph\n\nSecond paragraph\n\nThird paragraph\n\nFourth paragraph\n\nFifth paragraph";
|
|
43
|
+
const md = new Markdown(text, 0, 0, noopTheme());
|
|
44
|
+
md.maxLines = 3;
|
|
45
|
+
const lines = md.render(80);
|
|
46
|
+
// The last rendered line should contain "Fifth paragraph" (the most recent content)
|
|
47
|
+
const lastContentLine = lines.filter((l) => !l.includes("…")).pop() ?? "";
|
|
48
|
+
assert.ok(lastContentLine.includes("Fifth paragraph"), `expected last content line to contain "Fifth paragraph", got "${lastContentLine}"`);
|
|
49
|
+
});
|
|
50
|
+
test("Markdown does not truncate when content fits within maxLines", () => {
|
|
51
|
+
const text = "Short text";
|
|
52
|
+
const md = new Markdown(text, 0, 0, noopTheme());
|
|
53
|
+
md.maxLines = 10;
|
|
54
|
+
const lines = md.render(80);
|
|
55
|
+
assert.ok(!lines.some((l) => l.includes("…")), "should not contain ellipsis when content fits");
|
|
56
|
+
assert.ok(lines.some((l) => l.includes("Short text")), "should contain the original text");
|
|
57
|
+
});
|
|
58
|
+
test("Markdown trims trailing empty lines", () => {
|
|
59
|
+
const text = "Some text\n\n";
|
|
60
|
+
const md = new Markdown(text, 0, 0, noopTheme());
|
|
61
|
+
const lines = md.render(80);
|
|
62
|
+
// Last line should not be empty (trailing empties are trimmed)
|
|
63
|
+
const lastLine = lines[lines.length - 1];
|
|
64
|
+
assert.ok(lastLine.trim().length > 0 || lines.length === 1, "trailing empty lines should be trimmed");
|
|
65
|
+
});
|
|
66
|
+
//# sourceMappingURL=markdown-maxlines.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown-maxlines.test.js","sourceRoot":"","sources":["../../../src/components/__tests__/markdown-maxlines.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\";\nimport { test } from \"node:test\";\n\nimport { Markdown, type MarkdownTheme } from \"../markdown.js\";\n\nfunction noopTheme(): MarkdownTheme {\n\tconst identity = (text: string) => text;\n\treturn {\n\t\theading: identity,\n\t\tlink: identity,\n\t\tlinkUrl: identity,\n\t\tcode: identity,\n\t\tcodeBlock: identity,\n\t\tcodeBlockBorder: identity,\n\t\tquote: identity,\n\t\tquoteBorder: identity,\n\t\thr: identity,\n\t\tlistBullet: identity,\n\t\tbold: identity,\n\t\titalic: identity,\n\t\tstrikethrough: identity,\n\t\tunderline: identity,\n\t};\n}\n\ntest(\"Markdown renders all lines when maxLines is not set\", () => {\n\tconst text = \"Line 1\\n\\nLine 2\\n\\nLine 3\\n\\nLine 4\\n\\nLine 5\";\n\tconst md = new Markdown(text, 0, 0, noopTheme());\n\tconst lines = md.render(80);\n\t// Each paragraph produces a line + an inter-paragraph blank line\n\tconst contentLines = lines.filter((l) => l.trim().length > 0);\n\tassert.ok(contentLines.length >= 5, `expected at least 5 content lines, got ${contentLines.length}`);\n});\n\ntest(\"Markdown truncates from the top when maxLines is exceeded\", () => {\n\tconst text = \"Line 1\\n\\nLine 2\\n\\nLine 3\\n\\nLine 4\\n\\nLine 5\";\n\tconst md = new Markdown(text, 0, 0, noopTheme());\n\tmd.maxLines = 3;\n\tconst lines = md.render(80);\n\tassert.ok(lines.length <= 3, `expected at most 3 lines, got ${lines.length}`);\n\t// First line should be the ellipsis indicator\n\tassert.ok(lines[0].includes(\"…\"), \"first line should contain ellipsis indicator\");\n\tassert.ok(lines[0].includes(\"above\"), \"first line should mention lines above\");\n});\n\ntest(\"Markdown preserves most recent content when truncating\", () => {\n\tconst text = \"First paragraph\\n\\nSecond paragraph\\n\\nThird paragraph\\n\\nFourth paragraph\\n\\nFifth paragraph\";\n\tconst md = new Markdown(text, 0, 0, noopTheme());\n\tmd.maxLines = 3;\n\tconst lines = md.render(80);\n\t// The last rendered line should contain \"Fifth paragraph\" (the most recent content)\n\tconst lastContentLine = lines.filter((l) => !l.includes(\"…\")).pop() ?? \"\";\n\tassert.ok(\n\t\tlastContentLine.includes(\"Fifth paragraph\"),\n\t\t`expected last content line to contain \"Fifth paragraph\", got \"${lastContentLine}\"`,\n\t);\n});\n\ntest(\"Markdown does not truncate when content fits within maxLines\", () => {\n\tconst text = \"Short text\";\n\tconst md = new Markdown(text, 0, 0, noopTheme());\n\tmd.maxLines = 10;\n\tconst lines = md.render(80);\n\tassert.ok(!lines.some((l) => l.includes(\"…\")), \"should not contain ellipsis when content fits\");\n\tassert.ok(lines.some((l) => l.includes(\"Short text\")), \"should contain the original text\");\n});\n\ntest(\"Markdown trims trailing empty lines\", () => {\n\tconst text = \"Some text\\n\\n\";\n\tconst md = new Markdown(text, 0, 0, noopTheme());\n\tconst lines = md.render(80);\n\t// Last line should not be empty (trailing empties are trimmed)\n\tconst lastLine = lines[lines.length - 1];\n\tassert.ok(lastLine.trim().length > 0 || lines.length === 1, \"trailing empty lines should be trimmed\");\n});\n"]}
|
|
@@ -8,6 +8,8 @@ export declare class Input implements Component, Focusable {
|
|
|
8
8
|
onSubmit?: (value: string) => void;
|
|
9
9
|
onEscape?: () => void;
|
|
10
10
|
placeholder: string;
|
|
11
|
+
/** When true, render obscured characters instead of the actual value. */
|
|
12
|
+
secure: boolean;
|
|
11
13
|
/** Focusable interface - set by TUI when focus changes */
|
|
12
14
|
private _focused;
|
|
13
15
|
get focused(): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../../src/components/input.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,SAAS,EAAiB,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AAW1E;;GAEG;AACH,qBAAa,KAAM,YAAW,SAAS,EAAE,SAAS;IACjD,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,MAAM,CAAa;IACpB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,WAAW,EAAE,MAAM,CAAM;
|
|
1
|
+
{"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../../src/components/input.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,SAAS,EAAiB,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AAW1E;;GAEG;AACH,qBAAa,KAAM,YAAW,SAAS,EAAE,SAAS;IACjD,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,MAAM,CAAa;IACpB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,WAAW,EAAE,MAAM,CAAM;IAChC,yEAAyE;IAClE,MAAM,EAAE,OAAO,CAAS;IAE/B,0DAA0D;IAC1D,OAAO,CAAC,QAAQ,CAAkB;IAClC,IAAI,OAAO,IAAI,OAAO,CAErB;IACD,IAAI,OAAO,CAAC,KAAK,EAAE,OAAO,EAMzB;IAGD,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,SAAS,CAAkB;IAGnC,OAAO,CAAC,QAAQ,CAAkB;IAClC,OAAO,CAAC,UAAU,CAA8C;IAGhE,OAAO,CAAC,SAAS,CAA+B;IAEhD,QAAQ,IAAI,MAAM;IAIlB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAK7B,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAqK/B,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,eAAe;IAavB,OAAO,CAAC,mBAAmB;IAY3B,OAAO,CAAC,iBAAiB;IAUzB,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,mBAAmB;IAqB3B,OAAO,CAAC,iBAAiB;IAoBzB,OAAO,CAAC,IAAI;IAWZ,OAAO,CAAC,OAAO;IAkBf,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,IAAI;IAQZ,OAAO,CAAC,iBAAiB;IAkCzB,OAAO,CAAC,gBAAgB;IAmCxB,OAAO,CAAC,WAAW;IAYnB,UAAU,IAAI,IAAI;IAIlB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE;CAkG/B"}
|
|
@@ -13,6 +13,8 @@ export class Input {
|
|
|
13
13
|
this.value = "";
|
|
14
14
|
this.cursor = 0; // Cursor position in the value
|
|
15
15
|
this.placeholder = "";
|
|
16
|
+
/** When true, render obscured characters instead of the actual value. */
|
|
17
|
+
this.secure = false;
|
|
16
18
|
/** Focusable interface - set by TUI when focus changes */
|
|
17
19
|
this._focused = false;
|
|
18
20
|
// Bracketed paste mode buffering
|
|
@@ -376,6 +378,7 @@ export class Input {
|
|
|
376
378
|
// Calculate visible window
|
|
377
379
|
const prompt = "> ";
|
|
378
380
|
const availableWidth = width - prompt.length;
|
|
381
|
+
const renderValue = this.secure ? "*".repeat(this.value.length) : this.value;
|
|
379
382
|
if (availableWidth <= 0) {
|
|
380
383
|
return [prompt];
|
|
381
384
|
}
|
|
@@ -392,7 +395,7 @@ export class Input {
|
|
|
392
395
|
let cursorDisplay = this.cursor;
|
|
393
396
|
if (this.value.length < availableWidth) {
|
|
394
397
|
// Everything fits (leave room for cursor at end)
|
|
395
|
-
visibleText =
|
|
398
|
+
visibleText = renderValue;
|
|
396
399
|
}
|
|
397
400
|
else {
|
|
398
401
|
// Need horizontal scrolling
|
|
@@ -425,19 +428,19 @@ export class Input {
|
|
|
425
428
|
};
|
|
426
429
|
if (this.cursor < halfWidth) {
|
|
427
430
|
// Cursor near start
|
|
428
|
-
visibleText =
|
|
431
|
+
visibleText = renderValue.slice(0, findValidEnd(scrollWidth));
|
|
429
432
|
cursorDisplay = this.cursor;
|
|
430
433
|
}
|
|
431
434
|
else if (this.cursor > this.value.length - halfWidth) {
|
|
432
435
|
// Cursor near end
|
|
433
436
|
const start = findValidStart(this.value.length - scrollWidth);
|
|
434
|
-
visibleText =
|
|
437
|
+
visibleText = renderValue.slice(start);
|
|
435
438
|
cursorDisplay = this.cursor - start;
|
|
436
439
|
}
|
|
437
440
|
else {
|
|
438
441
|
// Cursor in middle
|
|
439
442
|
const start = findValidStart(this.cursor - halfWidth);
|
|
440
|
-
visibleText =
|
|
443
|
+
visibleText = renderValue.slice(start, findValidEnd(start + scrollWidth));
|
|
441
444
|
cursorDisplay = halfWidth;
|
|
442
445
|
}
|
|
443
446
|
}
|