@oh-my-pi/pi-coding-agent 15.10.10 → 15.10.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +95 -4
- package/dist/cli.js +23087 -0
- package/dist/tokenizers.linux-x64-gnu-xcjh3jwk.node +0 -0
- package/dist/types/async/job-manager.d.ts +18 -0
- package/dist/types/cli/args.d.ts +1 -1
- package/dist/types/cli/dry-balance-cli.d.ts +1 -1
- package/dist/types/cli/gallery-cli.d.ts +1 -1
- package/dist/types/cli/gallery-fixtures/types.d.ts +1 -1
- package/dist/types/cli/usage-cli.d.ts +72 -0
- package/dist/types/commands/launch.d.ts +1 -1
- package/dist/types/commands/read.d.ts +1 -1
- package/dist/types/commands/usage.d.ts +25 -0
- package/dist/types/config/append-only-context-mode.d.ts +2 -1
- package/dist/types/config/model-discovery.d.ts +55 -0
- package/dist/types/config/model-registry.d.ts +7 -219
- package/dist/types/config/model-resolver.d.ts +16 -10
- package/dist/types/config/model-roles.d.ts +28 -0
- package/dist/types/config/models-config-schema.d.ts +523 -42
- package/dist/types/config/models-config.d.ts +385 -0
- package/dist/types/config/settings-schema.d.ts +12 -7
- package/dist/types/config/settings.d.ts +1 -1
- package/dist/types/debug/log-viewer.d.ts +1 -1
- package/dist/types/debug/raw-sse.d.ts +1 -1
- package/dist/types/eval/backend.d.ts +0 -2
- package/dist/types/eval/idle-timeout.d.ts +0 -4
- package/dist/types/eval/js/shared/rewrite-imports.d.ts +6 -6
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/extensions/types.d.ts +3 -3
- package/dist/types/hindsight/mental-models.d.ts +17 -8
- package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
- package/dist/types/internal-urls/types.d.ts +1 -1
- package/dist/types/lsp/edits.d.ts +9 -0
- package/dist/types/lsp/index.d.ts +2 -2
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/lsp/utils.d.ts +3 -0
- package/dist/types/mcp/json-rpc.d.ts +5 -0
- package/dist/types/mnemopi/state.d.ts +11 -1
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/bash-execution.d.ts +1 -1
- package/dist/types/modes/components/copy-selector.d.ts +1 -1
- package/dist/types/modes/components/dynamic-border.d.ts +1 -1
- package/dist/types/modes/components/extensions/extension-dashboard.d.ts +1 -1
- package/dist/types/modes/components/extensions/extension-list.d.ts +1 -1
- package/dist/types/modes/components/extensions/inspector-panel.d.ts +1 -1
- package/dist/types/modes/components/footer.d.ts +1 -1
- package/dist/types/modes/components/hook-editor.d.ts +5 -0
- package/dist/types/modes/components/hook-input.d.ts +4 -0
- package/dist/types/modes/components/hook-selector.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +1 -1
- package/dist/types/modes/components/plan-review-overlay.d.ts +1 -1
- package/dist/types/modes/components/session-observer-overlay.d.ts +1 -1
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/status-line/component.d.ts +1 -1
- package/dist/types/modes/components/tiny-title-download-progress.d.ts +1 -1
- package/dist/types/modes/components/transcript-container.d.ts +25 -6
- package/dist/types/modes/components/tree-selector.d.ts +1 -1
- package/dist/types/modes/components/user-message-selector.d.ts +1 -1
- package/dist/types/modes/components/user-message.d.ts +2 -1
- package/dist/types/modes/components/visual-truncate.d.ts +1 -1
- package/dist/types/modes/components/welcome.d.ts +19 -3
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/streaming-reveal.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/types.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
- package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +1 -1
- package/dist/types/modes/types.d.ts +2 -1
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/auth-broker-config.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +1 -1
- package/dist/types/slash-commands/helpers/stats-dashboard.d.ts +13 -0
- package/dist/types/ssh/connection-manager.d.ts +8 -0
- package/dist/types/task/parallel.d.ts +2 -2
- package/dist/types/task/worktree.d.ts +2 -0
- package/dist/types/tools/ask.d.ts +4 -0
- package/dist/types/tools/conflict-detect.d.ts +16 -0
- package/dist/types/tools/github-cache.d.ts +7 -0
- package/dist/types/tools/sqlite-reader.d.ts +3 -0
- package/dist/types/tui/output-block.d.ts +3 -3
- package/dist/types/utils/changelog.d.ts +8 -0
- package/dist/types/web/scrapers/readthedocs.d.ts +3 -0
- package/dist/types/web/scrapers/types.d.ts +12 -0
- package/dist/types/web/search/providers/codex.d.ts +1 -1
- package/dist/types/web/search/providers/gemini.d.ts +1 -1
- package/examples/extensions/tools.ts +5 -4
- package/package.json +14 -11
- package/scripts/build-binary.ts +18 -23
- package/scripts/bundle-dist.ts +81 -0
- package/scripts/{dev-launch → omp} +1 -1
- package/scripts/{dev-launch-preload.ts → omp.ts} +1 -1
- package/src/async/job-manager.ts +57 -3
- package/src/autoresearch/dashboard.ts +1 -1
- package/src/autoresearch/prompt-setup.md +6 -6
- package/src/autoresearch/prompt.md +6 -6
- package/src/capability/fs.ts +10 -0
- package/src/cli/args.ts +1 -1
- package/src/cli/auth-gateway-cli.ts +1 -3
- package/src/cli/dry-balance-cli.ts +1 -1
- package/src/cli/gallery-cli.ts +1 -1
- package/src/cli/gallery-fixtures/fs.ts +1 -1
- package/src/cli/gallery-fixtures/types.ts +5 -1
- package/src/cli/list-models.ts +2 -1
- package/src/cli/usage-cli.ts +603 -0
- package/src/cli-commands.ts +1 -0
- package/src/cli.ts +69 -5
- package/src/commands/complete.ts +1 -1
- package/src/commands/launch.ts +1 -1
- package/src/commands/read.ts +6 -3
- package/src/commands/usage.ts +35 -0
- package/src/commit/agentic/agent.ts +1 -1
- package/src/commit/model-selection.ts +1 -1
- package/src/config/append-only-context-mode.ts +6 -12
- package/src/config/model-discovery.ts +554 -0
- package/src/config/model-registry.ts +231 -1019
- package/src/config/model-resolver.ts +113 -156
- package/src/config/model-roles.ts +74 -0
- package/src/config/models-config-schema.ts +57 -8
- package/src/config/models-config.ts +129 -0
- package/src/config/settings-schema.ts +18 -4
- package/src/config/settings.ts +37 -1
- package/src/dap/client.ts +124 -37
- package/src/dap/session.ts +259 -158
- package/src/debug/log-viewer.ts +1 -1
- package/src/debug/raw-sse.ts +1 -1
- package/src/edit/diff.ts +47 -3
- package/src/edit/hashline/block-resolver.ts +20 -1
- package/src/edit/hashline/diff.ts +36 -1
- package/src/edit/hashline/execute.ts +8 -2
- package/src/edit/index.ts +16 -1
- package/src/edit/modes/patch.ts +52 -0
- package/src/edit/modes/replace.ts +56 -22
- package/src/edit/notebook.ts +22 -2
- package/src/edit/renderer.ts +36 -10
- package/src/eval/__tests__/completion-bridge.test.ts +1 -1
- package/src/eval/backend.ts +0 -2
- package/src/eval/completion-bridge.ts +2 -1
- package/src/eval/idle-timeout.ts +2 -9
- package/src/eval/js/context-manager.ts +6 -8
- package/src/eval/js/executor.ts +6 -2
- package/src/eval/js/index.ts +0 -2
- package/src/eval/js/shared/helpers.ts +5 -6
- package/src/eval/js/shared/local-module-loader.ts +1 -1
- package/src/eval/js/shared/prelude.txt +62 -1
- package/src/eval/js/shared/rewrite-imports.ts +40 -22
- package/src/eval/js/shared/runtime.ts +1 -1
- package/src/eval/py/index.ts +0 -2
- package/src/eval/py/kernel.ts +19 -0
- package/src/eval/py/runner.py +107 -3
- package/src/exec/bash-executor.ts +3 -1
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +3 -1
- package/src/extensibility/extensions/types.ts +3 -2
- package/src/extensibility/plugins/legacy-pi-compat.ts +20 -3
- package/src/hindsight/mental-models.ts +59 -12
- package/src/hindsight/state.ts +6 -1
- package/src/internal-urls/artifact-protocol.ts +11 -2
- package/src/internal-urls/docs-index.generated.ts +8 -8
- package/src/internal-urls/issue-pr-protocol.ts +12 -5
- package/src/internal-urls/router.ts +1 -1
- package/src/internal-urls/types.ts +1 -1
- package/src/lib/xai-http.ts +1 -1
- package/src/lsp/client.ts +118 -38
- package/src/lsp/clients/biome-client.ts +101 -39
- package/src/lsp/edits.ts +143 -95
- package/src/lsp/index.ts +31 -22
- package/src/lsp/render.ts +1 -1
- package/src/lsp/types.ts +2 -0
- package/src/lsp/utils.ts +28 -10
- package/src/main.ts +165 -17
- package/src/mcp/json-rpc.ts +35 -5
- package/src/mcp/transports/stdio.ts +7 -1
- package/src/memories/index.ts +2 -1
- package/src/mnemopi/backend.ts +25 -3
- package/src/mnemopi/state.ts +38 -2
- package/src/modes/components/agent-dashboard.ts +10 -7
- package/src/modes/components/assistant-message.ts +19 -13
- package/src/modes/components/bash-execution.ts +1 -1
- package/src/modes/components/copy-selector.ts +1 -1
- package/src/modes/components/diff.ts +13 -2
- package/src/modes/components/dynamic-border.ts +12 -3
- package/src/modes/components/extensions/extension-dashboard.ts +8 -5
- package/src/modes/components/extensions/extension-list.ts +1 -1
- package/src/modes/components/extensions/inspector-panel.ts +1 -1
- package/src/modes/components/footer.ts +1 -1
- package/src/modes/components/history-search.ts +1 -1
- package/src/modes/components/hook-editor.ts +8 -0
- package/src/modes/components/hook-input.ts +8 -0
- package/src/modes/components/hook-selector.ts +2 -2
- package/src/modes/components/model-selector.ts +4 -2
- package/src/modes/components/plan-review-overlay.ts +1 -1
- package/src/modes/components/session-observer-overlay.ts +2 -2
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/settings-selector.ts +5 -1
- package/src/modes/components/status-line/component.ts +1 -1
- package/src/modes/components/tiny-title-download-progress.ts +1 -1
- package/src/modes/components/transcript-container.ts +258 -53
- package/src/modes/components/tree-selector.ts +3 -3
- package/src/modes/components/user-message-selector.ts +1 -1
- package/src/modes/components/user-message.ts +17 -5
- package/src/modes/components/visual-truncate.ts +1 -1
- package/src/modes/components/welcome.ts +108 -26
- package/src/modes/controllers/command-controller.ts +10 -3
- package/src/modes/controllers/event-controller.ts +73 -4
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/controllers/mcp-command-controller.ts +1 -1
- package/src/modes/controllers/selector-controller.ts +1 -1
- package/src/modes/controllers/streaming-reveal.ts +85 -18
- package/src/modes/interactive-mode.ts +3 -9
- package/src/modes/setup-wizard/scenes/glyph.ts +1 -1
- package/src/modes/setup-wizard/scenes/providers.ts +1 -1
- package/src/modes/setup-wizard/scenes/sign-in.ts +1 -1
- package/src/modes/setup-wizard/scenes/theme.ts +1 -1
- package/src/modes/setup-wizard/scenes/types.ts +1 -1
- package/src/modes/setup-wizard/scenes/web-search.ts +1 -1
- package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
- package/src/modes/types.ts +2 -1
- package/src/prompts/agents/explore.md +2 -2
- package/src/prompts/agents/librarian.md +1 -2
- package/src/prompts/agents/oracle.md +1 -1
- package/src/prompts/agents/plan.md +5 -5
- package/src/prompts/agents/task.md +5 -5
- package/src/prompts/ci-green-request.md +5 -7
- package/src/prompts/goals/goal-budget-limit.md +2 -2
- package/src/prompts/goals/goal-continuation.md +4 -4
- package/src/prompts/goals/goal-mode-active.md +1 -1
- package/src/prompts/memories/read-path.md +1 -1
- package/src/prompts/memories/stage_one_system.md +2 -2
- package/src/prompts/review-custom-request.md +1 -1
- package/src/prompts/system/agent-creation-architect.md +2 -2
- package/src/prompts/system/auto-continue.md +1 -1
- package/src/prompts/system/background-tan-dispatch.md +1 -1
- package/src/prompts/system/btw-user.md +2 -2
- package/src/prompts/system/commit-message-system.md +13 -1
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/eager-todo.md +2 -2
- package/src/prompts/system/irc-incoming.md +1 -1
- package/src/prompts/system/manual-continue.md +1 -1
- package/src/prompts/system/omfg-user.md +3 -4
- package/src/prompts/system/orchestrate-notice.md +9 -9
- package/src/prompts/system/plan-mode-active.md +4 -4
- package/src/prompts/system/plan-mode-subagent.md +4 -5
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
- package/src/prompts/system/project-prompt.md +2 -2
- package/src/prompts/system/subagent-system-prompt.md +4 -4
- package/src/prompts/system/system-prompt.md +13 -24
- package/src/prompts/system/title-system.md +2 -2
- package/src/prompts/system/ttsr-tool-reminder.md +1 -1
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +2 -2
- package/src/prompts/tools/bash.md +5 -7
- package/src/prompts/tools/browser.md +7 -7
- package/src/prompts/tools/debug.md +1 -1
- package/src/prompts/tools/eval.md +3 -3
- package/src/prompts/tools/find.md +0 -1
- package/src/prompts/tools/github.md +8 -7
- package/src/prompts/tools/goal.md +1 -1
- package/src/prompts/tools/image-gen.md +1 -1
- package/src/prompts/tools/inspect-image-system.md +1 -1
- package/src/prompts/tools/irc.md +15 -15
- package/src/prompts/tools/lsp.md +2 -2
- package/src/prompts/tools/patch.md +2 -2
- package/src/prompts/tools/read.md +3 -4
- package/src/prompts/tools/recall.md +1 -1
- package/src/prompts/tools/reflect.md +1 -1
- package/src/prompts/tools/render-mermaid.md +2 -2
- package/src/prompts/tools/replace.md +4 -10
- package/src/prompts/tools/rewind.md +2 -2
- package/src/prompts/tools/search-tool-bm25.md +1 -9
- package/src/prompts/tools/search.md +0 -1
- package/src/prompts/tools/ssh.md +0 -4
- package/src/prompts/tools/task.md +2 -3
- package/src/prompts/tools/todo.md +1 -1
- package/src/sdk.ts +23 -10
- package/src/session/agent-session.ts +44 -10
- package/src/session/auth-broker-config.ts +30 -1
- package/src/session/session-manager.ts +2 -2
- package/src/session/streaming-output.ts +23 -2
- package/src/slash-commands/builtin-registry.ts +20 -0
- package/src/slash-commands/helpers/stats-dashboard.ts +85 -0
- package/src/ssh/connection-manager.ts +27 -0
- package/src/task/commands.ts +2 -1
- package/src/task/executor.ts +61 -53
- package/src/task/index.ts +137 -60
- package/src/task/parallel.ts +3 -3
- package/src/task/render.ts +2 -2
- package/src/task/worktree.ts +64 -56
- package/src/thinking.ts +2 -1
- package/src/tiny/title-client.ts +26 -11
- package/src/tools/archive-reader.ts +30 -2
- package/src/tools/ask.ts +104 -21
- package/src/tools/ast-edit.ts +25 -5
- package/src/tools/auto-generated-guard.ts +20 -3
- package/src/tools/bash-interactive.ts +27 -7
- package/src/tools/bash.ts +54 -13
- package/src/tools/browser/launch.ts +11 -2
- package/src/tools/browser/readable.ts +19 -2
- package/src/tools/browser/registry.ts +4 -1
- package/src/tools/browser/render.ts +2 -2
- package/src/tools/browser/tab-supervisor.ts +55 -16
- package/src/tools/conflict-detect.ts +50 -4
- package/src/tools/debug.ts +1 -1
- package/src/tools/eval-render.ts +5 -5
- package/src/tools/eval.ts +0 -2
- package/src/tools/fetch.ts +33 -10
- package/src/tools/gh-cache-invalidation.ts +63 -8
- package/src/tools/gh-renderer.ts +1 -1
- package/src/tools/gh.ts +172 -29
- package/src/tools/github-cache.ts +70 -6
- package/src/tools/image-gen.ts +3 -9
- package/src/tools/irc.ts +5 -1
- package/src/tools/job.ts +1 -1
- package/src/tools/read.ts +202 -61
- package/src/tools/render-utils.ts +3 -3
- package/src/tools/resolve.ts +1 -1
- package/src/tools/search.ts +92 -29
- package/src/tools/sqlite-reader.ts +17 -5
- package/src/tools/ssh.ts +8 -8
- package/src/tools/todo.ts +38 -8
- package/src/tools/write.ts +118 -18
- package/src/tui/output-block.ts +4 -4
- package/src/utils/changelog.ts +27 -1
- package/src/utils/file-mentions.ts +2 -1
- package/src/web/scrapers/arxiv.ts +1 -1
- package/src/web/scrapers/go-pkg.ts +1 -1
- package/src/web/scrapers/iacr.ts +1 -1
- package/src/web/scrapers/readthedocs.ts +1 -1
- package/src/web/scrapers/twitter.ts +2 -1
- package/src/web/scrapers/types.ts +87 -8
- package/src/web/scrapers/wikipedia.ts +1 -1
- package/src/web/scrapers/youtube.ts +6 -1
- package/src/web/search/index.ts +1 -1
- package/src/web/search/providers/codex.ts +2 -1
- package/src/web/search/providers/gemini.ts +2 -3
- package/src/web/search/render.ts +8 -6
- package/dist/types/config/model-equivalence.d.ts +0 -24
- package/dist/types/config/model-id-affixes.d.ts +0 -12
- package/dist/types/config/model-provider-priority.d.ts +0 -1
- package/dist/types/exec/idle-timeout-watchdog.d.ts +0 -18
- package/src/config/model-equivalence.ts +0 -875
- package/src/config/model-id-affixes.ts +0 -81
- package/src/config/model-provider-priority.ts +0 -56
- package/src/exec/idle-timeout-watchdog.ts +0 -126
|
@@ -17,6 +17,24 @@ const TIPS: readonly string[] = tipsText
|
|
|
17
17
|
.map(line => line.trim())
|
|
18
18
|
.filter(line => line.length > 0);
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Tip chosen once per process so the pre-TUI startup splash and the in-TUI
|
|
22
|
+
* welcome screen show the same tip instead of shuffling on the swap.
|
|
23
|
+
*/
|
|
24
|
+
const PROCESS_TIP: string | undefined = TIPS.length > 0 ? TIPS[Math.floor(Math.random() * TIPS.length)] : undefined;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Fixed number of session rows in the welcome box so its height doesn't shift
|
|
28
|
+
* between the pre-TUI splash (loading placeholder) and the loaded state.
|
|
29
|
+
*/
|
|
30
|
+
export const WELCOME_SESSION_SLOTS = 4;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Fixed number of LSP-server rows, for the same reason. Overflow is sliced so
|
|
34
|
+
* the box height is constant regardless of how many servers a project has.
|
|
35
|
+
*/
|
|
36
|
+
export const WELCOME_LSP_SLOTS = 4;
|
|
37
|
+
|
|
20
38
|
export function renderWelcomeTip(tip: string, boxWidth: number): string[] {
|
|
21
39
|
const label = "Tip: ";
|
|
22
40
|
const labelWidth = visibleWidth(label);
|
|
@@ -48,7 +66,7 @@ export interface RecentSession {
|
|
|
48
66
|
|
|
49
67
|
export interface LspServerInfo {
|
|
50
68
|
name: string;
|
|
51
|
-
status: "ready" | "error" | "connecting";
|
|
69
|
+
status: "ready" | "error" | "connecting" | "available";
|
|
52
70
|
fileTypes: string[];
|
|
53
71
|
}
|
|
54
72
|
|
|
@@ -58,18 +76,38 @@ export interface LspServerInfo {
|
|
|
58
76
|
export class WelcomeComponent implements Component {
|
|
59
77
|
#animStart: number | null = null;
|
|
60
78
|
#animTimer: ReturnType<typeof setInterval> | null = null;
|
|
61
|
-
/**
|
|
62
|
-
|
|
79
|
+
/** When set, a non-animating render shows the intro's first frame instead of the resting frame. */
|
|
80
|
+
#holdIntroFirstFrame = false;
|
|
81
|
+
/** Per-process tip so re-renders (intro, LSP updates, splash swap) don't shuffle it. */
|
|
82
|
+
readonly #tip: string | undefined = PROCESS_TIP;
|
|
83
|
+
// Render cache: the welcome box is the first transcript-area component, so
|
|
84
|
+
// returning a stable array reference keeps the whole frame prefix stable.
|
|
85
|
+
// Bypassed while the intro animation runs (every frame differs).
|
|
86
|
+
#cachedWidth = -1;
|
|
87
|
+
#cachedLines: string[] | undefined;
|
|
63
88
|
|
|
64
89
|
constructor(
|
|
65
90
|
private readonly version: string,
|
|
66
91
|
private modelName: string,
|
|
67
92
|
private providerName: string,
|
|
68
|
-
private recentSessions: RecentSession[] = [],
|
|
93
|
+
private recentSessions: RecentSession[] | null = [],
|
|
69
94
|
private lspServers: LspServerInfo[] = [],
|
|
70
95
|
) {}
|
|
71
96
|
|
|
72
|
-
invalidate(): void {
|
|
97
|
+
invalidate(): void {
|
|
98
|
+
this.#cachedWidth = -1;
|
|
99
|
+
this.#cachedLines = undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Freeze the logo on the intro animation's first frame. The pre-TUI startup
|
|
104
|
+
* splash uses this so the in-TUI intro — which starts at that exact frame —
|
|
105
|
+
* picks up seamlessly from the splash's static box.
|
|
106
|
+
*/
|
|
107
|
+
holdIntroFirstFrame(): void {
|
|
108
|
+
this.#holdIntroFirstFrame = true;
|
|
109
|
+
this.invalidate();
|
|
110
|
+
}
|
|
73
111
|
|
|
74
112
|
/**
|
|
75
113
|
* Play a one-shot intro that sweeps the gradient through every phase
|
|
@@ -78,6 +116,7 @@ export class WelcomeComponent implements Component {
|
|
|
78
116
|
*/
|
|
79
117
|
playIntro(requestRender: () => void): void {
|
|
80
118
|
this.#stopAnimation();
|
|
119
|
+
this.#holdIntroFirstFrame = false;
|
|
81
120
|
this.#animStart = performance.now();
|
|
82
121
|
requestRender();
|
|
83
122
|
this.#animTimer = setInterval(() => {
|
|
@@ -95,22 +134,43 @@ export class WelcomeComponent implements Component {
|
|
|
95
134
|
this.#animTimer = null;
|
|
96
135
|
}
|
|
97
136
|
this.#animStart = null;
|
|
137
|
+
// The settled (resting) frame differs from the last intro frame.
|
|
138
|
+
this.invalidate();
|
|
98
139
|
}
|
|
99
140
|
|
|
100
141
|
setModel(modelName: string, providerName: string): void {
|
|
101
142
|
this.modelName = modelName;
|
|
102
143
|
this.providerName = providerName;
|
|
144
|
+
this.invalidate();
|
|
103
145
|
}
|
|
104
146
|
|
|
105
147
|
setRecentSessions(sessions: RecentSession[]): void {
|
|
106
148
|
this.recentSessions = sessions;
|
|
149
|
+
this.invalidate();
|
|
107
150
|
}
|
|
108
151
|
|
|
109
152
|
setLspServers(servers: LspServerInfo[]): void {
|
|
110
153
|
this.lspServers = servers;
|
|
154
|
+
this.invalidate();
|
|
111
155
|
}
|
|
112
156
|
|
|
113
|
-
render(termWidth: number): string[] {
|
|
157
|
+
render(termWidth: number): readonly string[] {
|
|
158
|
+
const animating = this.#animStart != null;
|
|
159
|
+
if (!animating && this.#cachedLines && this.#cachedWidth === termWidth) {
|
|
160
|
+
return this.#cachedLines;
|
|
161
|
+
}
|
|
162
|
+
const lines = this.#renderLines(termWidth);
|
|
163
|
+
if (animating) {
|
|
164
|
+
this.#cachedLines = undefined;
|
|
165
|
+
this.#cachedWidth = -1;
|
|
166
|
+
} else {
|
|
167
|
+
this.#cachedLines = lines;
|
|
168
|
+
this.#cachedWidth = termWidth;
|
|
169
|
+
}
|
|
170
|
+
return lines;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#renderLines(termWidth: number): string[] {
|
|
114
174
|
// Box dimensions - responsive with max width and small-terminal support
|
|
115
175
|
const maxWidth = 100;
|
|
116
176
|
const boxWidth = Math.min(maxWidth, Math.max(0, termWidth - 2));
|
|
@@ -157,7 +217,9 @@ export class WelcomeComponent implements Component {
|
|
|
157
217
|
|
|
158
218
|
// Recent sessions content
|
|
159
219
|
const sessionLines: string[] = [];
|
|
160
|
-
if (this.recentSessions
|
|
220
|
+
if (this.recentSessions === null) {
|
|
221
|
+
sessionLines.push(` ${theme.fg("dim", "Loading…")}`);
|
|
222
|
+
} else if (this.recentSessions.length === 0) {
|
|
161
223
|
sessionLines.push(` ${theme.fg("dim", "No recent sessions")}`);
|
|
162
224
|
} else {
|
|
163
225
|
// Reserve width for the bullet prefix (" • ") and the trailing " (timeAgo)"
|
|
@@ -165,7 +227,7 @@ export class WelcomeComponent implements Component {
|
|
|
165
227
|
// absorbs whatever space is left.
|
|
166
228
|
const bulletPrefix = ` ${theme.md.bullet} `;
|
|
167
229
|
const prefixWidth = visibleWidth(bulletPrefix);
|
|
168
|
-
for (const session of this.recentSessions.slice(0,
|
|
230
|
+
for (const session of this.recentSessions.slice(0, WELCOME_SESSION_SLOTS)) {
|
|
169
231
|
const timeSuffixRaw = ` (${session.timeAgo})`;
|
|
170
232
|
const timeWidth = visibleWidth(timeSuffixRaw);
|
|
171
233
|
const nameBudget = Math.max(1, rightCol - prefixWidth - timeWidth);
|
|
@@ -176,23 +238,33 @@ export class WelcomeComponent implements Component {
|
|
|
176
238
|
);
|
|
177
239
|
}
|
|
178
240
|
}
|
|
241
|
+
// Pad to the fixed slot count so the box doesn't grow when sessions load in.
|
|
242
|
+
while (sessionLines.length < WELCOME_SESSION_SLOTS) {
|
|
243
|
+
sessionLines.push("");
|
|
244
|
+
}
|
|
179
245
|
|
|
180
246
|
// LSP servers content
|
|
181
247
|
const lspLines: string[] = [];
|
|
182
248
|
if (this.lspServers.length === 0) {
|
|
183
249
|
lspLines.push(` ${theme.fg("dim", "No LSP servers")}`);
|
|
184
250
|
} else {
|
|
185
|
-
for (const server of this.lspServers) {
|
|
251
|
+
for (const server of this.lspServers.slice(0, WELCOME_LSP_SLOTS)) {
|
|
186
252
|
const icon =
|
|
187
253
|
server.status === "ready"
|
|
188
254
|
? theme.styledSymbol("status.enabled", "success")
|
|
189
|
-
: server.status === "
|
|
190
|
-
? theme.styledSymbol("status.
|
|
191
|
-
:
|
|
255
|
+
: server.status === "available"
|
|
256
|
+
? theme.styledSymbol("status.enabled", "dim")
|
|
257
|
+
: server.status === "connecting"
|
|
258
|
+
? theme.styledSymbol("status.pending", "muted")
|
|
259
|
+
: theme.styledSymbol("status.error", "error");
|
|
192
260
|
const exts = server.fileTypes.slice(0, 3).join(" ");
|
|
193
261
|
lspLines.push(` ${icon} ${theme.fg("muted", server.name)} ${theme.fg("dim", exts)}`);
|
|
194
262
|
}
|
|
195
263
|
}
|
|
264
|
+
// Pad to the fixed slot count so the box height doesn't depend on server count.
|
|
265
|
+
while (lspLines.length < WELCOME_LSP_SLOTS) {
|
|
266
|
+
lspLines.push("");
|
|
267
|
+
}
|
|
196
268
|
|
|
197
269
|
// Right column
|
|
198
270
|
const rightLines = [
|
|
@@ -305,23 +377,12 @@ export class WelcomeComponent implements Component {
|
|
|
305
377
|
return str + padding(width - visLen);
|
|
306
378
|
}
|
|
307
379
|
|
|
308
|
-
/** Pick the logo frame for the current intro phase, or the resting frame. */
|
|
380
|
+
/** Pick the logo frame for the current intro phase, or the resting/held frame. */
|
|
309
381
|
#currentLogoFrame(): readonly string[] {
|
|
310
|
-
if (this.#animStart == null) return REST_FRAME;
|
|
382
|
+
if (this.#animStart == null) return this.#holdIntroFirstFrame ? INTRO_FIRST_FRAME : REST_FRAME;
|
|
311
383
|
const elapsed = performance.now() - this.#animStart;
|
|
312
384
|
if (elapsed >= INTRO_MS) return REST_FRAME;
|
|
313
|
-
|
|
314
|
-
const progress = elapsed / INTRO_MS;
|
|
315
|
-
const eased = 1 - (1 - progress) ** 3;
|
|
316
|
-
// Sweep backward through INTRO_SWEEPS full rotations so the gradient
|
|
317
|
-
// visibly spins multiple times. `eased == 1` → phase = 0 = resting frame.
|
|
318
|
-
const phase = ((((1 - eased) * INTRO_SWEEPS) % 1) + 1) % 1;
|
|
319
|
-
// Shine traverses the diagonal at a steady pace, decoupled from the
|
|
320
|
-
// gradient phase so the two layers parallax. Strength fades out with
|
|
321
|
-
// the same ease-out curve so the highlight is gone by the resting frame.
|
|
322
|
-
const shinePos = (((progress * INTRO_SHINE_TRAVERSALS) % 1) + 1) % 1;
|
|
323
|
-
const shineStrength = (1 - eased) ** 1.5;
|
|
324
|
-
return gradientLogo(PI_LOGO, phase, { strength: shineStrength, pos: shinePos });
|
|
385
|
+
return introLogoFrame(elapsed / INTRO_MS);
|
|
325
386
|
}
|
|
326
387
|
}
|
|
327
388
|
|
|
@@ -431,5 +492,26 @@ const INTRO_SWEEPS = 2.5;
|
|
|
431
492
|
/** Number of times the shine highlight crosses the diagonal across the intro. */
|
|
432
493
|
const INTRO_SHINE_TRAVERSALS = 3;
|
|
433
494
|
|
|
495
|
+
/**
|
|
496
|
+
* Logo frame for a normalized intro progress in [0, 1).
|
|
497
|
+
*
|
|
498
|
+
* Ease-out cubic so the spin decelerates into the resting state. The gradient
|
|
499
|
+
* sweeps backward through INTRO_SWEEPS full rotations (`eased == 1` → phase =
|
|
500
|
+
* 0 = resting frame) while the shine traverses the diagonal at a steady pace,
|
|
501
|
+
* decoupled from the gradient phase so the two layers parallax; its strength
|
|
502
|
+
* fades with the same ease-out curve so the highlight is gone by the resting
|
|
503
|
+
* frame.
|
|
504
|
+
*/
|
|
505
|
+
function introLogoFrame(progress: number): string[] {
|
|
506
|
+
const eased = 1 - (1 - progress) ** 3;
|
|
507
|
+
const phase = ((((1 - eased) * INTRO_SWEEPS) % 1) + 1) % 1;
|
|
508
|
+
const shinePos = (((progress * INTRO_SHINE_TRAVERSALS) % 1) + 1) % 1;
|
|
509
|
+
const shineStrength = (1 - eased) ** 1.5;
|
|
510
|
+
return gradientLogo(PI_LOGO, phase, { strength: shineStrength, pos: shinePos });
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** First intro frame, cached for splash-held renders (resize re-renders reuse it). */
|
|
514
|
+
const INTRO_FIRST_FRAME = introLogoFrame(0);
|
|
515
|
+
|
|
434
516
|
/** Resting gradient frame, cached for re-renders outside of the intro. */
|
|
435
517
|
const REST_FRAME = gradientLogo(PI_LOGO, 0);
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
loadHindsightConfig,
|
|
22
22
|
reloadMentalModelsForSession,
|
|
23
23
|
resolveSeedsForScope,
|
|
24
|
+
seedAlreadyExists,
|
|
24
25
|
summarizeMentalModel,
|
|
25
26
|
} from "../../hindsight";
|
|
26
27
|
import { resolveMemoryBackend } from "../../memory-backend";
|
|
@@ -314,7 +315,13 @@ export class CommandController {
|
|
|
314
315
|
info += `\n${theme.bold("LSP Servers")}\n`;
|
|
315
316
|
for (const server of this.ctx.lspServers) {
|
|
316
317
|
const statusColor =
|
|
317
|
-
server.status === "ready"
|
|
318
|
+
server.status === "ready"
|
|
319
|
+
? "success"
|
|
320
|
+
: server.status === "available"
|
|
321
|
+
? "dim"
|
|
322
|
+
: server.status === "connecting"
|
|
323
|
+
? "warning"
|
|
324
|
+
: "error";
|
|
318
325
|
const statusText =
|
|
319
326
|
server.status === "error" && server.error ? `${server.status}: ${server.error}` : server.status;
|
|
320
327
|
info += `${theme.fg("dim", `${server.name}:`)} ${theme.fg(statusColor, statusText)} ${theme.fg("dim", `(${server.fileTypes.join(", ")})`)}\n`;
|
|
@@ -712,11 +719,11 @@ export class CommandController {
|
|
|
712
719
|
return;
|
|
713
720
|
}
|
|
714
721
|
const list = await state.client.listMentalModels(state.bankId, { detail: "metadata" });
|
|
715
|
-
const existing =
|
|
722
|
+
const existing = list.items ?? [];
|
|
716
723
|
let created = 0;
|
|
717
724
|
let skipped = 0;
|
|
718
725
|
for (const seed of seeds) {
|
|
719
|
-
if (
|
|
726
|
+
if (seedAlreadyExists(seed, existing)) {
|
|
720
727
|
skipped++;
|
|
721
728
|
continue;
|
|
722
729
|
}
|
|
@@ -25,6 +25,16 @@ import { StreamingRevealController } from "./streaming-reveal";
|
|
|
25
25
|
type AgentSessionEventKind = AgentSessionEvent["type"];
|
|
26
26
|
|
|
27
27
|
const IRC_MESSAGE_VISIBLE_TTL_MS = 10_000;
|
|
28
|
+
/**
|
|
29
|
+
* Concurrent IRC cards allowed in the transcript's live region. Cards land
|
|
30
|
+
* below a still-live block (a running task), where they cannot commit to
|
|
31
|
+
* native scrollback (commits are prefix-only) — every visible card inflates
|
|
32
|
+
* the live region and pushes the live block's uncommitted rows above the
|
|
33
|
+
* window top, where they are neither on screen nor in history. A swarm burst
|
|
34
|
+
* (several agents coordinating at once) must therefore stay bounded: the
|
|
35
|
+
* oldest live-region card retires as soon as a new one would exceed the cap.
|
|
36
|
+
*/
|
|
37
|
+
const MAX_LIVE_IRC_CARDS = 4;
|
|
28
38
|
|
|
29
39
|
/**
|
|
30
40
|
* Loader label shown the instant a user interrupt (Esc) is requested, kept until
|
|
@@ -64,6 +74,9 @@ export class EventController {
|
|
|
64
74
|
#pinnedErrorComponent: AssistantMessageComponent | undefined = undefined;
|
|
65
75
|
#idleCompactionTimer?: NodeJS.Timeout;
|
|
66
76
|
#ircExpiryTimers = new Map<string, NodeJS.Timeout>();
|
|
77
|
+
// Insertion-ordered IRC cards not yet retired; values are the transcript
|
|
78
|
+
// components each card contributed (see #retireIrcCard for the guard).
|
|
79
|
+
#liveIrcCards = new Map<string, Component[]>();
|
|
67
80
|
#streamingReveal: StreamingRevealController;
|
|
68
81
|
#handlers: AgentSessionEventHandlers;
|
|
69
82
|
|
|
@@ -111,6 +124,7 @@ export class EventController {
|
|
|
111
124
|
clearTimeout(timer);
|
|
112
125
|
}
|
|
113
126
|
this.#ircExpiryTimers.clear();
|
|
127
|
+
this.#liveIrcCards.clear();
|
|
114
128
|
}
|
|
115
129
|
|
|
116
130
|
#resetReadGroup(): void {
|
|
@@ -324,6 +338,7 @@ export class EventController {
|
|
|
324
338
|
this.#resetReadGroup();
|
|
325
339
|
const components = this.ctx.addMessageToChat(event.message);
|
|
326
340
|
this.#scheduleIrcExpiry(signature, components);
|
|
341
|
+
this.#enforceIrcCardCap(signature);
|
|
327
342
|
this.ctx.ui.requestRender();
|
|
328
343
|
}
|
|
329
344
|
|
|
@@ -331,13 +346,47 @@ export class EventController {
|
|
|
331
346
|
if (components.length === 0 || this.#ircExpiryTimers.has(signature)) return;
|
|
332
347
|
const timer = setTimeout(() => {
|
|
333
348
|
this.#ircExpiryTimers.delete(signature);
|
|
334
|
-
|
|
335
|
-
this.ctx.chatContainer.removeChild(component);
|
|
336
|
-
}
|
|
337
|
-
this.ctx.ui.requestRender();
|
|
349
|
+
this.#retireIrcCard(signature);
|
|
338
350
|
}, IRC_MESSAGE_VISIBLE_TTL_MS);
|
|
339
351
|
timer.unref?.();
|
|
340
352
|
this.#ircExpiryTimers.set(signature, timer);
|
|
353
|
+
this.#liveIrcCards.set(signature, components);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Remove an expired/evicted IRC card — but only while it still sits below a
|
|
358
|
+
* live block, where its rows cannot have entered native scrollback. Once
|
|
359
|
+
* everything above it has finalized, its rows may already be committed;
|
|
360
|
+
* removing them then is an interior deletion of the committed prefix, which
|
|
361
|
+
* the engine can only repair by recommitting every row below the gap —
|
|
362
|
+
* exactly the duplicated-block artifact this guard exists to prevent. Such
|
|
363
|
+
* a card simply stays: it is final history, and the window scrolls past it.
|
|
364
|
+
*/
|
|
365
|
+
#retireIrcCard(signature: string): void {
|
|
366
|
+
const components = this.#liveIrcCards.get(signature);
|
|
367
|
+
this.#liveIrcCards.delete(signature);
|
|
368
|
+
if (!components) return;
|
|
369
|
+
let removed = false;
|
|
370
|
+
for (const component of components) {
|
|
371
|
+
if (!this.ctx.chatContainer.isWithinLiveRegion(component)) continue;
|
|
372
|
+
this.ctx.chatContainer.removeChild(component);
|
|
373
|
+
removed = true;
|
|
374
|
+
}
|
|
375
|
+
if (removed) this.ctx.ui.requestRender();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** Evict oldest live-region cards beyond {@link MAX_LIVE_IRC_CARDS}. */
|
|
379
|
+
#enforceIrcCardCap(latestSignature: string): void {
|
|
380
|
+
while (this.#liveIrcCards.size > MAX_LIVE_IRC_CARDS) {
|
|
381
|
+
const oldest = this.#liveIrcCards.keys().next().value;
|
|
382
|
+
if (oldest === undefined || oldest === latestSignature) return;
|
|
383
|
+
const timer = this.#ircExpiryTimers.get(oldest);
|
|
384
|
+
if (timer) {
|
|
385
|
+
clearTimeout(timer);
|
|
386
|
+
this.#ircExpiryTimers.delete(oldest);
|
|
387
|
+
}
|
|
388
|
+
this.#retireIrcCard(oldest);
|
|
389
|
+
}
|
|
341
390
|
}
|
|
342
391
|
|
|
343
392
|
async #handleNotice(event: Extract<AgentSessionEvent, { type: "notice" }>): Promise<void> {
|
|
@@ -365,6 +414,26 @@ export class EventController {
|
|
|
365
414
|
this.#resetReadGroup();
|
|
366
415
|
this.#lastVisibleBlockCount = visibleBlockCount;
|
|
367
416
|
}
|
|
417
|
+
|
|
418
|
+
// Content blocks stream sequentially: a toolCall block can only begin
|
|
419
|
+
// after every preceding thinking/text block has closed, and the
|
|
420
|
+
// reveal's setTarget above force-completes the visible text for
|
|
421
|
+
// toolCall messages. Finalize the assistant block now instead of at
|
|
422
|
+
// message_end so the transcript's commit-safe run can extend through
|
|
423
|
+
// it into the streaming tool preview below — otherwise a long args
|
|
424
|
+
// stream (a big write/edit/eval) sits below a still-live block and
|
|
425
|
+
// can never reach native scrollback: the head of the preview is
|
|
426
|
+
// neither committed nor on screen and the transcript reads as cut.
|
|
427
|
+
// Skipped when the per-turn usage row is enabled: that row is only
|
|
428
|
+
// known at message_end and appends to this block, which would shift
|
|
429
|
+
// committed tool rows below it every turn (audit recommit →
|
|
430
|
+
// duplicated preview copies in scrollback).
|
|
431
|
+
if (
|
|
432
|
+
this.ctx.streamingMessage.content.some(content => content.type === "toolCall") &&
|
|
433
|
+
!settings.get("display.showTokenUsage")
|
|
434
|
+
) {
|
|
435
|
+
this.ctx.streamingComponent.markTranscriptBlockFinalized();
|
|
436
|
+
}
|
|
368
437
|
for (const content of this.ctx.streamingMessage.content) {
|
|
369
438
|
if (content.type !== "toolCall") continue;
|
|
370
439
|
if (content.name === "read") {
|
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs/promises";
|
|
|
2
2
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
3
3
|
import type { AutocompleteProvider, SlashCommand } from "@oh-my-pi/pi-tui";
|
|
4
4
|
import { $env, logger, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
5
|
-
import { getRoleInfo } from "../../config/model-
|
|
5
|
+
import { getRoleInfo } from "../../config/model-roles";
|
|
6
6
|
import { isSettingsInitialized, settings } from "../../config/settings";
|
|
7
7
|
import { renderSegmentTrack } from "../../modes/components/segment-track";
|
|
8
8
|
import { TinyTitleDownloadProgressComponent } from "../../modes/components/tiny-title-download-progress";
|
|
@@ -62,7 +62,7 @@ export class MCPAuthorizationLinkPrompt implements Component {
|
|
|
62
62
|
|
|
63
63
|
invalidate(): void {}
|
|
64
64
|
|
|
65
|
-
render(_width: number): string[] {
|
|
65
|
+
render(_width: number): readonly string[] {
|
|
66
66
|
const link = urlHyperlinkAlways(this.#url, "Click here to authorize");
|
|
67
67
|
return [
|
|
68
68
|
` ${theme.fg("success", "Open authorization URL:")}`,
|
|
@@ -5,8 +5,8 @@ import type { OAuthProvider } from "@oh-my-pi/pi-ai/oauth/types";
|
|
|
5
5
|
import type { Component, OverlayHandle } from "@oh-my-pi/pi-tui";
|
|
6
6
|
import { Input, Loader, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
7
7
|
import { getAgentDbPath, getProjectDir, normalizePathForComparison } from "@oh-my-pi/pi-utils";
|
|
8
|
-
import { getRoleInfo } from "../../config/model-registry";
|
|
9
8
|
import { formatModelSelectorValue } from "../../config/model-resolver";
|
|
9
|
+
import { getRoleInfo } from "../../config/model-roles";
|
|
10
10
|
import { settings } from "../../config/settings";
|
|
11
11
|
import { disableProvider, enableProvider } from "../../discovery";
|
|
12
12
|
import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
|
|
@@ -23,6 +23,45 @@ function countGraphemes(text: string): number {
|
|
|
23
23
|
return count;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/** Count graphemes of `text` from code-unit offset `start`, also reporting the
|
|
27
|
+
* start offset of the final grapheme (where an append could extend a cluster). */
|
|
28
|
+
function countGraphemesFrom(text: string, start: number): { count: number; tailStart: number } {
|
|
29
|
+
let count = 0;
|
|
30
|
+
let tailStart = start;
|
|
31
|
+
for (const seg of getSegmenter().segment(start === 0 ? text : text.slice(start))) {
|
|
32
|
+
count += 1;
|
|
33
|
+
tailStart = start + seg.index;
|
|
34
|
+
}
|
|
35
|
+
return { count, tailStart };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Memoizes per-block grapheme counts across reveal ticks. Streaming blocks only
|
|
39
|
+
* grow by appending, and an append can only alter the final grapheme cluster of
|
|
40
|
+
* the previous text, so only the suffix from that cluster needs re-segmenting. */
|
|
41
|
+
class BlockUnitCounter {
|
|
42
|
+
#entries = new Map<number, { text: string; count: number; tailStart: number }>();
|
|
43
|
+
|
|
44
|
+
count(index: number, text: string): number {
|
|
45
|
+
const entry = this.#entries.get(index);
|
|
46
|
+
if (entry !== undefined) {
|
|
47
|
+
if (entry.text === text) return entry.count;
|
|
48
|
+
if (entry.count > 0 && text.length > entry.text.length && text.startsWith(entry.text)) {
|
|
49
|
+
const tail = countGraphemesFrom(text, entry.tailStart);
|
|
50
|
+
const next = { text, count: entry.count - 1 + tail.count, tailStart: tail.tailStart };
|
|
51
|
+
this.#entries.set(index, next);
|
|
52
|
+
return next.count;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const full = countGraphemesFrom(text, 0);
|
|
56
|
+
this.#entries.set(index, { text, count: full.count, tailStart: full.tailStart });
|
|
57
|
+
return full.count;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
reset(): void {
|
|
61
|
+
this.#entries.clear();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
26
65
|
function sliceGraphemes(text: string, units: number): string {
|
|
27
66
|
if (units <= 0 || text.length === 0) return "";
|
|
28
67
|
let count = 0;
|
|
@@ -51,9 +90,9 @@ export function visibleUnits(message: AssistantMessage, hideThinking: boolean):
|
|
|
51
90
|
function revealTextBlock(
|
|
52
91
|
block: Extract<AssistantContentBlock, { type: "text" }>,
|
|
53
92
|
remaining: number,
|
|
93
|
+
units: number,
|
|
54
94
|
): AssistantContentBlock {
|
|
55
95
|
if (remaining <= 0) return block.text.length === 0 ? block : { ...block, text: "" };
|
|
56
|
-
const units = countGraphemes(block.text);
|
|
57
96
|
if (remaining >= units) return block;
|
|
58
97
|
return { ...block, text: sliceGraphemes(block.text, remaining) };
|
|
59
98
|
}
|
|
@@ -61,9 +100,9 @@ function revealTextBlock(
|
|
|
61
100
|
function revealThinkingBlock(
|
|
62
101
|
block: Extract<AssistantContentBlock, { type: "thinking" }>,
|
|
63
102
|
remaining: number,
|
|
103
|
+
units: number,
|
|
64
104
|
): AssistantContentBlock {
|
|
65
105
|
if (remaining <= 0) return block.thinking.length === 0 ? block : { ...block, thinking: "" };
|
|
66
|
-
const units = countGraphemes(block.thinking);
|
|
67
106
|
if (remaining >= units) return block;
|
|
68
107
|
return { ...block, thinking: sliceGraphemes(block.thinking, remaining) };
|
|
69
108
|
}
|
|
@@ -72,16 +111,20 @@ export function buildDisplayMessage(
|
|
|
72
111
|
target: AssistantMessage,
|
|
73
112
|
revealed: number,
|
|
74
113
|
hideThinking: boolean,
|
|
114
|
+
countOf: (index: number, text: string) => number = (_index, text) => countGraphemes(text),
|
|
75
115
|
): AssistantMessage {
|
|
76
116
|
let remaining = Math.max(0, Math.floor(revealed));
|
|
77
117
|
const content: AssistantContentBlock[] = [];
|
|
78
|
-
for (
|
|
118
|
+
for (let i = 0; i < target.content.length; i++) {
|
|
119
|
+
const block = target.content[i]!;
|
|
79
120
|
if (block.type === "text") {
|
|
80
|
-
|
|
81
|
-
|
|
121
|
+
const units = countOf(i, block.text);
|
|
122
|
+
content.push(revealTextBlock(block, remaining, units));
|
|
123
|
+
remaining = Math.max(0, remaining - units);
|
|
82
124
|
} else if (block.type === "thinking" && !hideThinking) {
|
|
83
|
-
|
|
84
|
-
|
|
125
|
+
const units = countOf(i, block.thinking);
|
|
126
|
+
content.push(revealThinkingBlock(block, remaining, units));
|
|
127
|
+
remaining = Math.max(0, remaining - units);
|
|
85
128
|
} else {
|
|
86
129
|
content.push(block);
|
|
87
130
|
}
|
|
@@ -103,6 +146,8 @@ export class StreamingRevealController {
|
|
|
103
146
|
#revealed = 0;
|
|
104
147
|
#hideThinkingBlock = false;
|
|
105
148
|
#smoothStreaming = true;
|
|
149
|
+
readonly #unitCounter = new BlockUnitCounter();
|
|
150
|
+
readonly #countOf = (index: number, text: string): number => this.#unitCounter.count(index, text);
|
|
106
151
|
|
|
107
152
|
constructor(options: StreamingRevealControllerOptions) {
|
|
108
153
|
this.#getSmoothStreaming = options.getSmoothStreaming;
|
|
@@ -121,15 +166,15 @@ export class StreamingRevealController {
|
|
|
121
166
|
component.updateContent(message);
|
|
122
167
|
return;
|
|
123
168
|
}
|
|
124
|
-
const total = visibleUnits(message
|
|
169
|
+
const total = this.#visibleUnits(message);
|
|
125
170
|
if (message.content.some(block => block.type === "toolCall")) {
|
|
126
171
|
// A tool call is a transcript-order boundary: finish any leading
|
|
127
172
|
// assistant text before EventController renders the separate tool card.
|
|
128
173
|
this.#revealed = total;
|
|
129
|
-
component.updateContent(buildDisplayMessage(message, this.#revealed, this.#hideThinkingBlock));
|
|
174
|
+
component.updateContent(buildDisplayMessage(message, this.#revealed, this.#hideThinkingBlock, this.#countOf));
|
|
130
175
|
return;
|
|
131
176
|
}
|
|
132
|
-
this.#renderCurrent();
|
|
177
|
+
this.#renderCurrent(total);
|
|
133
178
|
this.#syncTimer(total);
|
|
134
179
|
}
|
|
135
180
|
|
|
@@ -140,19 +185,21 @@ export class StreamingRevealController {
|
|
|
140
185
|
this.#component.updateContent(message);
|
|
141
186
|
return;
|
|
142
187
|
}
|
|
143
|
-
const total = visibleUnits(message
|
|
188
|
+
const total = this.#visibleUnits(message);
|
|
144
189
|
if (message.content.some(block => block.type === "toolCall")) {
|
|
145
190
|
// A tool call is a transcript-order boundary: finish any leading
|
|
146
191
|
// assistant text before EventController renders the separate tool card.
|
|
147
192
|
this.#revealed = total;
|
|
148
193
|
this.#stopTimer();
|
|
149
|
-
this.#component.updateContent(
|
|
194
|
+
this.#component.updateContent(
|
|
195
|
+
buildDisplayMessage(message, this.#revealed, this.#hideThinkingBlock, this.#countOf),
|
|
196
|
+
);
|
|
150
197
|
return;
|
|
151
198
|
}
|
|
152
199
|
if (this.#revealed > total) {
|
|
153
200
|
this.#revealed = total;
|
|
154
201
|
}
|
|
155
|
-
this.#renderCurrent();
|
|
202
|
+
this.#renderCurrent(total);
|
|
156
203
|
this.#syncTimer(total);
|
|
157
204
|
}
|
|
158
205
|
|
|
@@ -161,14 +208,32 @@ export class StreamingRevealController {
|
|
|
161
208
|
this.#target = undefined;
|
|
162
209
|
this.#component = undefined;
|
|
163
210
|
this.#revealed = 0;
|
|
211
|
+
this.#unitCounter.reset();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Total reveal units of `message`, memoized per block across ticks. */
|
|
215
|
+
#visibleUnits(message: AssistantMessage): number {
|
|
216
|
+
let total = 0;
|
|
217
|
+
for (let i = 0; i < message.content.length; i++) {
|
|
218
|
+
const block = message.content[i]!;
|
|
219
|
+
if (block.type === "text") {
|
|
220
|
+
total += this.#unitCounter.count(i, block.text);
|
|
221
|
+
} else if (block.type === "thinking" && !this.#hideThinkingBlock) {
|
|
222
|
+
total += this.#unitCounter.count(i, block.thinking);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return total;
|
|
164
226
|
}
|
|
165
227
|
|
|
166
|
-
#renderCurrent(): void {
|
|
228
|
+
#renderCurrent(total = this.#target ? this.#visibleUnits(this.#target) : 0): void {
|
|
167
229
|
if (!this.#target || !this.#component) return;
|
|
168
|
-
this.#component.updateContent(
|
|
230
|
+
this.#component.updateContent(
|
|
231
|
+
buildDisplayMessage(this.#target, this.#revealed, this.#hideThinkingBlock, this.#countOf),
|
|
232
|
+
{ transient: this.#revealed < total },
|
|
233
|
+
);
|
|
169
234
|
}
|
|
170
235
|
|
|
171
|
-
#syncTimer(total = this.#target ? visibleUnits(this.#target
|
|
236
|
+
#syncTimer(total = this.#target ? this.#visibleUnits(this.#target) : 0): void {
|
|
172
237
|
if (!this.#target || !this.#component || this.#revealed >= total) {
|
|
173
238
|
this.#stopTimer();
|
|
174
239
|
return;
|
|
@@ -197,13 +262,15 @@ export class StreamingRevealController {
|
|
|
197
262
|
this.stop();
|
|
198
263
|
return;
|
|
199
264
|
}
|
|
200
|
-
const total = visibleUnits(target
|
|
265
|
+
const total = this.#visibleUnits(target);
|
|
201
266
|
if (this.#revealed >= total) {
|
|
202
267
|
this.#stopTimer();
|
|
203
268
|
return;
|
|
204
269
|
}
|
|
205
270
|
this.#revealed = Math.min(total, this.#revealed + nextStep(total - this.#revealed));
|
|
206
|
-
component.updateContent(buildDisplayMessage(target, this.#revealed, this.#hideThinkingBlock)
|
|
271
|
+
component.updateContent(buildDisplayMessage(target, this.#revealed, this.#hideThinkingBlock, this.#countOf), {
|
|
272
|
+
transient: this.#revealed < total,
|
|
273
|
+
});
|
|
207
274
|
this.#requestRender();
|
|
208
275
|
if (this.#revealed >= total) {
|
|
209
276
|
this.#stopTimer();
|
|
@@ -12,14 +12,8 @@ import {
|
|
|
12
12
|
ThinkingLevel,
|
|
13
13
|
} from "@oh-my-pi/pi-agent-core";
|
|
14
14
|
import type { CompactionOutcome } from "@oh-my-pi/pi-agent-core/compaction";
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
type ImageContent,
|
|
18
|
-
type Message,
|
|
19
|
-
type Model,
|
|
20
|
-
modelsAreEqual,
|
|
21
|
-
type UsageReport,
|
|
22
|
-
} from "@oh-my-pi/pi-ai";
|
|
15
|
+
import type { AssistantMessage, ImageContent, Message, Model, UsageReport } from "@oh-my-pi/pi-ai";
|
|
16
|
+
import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
|
|
23
17
|
import type { Component, EditorTheme, LoaderMessageColorFn, OverlayHandle, SlashCommand } from "@oh-my-pi/pi-tui";
|
|
24
18
|
import {
|
|
25
19
|
Container,
|
|
@@ -49,7 +43,7 @@ import {
|
|
|
49
43
|
import chalk from "chalk";
|
|
50
44
|
import { reset as resetCapabilities } from "../capability";
|
|
51
45
|
import { KeybindingsManager } from "../config/keybindings";
|
|
52
|
-
import { MODEL_ROLES, type ModelRole } from "../config/model-
|
|
46
|
+
import { MODEL_ROLES, type ModelRole } from "../config/model-roles";
|
|
53
47
|
import { isSettingsInitialized, onStatusLineSessionAccentChanged, Settings, settings } from "../config/settings";
|
|
54
48
|
import { clearClaudePluginRootsCache } from "../discovery/helpers";
|
|
55
49
|
import type {
|
|
@@ -60,7 +60,7 @@ class GlyphSceneController implements SetupSceneController {
|
|
|
60
60
|
this.#selectList.handleInput(data);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
render(width: number): string[] {
|
|
63
|
+
render(width: number): readonly string[] {
|
|
64
64
|
return [
|
|
65
65
|
theme.fg("muted", "If a row shows boxes, tofu, or misaligned icons, pick another."),
|
|
66
66
|
"",
|
|
@@ -52,7 +52,7 @@ class ProvidersSceneController implements SetupSceneController {
|
|
|
52
52
|
tab.handleInput(data);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
render(width: number): string[] {
|
|
55
|
+
render(width: number): readonly string[] {
|
|
56
56
|
return [...this.#tabBar.render(width), "", ...this.#activeTab().render(width)];
|
|
57
57
|
}
|
|
58
58
|
|