@oh-my-pi/pi-coding-agent 15.5.15 → 15.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +81 -0
- package/dist/types/capability/rule-buckets.d.ts +30 -0
- package/dist/types/capability/rule.d.ts +7 -0
- package/dist/types/cli/classify-install-target.d.ts +0 -10
- package/dist/types/cli/completion-gen.d.ts +80 -0
- package/dist/types/cli/initial-message.d.ts +1 -1
- package/dist/types/cli/tiny-models-cli.d.ts +9 -0
- package/dist/types/commands/complete.d.ts +6 -0
- package/dist/types/commands/completions.d.ts +13 -0
- package/dist/types/commands/setup.d.ts +10 -1
- package/dist/types/commands/tiny-models.d.ts +22 -0
- package/dist/types/commit/analysis/conventional.d.ts +1 -1
- package/dist/types/commit/analysis/summary.d.ts +1 -1
- package/dist/types/commit/changelog/generate.d.ts +1 -1
- package/dist/types/commit/changelog/index.d.ts +2 -2
- package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
- package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
- package/dist/types/config/model-id-affixes.d.ts +10 -0
- package/dist/types/config/settings-schema.d.ts +402 -17
- package/dist/types/discovery/builtin-defaults.d.ts +1 -0
- package/dist/types/discovery/builtin-rules/index.d.ts +7 -0
- package/dist/types/discovery/helpers.d.ts +1 -1
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
- package/dist/types/edit/hashline/block-resolver.d.ts +9 -0
- package/dist/types/edit/hashline/index.d.ts +1 -0
- package/dist/types/eval/js/shared/rewrite-imports.d.ts +16 -1
- package/dist/types/eval/py/kernel.d.ts +3 -0
- package/dist/types/eval/py/runtime.d.ts +11 -1
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/internal-urls/agent-protocol.d.ts +2 -1
- package/dist/types/internal-urls/artifact-protocol.d.ts +2 -1
- package/dist/types/internal-urls/local-protocol.d.ts +2 -1
- package/dist/types/internal-urls/memory-protocol.d.ts +2 -1
- package/dist/types/internal-urls/omp-protocol.d.ts +2 -1
- package/dist/types/internal-urls/router.d.ts +8 -1
- package/dist/types/internal-urls/rule-protocol.d.ts +2 -1
- package/dist/types/internal-urls/skill-protocol.d.ts +2 -1
- package/dist/types/internal-urls/types.d.ts +26 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/memory-backend/index.d.ts +1 -0
- package/dist/types/memory-backend/resolve.d.ts +2 -1
- package/dist/types/memory-backend/types.d.ts +7 -1
- package/dist/types/mnemosyne/backend.d.ts +4 -0
- package/dist/types/mnemosyne/config.d.ts +29 -0
- package/dist/types/mnemosyne/index.d.ts +3 -0
- package/dist/types/mnemosyne/state.d.ts +72 -0
- package/dist/types/modes/components/custom-editor.d.ts +2 -3
- package/dist/types/modes/components/hook-selector.d.ts +27 -0
- package/dist/types/modes/components/index.d.ts +2 -0
- package/dist/types/modes/components/segment-track.d.ts +22 -0
- package/dist/types/modes/components/status-line/context-thresholds.d.ts +6 -0
- package/dist/types/modes/components/tiny-title-download-progress.d.ts +11 -0
- package/dist/types/modes/components/welcome.d.ts +22 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +4 -1
- package/dist/types/modes/gradient-highlight.d.ts +23 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -4
- package/dist/types/modes/internal-url-autocomplete.d.ts +43 -0
- package/dist/types/modes/orchestrate.d.ts +10 -0
- package/dist/types/modes/setup-wizard/index.d.ts +16 -0
- package/dist/types/modes/setup-wizard/scenes/glyph.d.ts +2 -0
- package/dist/types/modes/setup-wizard/scenes/outro.d.ts +2 -0
- package/dist/types/modes/setup-wizard/scenes/providers.d.ts +2 -0
- package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +19 -0
- package/dist/types/modes/setup-wizard/scenes/splash.d.ts +11 -0
- package/dist/types/modes/setup-wizard/scenes/theme.d.ts +2 -0
- package/dist/types/modes/setup-wizard/scenes/types.d.ts +43 -0
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +19 -0
- package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +14 -0
- package/dist/types/modes/theme/defaults/index.d.ts +8406 -8406
- package/dist/types/modes/theme/shimmer.d.ts +2 -0
- package/dist/types/modes/theme/theme.d.ts +11 -0
- package/dist/types/modes/types.d.ts +5 -1
- package/dist/types/modes/ultrathink.d.ts +3 -3
- package/dist/types/modes/utils/keybinding-matchers.d.ts +5 -0
- package/dist/types/sdk.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +33 -0
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +2 -0
- package/dist/types/task/render.d.ts +5 -1
- package/dist/types/tiny/device.d.ts +78 -0
- package/dist/types/tiny/dtype.d.ts +85 -0
- package/dist/types/tiny/models.d.ts +185 -0
- package/dist/types/tiny/text.d.ts +19 -0
- package/dist/types/tiny/title-client.d.ts +32 -0
- package/dist/types/tiny/title-protocol.d.ts +74 -0
- package/dist/types/tiny/worker.d.ts +2 -0
- package/dist/types/tools/bash.d.ts +3 -2
- package/dist/types/tools/eval.d.ts +1 -1
- package/dist/types/tools/index.d.ts +7 -4
- package/dist/types/tools/memory-edit.d.ts +40 -0
- package/dist/types/tools/{hindsight-recall.d.ts → memory-recall.d.ts} +6 -6
- package/dist/types/tools/{hindsight-reflect.d.ts → memory-reflect.d.ts} +6 -6
- package/dist/types/tools/memory-render.d.ts +60 -0
- package/dist/types/tools/{hindsight-retain.d.ts → memory-retain.d.ts} +6 -6
- package/dist/types/tools/todo-write.d.ts +8 -0
- package/dist/types/tools/tool-result.d.ts +2 -0
- package/dist/types/tui/code-cell.d.ts +2 -0
- package/dist/types/tui/output-block.d.ts +17 -0
- package/dist/types/utils/title-generator.d.ts +3 -0
- package/package.json +18 -14
- package/scripts/build-binary.ts +1 -0
- package/src/capability/rule-buckets.ts +64 -0
- package/src/capability/rule.ts +8 -0
- package/src/cli/completion-gen.ts +550 -0
- package/src/cli/setup-cli.ts +5 -3
- package/src/cli/tiny-models-cli.ts +127 -0
- package/src/cli-commands.ts +3 -0
- package/src/cli.ts +9 -15
- package/src/commands/complete.ts +66 -0
- package/src/commands/completions.ts +60 -0
- package/src/commands/setup.ts +29 -4
- package/src/commands/tiny-models.ts +36 -0
- package/src/config/model-equivalence.ts +43 -2
- package/src/config/model-id-affixes.ts +64 -0
- package/src/config/model-registry.ts +84 -10
- package/src/config/settings-schema.ts +275 -15
- package/src/discovery/builtin-defaults.ts +39 -0
- package/src/discovery/builtin-rules/index.ts +48 -0
- package/src/discovery/builtin-rules/rs-box-leak.md +48 -0
- package/src/discovery/builtin-rules/rs-future-prelude.md +23 -0
- package/src/discovery/builtin-rules/rs-lazylock.md +51 -0
- package/src/discovery/builtin-rules/rs-match-ergonomics.md +67 -0
- package/src/discovery/builtin-rules/rs-parking-lot.md +44 -0
- package/src/discovery/builtin-rules/rs-result-type.md +19 -0
- package/src/discovery/builtin-rules/ts-bare-catch.md +38 -0
- package/src/discovery/builtin-rules/ts-import-type.md +42 -0
- package/src/discovery/builtin-rules/ts-no-any.md +56 -0
- package/src/discovery/builtin-rules/ts-no-dynamic-import.md +39 -0
- package/src/discovery/builtin-rules/ts-no-return-type.md +45 -0
- package/src/discovery/builtin-rules/ts-no-tiny-functions.md +50 -0
- package/src/discovery/builtin-rules/ts-promise-with-resolvers.md +65 -0
- package/src/discovery/builtin-rules/ts-set-map.md +28 -0
- package/src/discovery/index.ts +1 -0
- package/src/edit/hashline/block-resolver.ts +14 -0
- package/src/edit/hashline/diff.ts +9 -8
- package/src/edit/hashline/execute.ts +2 -1
- package/src/edit/hashline/index.ts +1 -0
- package/src/eval/__tests__/shared-executors.test.ts +36 -0
- package/src/eval/js/shared/local-module-loader.ts +13 -1
- package/src/eval/js/shared/rewrite-imports.ts +31 -26
- package/src/eval/py/kernel.ts +37 -15
- package/src/eval/py/runtime.ts +57 -28
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -12
- package/src/export/ttsr.ts +2 -0
- package/src/internal-urls/agent-protocol.ts +18 -1
- package/src/internal-urls/artifact-protocol.ts +19 -1
- package/src/internal-urls/docs-index.generated.ts +8 -7
- package/src/internal-urls/local-protocol.ts +14 -1
- package/src/internal-urls/memory-protocol.ts +6 -1
- package/src/internal-urls/omp-protocol.ts +5 -1
- package/src/internal-urls/router.ts +20 -1
- package/src/internal-urls/rule-protocol.ts +8 -1
- package/src/internal-urls/skill-protocol.ts +8 -1
- package/src/internal-urls/types.ts +27 -0
- package/src/lsp/render.ts +1 -1
- package/src/main.ts +18 -1
- package/src/mcp/oauth-flow.ts +2 -2
- package/src/memory-backend/index.ts +1 -0
- package/src/memory-backend/resolve.ts +4 -1
- package/src/memory-backend/types.ts +8 -1
- package/src/mnemosyne/backend.ts +374 -0
- package/src/mnemosyne/config.ts +160 -0
- package/src/mnemosyne/index.ts +3 -0
- package/src/mnemosyne/state.ts +548 -0
- package/src/modes/acp/acp-agent.ts +11 -6
- package/src/modes/components/agent-dashboard.ts +4 -4
- package/src/modes/components/custom-editor.ts +3 -2
- package/src/modes/components/diff.ts +2 -2
- package/src/modes/components/extensions/extension-list.ts +3 -2
- package/src/modes/components/footer.ts +5 -6
- package/src/modes/components/history-search.ts +3 -3
- package/src/modes/components/hook-selector.ts +92 -8
- package/src/modes/components/index.ts +2 -0
- package/src/modes/components/mcp-add-wizard.ts +3 -3
- package/src/modes/components/model-selector.ts +5 -4
- package/src/modes/components/oauth-selector.ts +3 -3
- package/src/modes/components/segment-track.ts +52 -0
- package/src/modes/components/session-observer-overlay.ts +19 -13
- package/src/modes/components/session-selector.ts +3 -3
- package/src/modes/components/settings-defs.ts +7 -0
- package/src/modes/components/status-line/context-thresholds.ts +11 -0
- package/src/modes/components/status-line/segments.ts +2 -2
- package/src/modes/components/tiny-title-download-progress.ts +90 -0
- package/src/modes/components/tips.txt +13 -0
- package/src/modes/components/tool-execution.ts +72 -4
- package/src/modes/components/tree-selector.ts +3 -3
- package/src/modes/components/user-message-selector.ts +3 -3
- package/src/modes/components/welcome.ts +102 -43
- package/src/modes/controllers/command-controller.ts +16 -1
- package/src/modes/controllers/extension-ui-controller.ts +3 -1
- package/src/modes/controllers/input-controller.ts +69 -21
- package/src/modes/gradient-highlight.ts +70 -0
- package/src/modes/interactive-mode.ts +75 -114
- package/src/modes/internal-url-autocomplete.ts +143 -0
- package/src/modes/orchestrate.ts +36 -0
- package/src/modes/prompt-action-autocomplete.ts +12 -0
- package/src/modes/setup-wizard/index.ts +88 -0
- package/src/modes/setup-wizard/scenes/glyph.ts +96 -0
- package/src/modes/setup-wizard/scenes/outro.ts +35 -0
- package/src/modes/setup-wizard/scenes/providers.ts +69 -0
- package/src/modes/setup-wizard/scenes/sign-in.ts +193 -0
- package/src/modes/setup-wizard/scenes/splash.ts +201 -0
- package/src/modes/setup-wizard/scenes/theme.ts +299 -0
- package/src/modes/setup-wizard/scenes/types.ts +48 -0
- package/src/modes/setup-wizard/scenes/web-search.ts +128 -0
- package/src/modes/setup-wizard/wizard-overlay.ts +275 -0
- package/src/modes/theme/shimmer.ts +5 -0
- package/src/modes/theme/theme.ts +44 -20
- package/src/modes/types.ts +6 -1
- package/src/modes/ultrathink.ts +9 -53
- package/src/modes/utils/keybinding-matchers.ts +11 -0
- package/src/prompts/system/memory-consolidation-system.md +8 -0
- package/src/prompts/system/memory-extraction-system.md +26 -0
- package/src/prompts/{commands/orchestrate.md → system/orchestrate-notice.md} +6 -17
- package/src/prompts/system/system-prompt.md +2 -0
- package/src/prompts/system/tiny-title-system.md +8 -0
- package/src/prompts/tools/memory-edit.md +8 -0
- package/src/prompts/tools/read.md +4 -0
- package/src/prompts/tools/task.md +4 -7
- package/src/sdk.ts +13 -21
- package/src/session/agent-session.ts +128 -44
- package/src/slash-commands/builtin-registry.ts +18 -1
- package/src/system-prompt.ts +4 -0
- package/src/task/commands.ts +1 -5
- package/src/task/executor.ts +8 -0
- package/src/task/index.ts +2 -0
- package/src/task/render.ts +69 -26
- package/src/tiny/device.ts +117 -0
- package/src/tiny/dtype.ts +101 -0
- package/src/tiny/models.ts +218 -0
- package/src/tiny/text.ts +54 -0
- package/src/tiny/title-client.ts +395 -0
- package/src/tiny/title-protocol.ts +51 -0
- package/src/tiny/worker.ts +587 -0
- package/src/tools/bash.ts +74 -29
- package/src/tools/browser/tab-worker.ts +1 -1
- package/src/tools/eval.ts +9 -4
- package/src/tools/index.ts +17 -22
- package/src/tools/memory-edit.ts +59 -0
- package/src/tools/memory-recall.ts +100 -0
- package/src/tools/memory-reflect.ts +88 -0
- package/src/tools/memory-render.ts +185 -0
- package/src/tools/memory-retain.ts +91 -0
- package/src/tools/read.ts +1 -0
- package/src/tools/renderers.ts +4 -2
- package/src/tools/todo-write.ts +128 -29
- package/src/tools/tool-result.ts +8 -0
- package/src/tui/code-cell.ts +6 -1
- package/src/tui/output-block.ts +199 -38
- package/src/utils/title-generator.ts +115 -13
- package/dist/types/tools/recipe/index.d.ts +0 -46
- package/dist/types/tools/recipe/render.d.ts +0 -36
- package/dist/types/tools/recipe/runner.d.ts +0 -60
- package/dist/types/tools/recipe/runners/cargo.d.ts +0 -16
- package/dist/types/tools/recipe/runners/index.d.ts +0 -2
- package/dist/types/tools/recipe/runners/just.d.ts +0 -2
- package/dist/types/tools/recipe/runners/make.d.ts +0 -2
- package/dist/types/tools/recipe/runners/pkg.d.ts +0 -2
- package/dist/types/tools/recipe/runners/task.d.ts +0 -2
- package/src/prompts/tools/recipe.md +0 -16
- package/src/tools/hindsight-recall.ts +0 -69
- package/src/tools/hindsight-reflect.ts +0 -58
- package/src/tools/hindsight-retain.ts +0 -57
- package/src/tools/recipe/index.ts +0 -81
- package/src/tools/recipe/render.ts +0 -19
- package/src/tools/recipe/runner.ts +0 -219
- package/src/tools/recipe/runners/cargo.ts +0 -131
- package/src/tools/recipe/runners/index.ts +0 -8
- package/src/tools/recipe/runners/just.ts +0 -73
- package/src/tools/recipe/runners/make.ts +0 -101
- package/src/tools/recipe/runners/pkg.ts +0 -167
- package/src/tools/recipe/runners/task.ts +0 -72
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "@oh-my-pi/pi-tui";
|
|
16
16
|
import { getProjectDir, logger, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
17
17
|
import { EDIT_MODE_STRATEGIES, type EditMode, type PerFileDiffPreview } from "../../edit";
|
|
18
|
+
import { shimmerEnabled } from "../../modes/theme/shimmer";
|
|
18
19
|
import type { Theme } from "../../modes/theme/theme";
|
|
19
20
|
import { theme } from "../../modes/theme/theme";
|
|
20
21
|
import { BASH_DEFAULT_PREVIEW_LINES } from "../../tools/bash";
|
|
@@ -31,6 +32,7 @@ import {
|
|
|
31
32
|
} from "../../tools/json-tree";
|
|
32
33
|
import { formatExpandHint, replaceTabs, resolveImageOptions, truncateToWidth } from "../../tools/render-utils";
|
|
33
34
|
import { toolRenderers } from "../../tools/renderers";
|
|
35
|
+
import { TODO_WRITE_STRIKE_TOTAL_FRAMES } from "../../tools/todo-write";
|
|
34
36
|
import { renderStatusLine } from "../../tui";
|
|
35
37
|
import { sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
|
|
36
38
|
import { renderDiff } from "./diff";
|
|
@@ -163,6 +165,8 @@ export class ToolExecutionComponent extends Container {
|
|
|
163
165
|
// Spinner animation for partial task results
|
|
164
166
|
#spinnerFrame?: number;
|
|
165
167
|
#spinnerInterval?: NodeJS.Timeout;
|
|
168
|
+
// Todo write completion strikethrough reveal animation
|
|
169
|
+
#todoStrikeInterval?: NodeJS.Timeout;
|
|
166
170
|
// Track if args are still being streamed (for edit/write spinner)
|
|
167
171
|
#argsComplete = false;
|
|
168
172
|
#renderState: {
|
|
@@ -251,12 +255,24 @@ export class ToolExecutionComponent extends Container {
|
|
|
251
255
|
effectiveArgs = args;
|
|
252
256
|
}
|
|
253
257
|
|
|
254
|
-
// Coalesce duplicate computes for identical args.
|
|
258
|
+
// Coalesce duplicate computes for identical args. The key pairs the
|
|
259
|
+
// streaming flag with a content hash: the final (args-complete) pass
|
|
260
|
+
// computes an untrimmed diff and must run even when the payload is
|
|
261
|
+
// byte-identical to the last streamed chunk — only `isStreaming` differs,
|
|
262
|
+
// and it flips the trailing-line trim. Without the flag a single-line edit
|
|
263
|
+
// whose trailing payload line never gets a newline stays stuck on the
|
|
264
|
+
// trimmed "no changes" streaming preview and renders no diff. Hashing keeps
|
|
265
|
+
// the retained key tiny instead of holding the whole serialized blob.
|
|
266
|
+
const streamingState = this.#argsComplete ? "final" : "stream";
|
|
255
267
|
let argsKey: string;
|
|
256
268
|
try {
|
|
257
|
-
argsKey = JSON.stringify(effectiveArgs)
|
|
269
|
+
argsKey = `${streamingState}:${Bun.hash(JSON.stringify(effectiveArgs))}`;
|
|
258
270
|
} catch {
|
|
259
|
-
|
|
271
|
+
// effectiveArgs isn't JSON-serializable (exotic value in tool args).
|
|
272
|
+
// The raw streamed JSON is a plain string, so hash that instead of a
|
|
273
|
+
// timestamp — a deterministic key keeps the dedup cache working
|
|
274
|
+
// instead of recomputing (and re-reading the file) on every render.
|
|
275
|
+
argsKey = `${streamingState}:partial:${Bun.hash(partialJson ?? "")}`;
|
|
260
276
|
}
|
|
261
277
|
if (argsKey === this.#editDiffLastArgsKey) return;
|
|
262
278
|
this.#editDiffLastArgsKey = argsKey;
|
|
@@ -304,6 +320,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
304
320
|
this.#argsComplete = true;
|
|
305
321
|
}
|
|
306
322
|
this.#updateSpinnerAnimation();
|
|
323
|
+
this.#updateTodoStrikeAnimation();
|
|
307
324
|
this.#updateDisplay();
|
|
308
325
|
// Convert non-PNG images to PNG for Kitty protocol (async)
|
|
309
326
|
this.#maybeConvertImagesForKitty();
|
|
@@ -364,7 +381,10 @@ export class ToolExecutionComponent extends Container {
|
|
|
364
381
|
this.#toolName === "task" &&
|
|
365
382
|
(this.#result?.details as { async?: { state?: string } } | undefined)?.async?.state === "running";
|
|
366
383
|
const isPartialTask = this.#isPartial && this.#toolName === "task" && !isBackgroundAsyncTask;
|
|
367
|
-
|
|
384
|
+
// Sweep the border of bash/eval execution blocks while they're pending.
|
|
385
|
+
const isPendingExecBlock =
|
|
386
|
+
this.#isPartial && shimmerEnabled() && (this.#toolName === "bash" || this.#toolName === "eval");
|
|
387
|
+
const needsSpinner = isStreamingArgs || isPartialTask || isPendingExecBlock;
|
|
368
388
|
if (needsSpinner && !this.#spinnerInterval) {
|
|
369
389
|
this.#spinnerInterval = setInterval(() => {
|
|
370
390
|
const frameCount = theme.spinnerFrames.length;
|
|
@@ -379,6 +399,43 @@ export class ToolExecutionComponent extends Container {
|
|
|
379
399
|
}
|
|
380
400
|
}
|
|
381
401
|
|
|
402
|
+
#updateTodoStrikeAnimation(): void {
|
|
403
|
+
if (this.#toolName !== "todo_write" || this.#isPartial || this.#result?.isError) {
|
|
404
|
+
this.#stopTodoStrikeAnimation();
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const completedTasks = (this.#result?.details as { completedTasks?: unknown[] } | undefined)?.completedTasks;
|
|
408
|
+
if (!completedTasks || completedTasks.length === 0) {
|
|
409
|
+
this.#stopTodoStrikeAnimation();
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (this.#todoStrikeInterval) return;
|
|
413
|
+
|
|
414
|
+
this.#spinnerFrame = 0;
|
|
415
|
+
this.#renderState.spinnerFrame = 0;
|
|
416
|
+
this.#todoStrikeInterval = setInterval(() => {
|
|
417
|
+
const nextFrame = (this.#spinnerFrame ?? 0) + 1;
|
|
418
|
+
if (nextFrame > TODO_WRITE_STRIKE_TOTAL_FRAMES) {
|
|
419
|
+
this.#stopTodoStrikeAnimation();
|
|
420
|
+
} else {
|
|
421
|
+
this.#spinnerFrame = nextFrame;
|
|
422
|
+
this.#renderState.spinnerFrame = nextFrame;
|
|
423
|
+
}
|
|
424
|
+
this.#ui.requestRender();
|
|
425
|
+
}, 65);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
#stopTodoStrikeAnimation(): void {
|
|
429
|
+
if (this.#todoStrikeInterval) {
|
|
430
|
+
clearInterval(this.#todoStrikeInterval);
|
|
431
|
+
this.#todoStrikeInterval = undefined;
|
|
432
|
+
}
|
|
433
|
+
if (!this.#spinnerInterval) {
|
|
434
|
+
this.#spinnerFrame = undefined;
|
|
435
|
+
this.#renderState.spinnerFrame = undefined;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
382
439
|
/**
|
|
383
440
|
* Stop spinner animation and cleanup resources.
|
|
384
441
|
*/
|
|
@@ -388,6 +445,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
388
445
|
this.#spinnerInterval = undefined;
|
|
389
446
|
this.#spinnerFrame = undefined;
|
|
390
447
|
}
|
|
448
|
+
this.#stopTodoStrikeAnimation();
|
|
391
449
|
this.#editDiffAbort?.abort();
|
|
392
450
|
this.#editDiffAbort = undefined;
|
|
393
451
|
}
|
|
@@ -428,6 +486,11 @@ export class ToolExecutionComponent extends Container {
|
|
|
428
486
|
const inline = Boolean((tool as { inline?: boolean }).inline);
|
|
429
487
|
this.#contentBox.setBgFn(inline ? undefined : bgFn);
|
|
430
488
|
this.#contentBox.clear();
|
|
489
|
+
// Mirror the built-in renderer branch so custom renderers (notably the
|
|
490
|
+
// task tool, whose live instance routes through here) receive the same
|
|
491
|
+
// render context — e.g. the `hasResult` flag that suppresses the task
|
|
492
|
+
// call preview once result lines exist.
|
|
493
|
+
this.#renderState.renderContext = this.#buildRenderContext();
|
|
431
494
|
|
|
432
495
|
// Render call component
|
|
433
496
|
const shouldRenderCall = !this.#result || !mergeCallAndResult;
|
|
@@ -696,6 +759,11 @@ export class ToolExecutionComponent extends Container {
|
|
|
696
759
|
context.output = output;
|
|
697
760
|
context.expanded = this.#expanded;
|
|
698
761
|
context.previewLines = EVAL_DEFAULT_PREVIEW_LINES;
|
|
762
|
+
} else if (this.#toolName === "task") {
|
|
763
|
+
// Once a result snapshot exists the task renderer's `renderResult`
|
|
764
|
+
// draws every dispatched agent as a progress/result line, so tell
|
|
765
|
+
// `renderCall` to drop its duplicate streaming preview list.
|
|
766
|
+
context.hasResult = Boolean(this.#result);
|
|
699
767
|
} else if (isEditLikeToolName(this.#toolName)) {
|
|
700
768
|
context.editMode = this.#editMode;
|
|
701
769
|
const previews = this.#editDiffPreview;
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from "@oh-my-pi/pi-tui";
|
|
13
13
|
import type { TreeFilterMode } from "../../config/settings-schema";
|
|
14
14
|
import { theme } from "../../modes/theme/theme";
|
|
15
|
-
import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
|
|
15
|
+
import { matchesAppInterrupt, matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
|
|
16
16
|
import type { SessionTreeNode } from "../../session/session-manager";
|
|
17
17
|
import { shortenPath } from "../../tools/render-utils";
|
|
18
18
|
import { toPathList } from "../../tools/search";
|
|
@@ -718,9 +718,9 @@ class TreeList implements Component {
|
|
|
718
718
|
}
|
|
719
719
|
|
|
720
720
|
handleInput(keyData: string): void {
|
|
721
|
-
if (
|
|
721
|
+
if (matchesSelectUp(keyData)) {
|
|
722
722
|
this.#selectedIndex = this.#selectedIndex === 0 ? this.#filteredNodes.length - 1 : this.#selectedIndex - 1;
|
|
723
|
-
} else if (
|
|
723
|
+
} else if (matchesSelectDown(keyData)) {
|
|
724
724
|
this.#selectedIndex = this.#selectedIndex === this.#filteredNodes.length - 1 ? 0 : this.#selectedIndex + 1;
|
|
725
725
|
} else if (matchesKey(keyData, "left")) {
|
|
726
726
|
// Page up
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Component, Container, matchesKey, Spacer, Text, truncateToWidth } from "@oh-my-pi/pi-tui";
|
|
2
2
|
import { theme } from "../../modes/theme/theme";
|
|
3
|
-
import { matchesSelectCancel } from "../../modes/utils/keybinding-matchers";
|
|
3
|
+
import { matchesSelectCancel, matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
|
|
4
4
|
import { DynamicBorder } from "./dynamic-border";
|
|
5
5
|
|
|
6
6
|
interface UserMessageItem {
|
|
@@ -78,11 +78,11 @@ class UserMessageList implements Component {
|
|
|
78
78
|
|
|
79
79
|
handleInput(keyData: string): void {
|
|
80
80
|
// Up arrow - go to previous (older) message, wrap to bottom when at top
|
|
81
|
-
if (
|
|
81
|
+
if (matchesSelectUp(keyData)) {
|
|
82
82
|
this.#selectedIndex = this.#selectedIndex === 0 ? this.messages.length - 1 : this.#selectedIndex - 1;
|
|
83
83
|
}
|
|
84
84
|
// Down arrow - go to next (newer) message, wrap to top when at bottom
|
|
85
|
-
else if (
|
|
85
|
+
else if (matchesSelectDown(keyData)) {
|
|
86
86
|
this.#selectedIndex = this.#selectedIndex === this.messages.length - 1 ? 0 : this.#selectedIndex + 1;
|
|
87
87
|
}
|
|
88
88
|
// Enter - select message and branch
|
|
@@ -1,6 +1,45 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type Component,
|
|
3
|
+
padding,
|
|
4
|
+
replaceTabs,
|
|
5
|
+
TERMINAL,
|
|
6
|
+
truncateToWidth,
|
|
7
|
+
visibleWidth,
|
|
8
|
+
wrapTextWithAnsi,
|
|
9
|
+
} from "@oh-my-pi/pi-tui";
|
|
2
10
|
import { APP_NAME } from "@oh-my-pi/pi-utils";
|
|
3
11
|
import { theme } from "../../modes/theme/theme";
|
|
12
|
+
import tipsText from "./tips.txt" with { type: "text" };
|
|
13
|
+
|
|
14
|
+
/** Tips embedded at build time, one per line; blanks dropped. */
|
|
15
|
+
const TIPS: readonly string[] = tipsText
|
|
16
|
+
.split("\n")
|
|
17
|
+
.map(line => line.trim())
|
|
18
|
+
.filter(line => line.length > 0);
|
|
19
|
+
|
|
20
|
+
export function renderWelcomeTip(tip: string, boxWidth: number): string[] {
|
|
21
|
+
const label = "Tip: ";
|
|
22
|
+
const labelWidth = visibleWidth(label);
|
|
23
|
+
const bodyBudget = boxWidth - 1 - labelWidth; // 1 = leading indent
|
|
24
|
+
if (bodyBudget < 8) return [];
|
|
25
|
+
|
|
26
|
+
const wrappedBody = wrapTextWithAnsi(replaceTabs(tip), bodyBudget);
|
|
27
|
+
if (wrappedBody.length === 0) return [];
|
|
28
|
+
|
|
29
|
+
const encoding = TERMINAL.trueColor ? "ansi-16m" : "ansi-256";
|
|
30
|
+
const purple = Bun.color("#b48cff", encoding) ?? "";
|
|
31
|
+
const lightBlue = Bun.color("#9ccfff", encoding) ?? "";
|
|
32
|
+
const italic = "\x1b[3m";
|
|
33
|
+
const dim = "\x1b[2m";
|
|
34
|
+
const reset = "\x1b[0m";
|
|
35
|
+
const continuationIndent = padding(labelWidth);
|
|
36
|
+
|
|
37
|
+
return wrappedBody.map((body, index) =>
|
|
38
|
+
index === 0
|
|
39
|
+
? ` ${italic}${purple}${label}${dim}${lightBlue}${body}${reset}`
|
|
40
|
+
: ` ${italic}${continuationIndent}${dim}${lightBlue}${body}${reset}`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
4
43
|
|
|
5
44
|
export interface RecentSession {
|
|
6
45
|
name: string;
|
|
@@ -19,6 +58,8 @@ export interface LspServerInfo {
|
|
|
19
58
|
export class WelcomeComponent implements Component {
|
|
20
59
|
#animStart: number | null = null;
|
|
21
60
|
#animTimer: ReturnType<typeof setInterval> | null = null;
|
|
61
|
+
/** Tip chosen once per instance so re-renders (intro, LSP updates) don't shuffle it. */
|
|
62
|
+
readonly #tip: string | undefined = TIPS.length > 0 ? TIPS[Math.floor(Math.random() * TIPS.length)] : undefined;
|
|
22
63
|
|
|
23
64
|
constructor(
|
|
24
65
|
private readonly version: string,
|
|
@@ -212,9 +253,22 @@ export class WelcomeComponent implements Component {
|
|
|
212
253
|
lines.push(bl + h.repeat(leftCol) + br);
|
|
213
254
|
}
|
|
214
255
|
|
|
256
|
+
// Randomly picked tip, rendered directly beneath the box.
|
|
257
|
+
lines.push(...this.#renderTip(boxWidth));
|
|
258
|
+
|
|
215
259
|
return lines;
|
|
216
260
|
}
|
|
217
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Render the per-instance tip line: a purple "Tip:" label followed by the
|
|
264
|
+
* tip body in dimmed light blue, the whole line italicized. Returns `[]`
|
|
265
|
+
* when no tip is available or the box is too narrow to be useful.
|
|
266
|
+
*/
|
|
267
|
+
#renderTip(boxWidth: number): string[] {
|
|
268
|
+
if (!this.#tip) return [];
|
|
269
|
+
return renderWelcomeTip(this.#tip, boxWidth);
|
|
270
|
+
}
|
|
271
|
+
|
|
218
272
|
/** Center text within a given width */
|
|
219
273
|
#centerText(text: string, width: number): string {
|
|
220
274
|
const visLen = visibleWidth(text);
|
|
@@ -271,8 +325,7 @@ export class WelcomeComponent implements Component {
|
|
|
271
325
|
}
|
|
272
326
|
}
|
|
273
327
|
|
|
274
|
-
|
|
275
|
-
const PI_LOGO = ["▀██████████▀", " ╘██ ██ ", " ██ ██ ", " ██ ██ ", " ▄██▄ ▄██▄ "];
|
|
328
|
+
export const PI_LOGO = ["▀██████████▀", " ╘██ ██ ", " ██ ██ ", " ██ ██ ", " ▄██▄ ▄██▄ "];
|
|
276
329
|
|
|
277
330
|
/** Multi-stop palette for the diagonal gradient. */
|
|
278
331
|
const GRADIENT_STOPS: ReadonlyArray<readonly [number, number, number]> = [
|
|
@@ -289,63 +342,69 @@ const GRADIENT_RAMP_256 = [199, 171, 135, 99, 75, 51, 87];
|
|
|
289
342
|
/** Half-width of the shine highlight band, expressed in gradient-t units. */
|
|
290
343
|
const SHINE_HALF_WIDTH = 0.18;
|
|
291
344
|
|
|
292
|
-
interface ShineConfig {
|
|
345
|
+
export interface ShineConfig {
|
|
293
346
|
/** Overall opacity of the shine overlay, in [0, 1]. */
|
|
294
347
|
strength: number;
|
|
295
348
|
/** Center of the shine band along the diagonal, in [0, 1]. */
|
|
296
349
|
pos: number;
|
|
297
350
|
}
|
|
298
351
|
|
|
352
|
+
/**
|
|
353
|
+
* Resolve the gradient SGR foreground escape for a normalized position `t`
|
|
354
|
+
* (0..1) along the diagonal, compositing the optional sliding shine highlight.
|
|
355
|
+
* Shared by {@link gradientLogo} and the setup splash so both stay
|
|
356
|
+
* color-identical (truecolor when available, 256-color ramp otherwise).
|
|
357
|
+
*/
|
|
358
|
+
export function gradientEscape(t: number, shine?: ShineConfig): string {
|
|
359
|
+
const shineStrength = shine && shine.strength > 0 ? shine.strength : 0;
|
|
360
|
+
const shinePos = shine ? shine.pos : 0;
|
|
361
|
+
if (TERMINAL.trueColor) {
|
|
362
|
+
// 5-stop palette widens the visible color range and avoids the
|
|
363
|
+
// deep-blue valley a naive HSL lerp falls into.
|
|
364
|
+
const stops = GRADIENT_STOPS;
|
|
365
|
+
const seg = t * (stops.length - 1);
|
|
366
|
+
const i = Math.min(stops.length - 2, Math.floor(seg));
|
|
367
|
+
const f = seg - i;
|
|
368
|
+
const a = stops[i];
|
|
369
|
+
const b = stops[i + 1];
|
|
370
|
+
let r = a[0] + (b[0] - a[0]) * f;
|
|
371
|
+
let g = a[1] + (b[1] - a[1]) * f;
|
|
372
|
+
let bl = a[2] + (b[2] - a[2]) * f;
|
|
373
|
+
if (shineStrength > 0) {
|
|
374
|
+
const dist = Math.abs(t - shinePos);
|
|
375
|
+
const intensity = Math.max(0, 1 - dist / SHINE_HALF_WIDTH) * shineStrength;
|
|
376
|
+
if (intensity > 0) {
|
|
377
|
+
r += (255 - r) * intensity;
|
|
378
|
+
g += (255 - g) * intensity;
|
|
379
|
+
bl += (255 - bl) * intensity;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return `\x1b[38;2;${Math.round(r)};${Math.round(g)};${Math.round(bl)}m`;
|
|
383
|
+
}
|
|
384
|
+
const ramp = GRADIENT_RAMP_256;
|
|
385
|
+
let idx = Math.min(ramp.length - 1, Math.max(0, Math.floor(t * (ramp.length - 1) + 0.5)));
|
|
386
|
+
if (shineStrength > 0) {
|
|
387
|
+
const dist = Math.abs(t - shinePos);
|
|
388
|
+
const intensity = Math.max(0, 1 - dist / SHINE_HALF_WIDTH) * shineStrength;
|
|
389
|
+
// Promote to the brightest ramp slot when the shine band peaks here.
|
|
390
|
+
if (intensity > 0.5) idx = ramp.length - 1;
|
|
391
|
+
}
|
|
392
|
+
return `\x1b[38;5;${ramp[idx]}m`;
|
|
393
|
+
}
|
|
394
|
+
|
|
299
395
|
/**
|
|
300
396
|
* Apply a multi-stop diagonal gradient (bottom-left → top-right) plus an
|
|
301
397
|
* optional sliding shine band across multi-line art. `phase` (0..1) shifts the
|
|
302
398
|
* gradient along the diagonal, wrapping at 1. When `shine` is provided, a soft
|
|
303
399
|
* white highlight is composited on top, centered at `shine.pos`.
|
|
304
400
|
*/
|
|
305
|
-
function gradientLogo(lines: readonly string[], phase = 0, shine?: ShineConfig): string[] {
|
|
401
|
+
export function gradientLogo(lines: readonly string[], phase = 0, shine?: ShineConfig): string[] {
|
|
306
402
|
const reset = "\x1b[0m";
|
|
307
403
|
const rows = lines.length;
|
|
308
404
|
const cols = Math.max(...lines.map(l => l.length));
|
|
309
405
|
// span+1 so `base` stays strictly < 1: avoids the wrap-around at the
|
|
310
406
|
// far corner mapping back to t=0 (hot pink) on the resting frame.
|
|
311
407
|
const span = Math.max(1, cols + rows - 1);
|
|
312
|
-
const shineStrength = shine && shine.strength > 0 ? shine.strength : 0;
|
|
313
|
-
const shinePos = shine ? shine.pos : 0;
|
|
314
|
-
const colorAt = TERMINAL.trueColor
|
|
315
|
-
? (t: number): string => {
|
|
316
|
-
// 5-stop palette widens the visible color range and avoids the
|
|
317
|
-
// deep-blue valley a naive HSL lerp falls into.
|
|
318
|
-
const stops = GRADIENT_STOPS;
|
|
319
|
-
const seg = t * (stops.length - 1);
|
|
320
|
-
const i = Math.min(stops.length - 2, Math.floor(seg));
|
|
321
|
-
const f = seg - i;
|
|
322
|
-
const a = stops[i];
|
|
323
|
-
const b = stops[i + 1];
|
|
324
|
-
let r = a[0] + (b[0] - a[0]) * f;
|
|
325
|
-
let g = a[1] + (b[1] - a[1]) * f;
|
|
326
|
-
let bl = a[2] + (b[2] - a[2]) * f;
|
|
327
|
-
if (shineStrength > 0) {
|
|
328
|
-
const dist = Math.abs(t - shinePos);
|
|
329
|
-
const intensity = Math.max(0, 1 - dist / SHINE_HALF_WIDTH) * shineStrength;
|
|
330
|
-
if (intensity > 0) {
|
|
331
|
-
r += (255 - r) * intensity;
|
|
332
|
-
g += (255 - g) * intensity;
|
|
333
|
-
bl += (255 - bl) * intensity;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
return `\x1b[38;2;${Math.round(r)};${Math.round(g)};${Math.round(bl)}m`;
|
|
337
|
-
}
|
|
338
|
-
: (t: number): string => {
|
|
339
|
-
const ramp = GRADIENT_RAMP_256;
|
|
340
|
-
let idx = Math.min(ramp.length - 1, Math.max(0, Math.floor(t * (ramp.length - 1) + 0.5)));
|
|
341
|
-
if (shineStrength > 0) {
|
|
342
|
-
const dist = Math.abs(t - shinePos);
|
|
343
|
-
const intensity = Math.max(0, 1 - dist / SHINE_HALF_WIDTH) * shineStrength;
|
|
344
|
-
// Promote to the brightest ramp slot when the shine band peaks here.
|
|
345
|
-
if (intensity > 0.5) idx = ramp.length - 1;
|
|
346
|
-
}
|
|
347
|
-
return `\x1b[38;5;${ramp[idx]}m`;
|
|
348
|
-
};
|
|
349
408
|
return lines.map((line, y) => {
|
|
350
409
|
let result = "";
|
|
351
410
|
for (let x = 0; x < line.length; x++) {
|
|
@@ -357,7 +416,7 @@ function gradientLogo(lines: readonly string[], phase = 0, shine?: ShineConfig):
|
|
|
357
416
|
// Diagonal: bottom-left (x=0, y=rows-1) → top-right (x=cols-1, y=0)
|
|
358
417
|
const base = (x + (rows - 1 - y)) / span;
|
|
359
418
|
const t = (((base + phase) % 1) + 1) % 1;
|
|
360
|
-
result +=
|
|
419
|
+
result += gradientEscape(t, shine) + char + reset;
|
|
361
420
|
}
|
|
362
421
|
return result;
|
|
363
422
|
});
|
|
@@ -620,12 +620,27 @@ export class CommandController {
|
|
|
620
620
|
return;
|
|
621
621
|
}
|
|
622
622
|
|
|
623
|
+
if (action === "stats" || action === "diagnose") {
|
|
624
|
+
const hook = action === "stats" ? backend.stats : backend.diagnose;
|
|
625
|
+
try {
|
|
626
|
+
const payload = await hook?.(agentDir, this.ctx.sessionManager.getCwd(), this.ctx.session);
|
|
627
|
+
if (!payload) {
|
|
628
|
+
this.ctx.showWarning(`Memory ${action} is not available for the ${backend.id} backend.`);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
showMarkdownPanel(this.ctx, `Memory ${action === "stats" ? "Stats" : "Diagnostics"}`, payload);
|
|
632
|
+
} catch (error) {
|
|
633
|
+
this.ctx.showError(`Memory ${action} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
634
|
+
}
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
|
|
623
638
|
if (action === "mm") {
|
|
624
639
|
await this.#handleMentalModelsSubcommand(argumentText);
|
|
625
640
|
return;
|
|
626
641
|
}
|
|
627
642
|
|
|
628
|
-
this.ctx.showError("Usage: /memory <view|clear|reset|enqueue|rebuild|mm ...>");
|
|
643
|
+
this.ctx.showError("Usage: /memory <view|stats|diagnose|clear|reset|enqueue|rebuild|mm ...>");
|
|
629
644
|
}
|
|
630
645
|
|
|
631
646
|
async #handleMentalModelsSubcommand(argumentText: string): Promise<void> {
|
|
@@ -19,7 +19,7 @@ import type {
|
|
|
19
19
|
import { getSessionSlashCommands } from "../../extensibility/extensions/get-commands-handler";
|
|
20
20
|
import { HookEditorComponent } from "../../modes/components/hook-editor";
|
|
21
21
|
import { HookInputComponent } from "../../modes/components/hook-input";
|
|
22
|
-
import { HookSelectorComponent } from "../../modes/components/hook-selector";
|
|
22
|
+
import { HookSelectorComponent, type HookSelectorSlider } from "../../modes/components/hook-selector";
|
|
23
23
|
import { getAvailableThemesWithPaths, getThemeByName, setTheme, type Theme, theme } from "../../modes/theme/theme";
|
|
24
24
|
import type { InteractiveModeContext } from "../../modes/types";
|
|
25
25
|
import { setSessionTerminalTitle, setTerminalTitle } from "../../utils/title-generator";
|
|
@@ -583,6 +583,7 @@ export class ExtensionUiController {
|
|
|
583
583
|
title: string,
|
|
584
584
|
options: string[],
|
|
585
585
|
dialogOptions?: ExtensionUIDialogOptions,
|
|
586
|
+
extra?: { slider?: HookSelectorSlider },
|
|
586
587
|
): Promise<string | undefined> {
|
|
587
588
|
const { promise, finish, attachAbort } = this.#createHookDialogState(
|
|
588
589
|
() => this.hideHookSelector(),
|
|
@@ -623,6 +624,7 @@ export class ExtensionUiController {
|
|
|
623
624
|
tui: this.ctx.ui,
|
|
624
625
|
outline: dialogOptions?.outline,
|
|
625
626
|
maxVisible,
|
|
627
|
+
slider: extra?.slider,
|
|
626
628
|
},
|
|
627
629
|
);
|
|
628
630
|
this.ctx.editorContainer.clear();
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
|
-
import {
|
|
2
|
+
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
3
3
|
import type { AutocompleteProvider, SlashCommand } from "@oh-my-pi/pi-tui";
|
|
4
4
|
import { $env, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { getRoleInfo } from "../../config/model-registry";
|
|
5
6
|
import { isSettingsInitialized, settings } from "../../config/settings";
|
|
7
|
+
import { renderSegmentTrack } from "../../modes/components/segment-track";
|
|
8
|
+
import { TinyTitleDownloadProgressComponent } from "../../modes/components/tiny-title-download-progress";
|
|
6
9
|
import { expandEmoticons } from "../../modes/emoji-autocomplete";
|
|
7
10
|
import { createPromptActionAutocompleteProvider } from "../../modes/prompt-action-autocomplete";
|
|
8
11
|
import { theme } from "../../modes/theme/theme";
|
|
@@ -10,6 +13,9 @@ import type { InteractiveModeContext } from "../../modes/types";
|
|
|
10
13
|
import type { AgentSessionEvent } from "../../session/agent-session";
|
|
11
14
|
import { SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../../session/messages";
|
|
12
15
|
import { executeBuiltinSlashCommand } from "../../slash-commands/builtin-registry";
|
|
16
|
+
import { isTinyTitleLocalModelKey } from "../../tiny/models";
|
|
17
|
+
import { tinyTitleClient } from "../../tiny/title-client";
|
|
18
|
+
import type { TinyTitleProgressEvent } from "../../tiny/title-protocol";
|
|
13
19
|
import { copyToClipboard, readImageFromClipboard } from "../../utils/clipboard";
|
|
14
20
|
import { getEditorCommand, openInEditor } from "../../utils/external-editor";
|
|
15
21
|
import { ensureSupportedImageInput } from "../../utils/image-loading";
|
|
@@ -24,9 +30,61 @@ function isExpandable(obj: unknown): obj is Expandable {
|
|
|
24
30
|
return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
|
|
25
31
|
}
|
|
26
32
|
|
|
33
|
+
const TINY_TITLE_PROGRESS_DONE_TTL_MS = 3_000;
|
|
34
|
+
// A cached model fires its file-load events in a short burst and then goes silent
|
|
35
|
+
// while onnxruntime builds the session; a genuine download keeps streaming progress
|
|
36
|
+
// events for seconds. Only reveal the bar once a still-incomplete event arrives after
|
|
37
|
+
// this grace window, so an already-downloaded model never flashes the bar.
|
|
38
|
+
const TINY_TITLE_PROGRESS_REVEAL_DELAY_MS = 1_000;
|
|
39
|
+
|
|
27
40
|
export class InputController {
|
|
28
41
|
constructor(private ctx: InteractiveModeContext) {}
|
|
29
42
|
|
|
43
|
+
#showTinyTitleDownloadProgress(modelKey: string): void {
|
|
44
|
+
if (!isTinyTitleLocalModelKey(modelKey) || this.ctx.isBackgrounded) return;
|
|
45
|
+
const component = new TinyTitleDownloadProgressComponent(modelKey);
|
|
46
|
+
let added = false;
|
|
47
|
+
let disposed = false;
|
|
48
|
+
let removeTimer: NodeJS.Timeout | undefined;
|
|
49
|
+
const remove = (): void => {
|
|
50
|
+
if (disposed) return;
|
|
51
|
+
disposed = true;
|
|
52
|
+
unsubscribe();
|
|
53
|
+
if (removeTimer) {
|
|
54
|
+
clearTimeout(removeTimer);
|
|
55
|
+
removeTimer = undefined;
|
|
56
|
+
}
|
|
57
|
+
if (added) {
|
|
58
|
+
this.ctx.chatContainer.removeChild(component);
|
|
59
|
+
this.ctx.ui.requestRender();
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const scheduleRemove = (): void => {
|
|
63
|
+
if (removeTimer) clearTimeout(removeTimer);
|
|
64
|
+
removeTimer = setTimeout(remove, TINY_TITLE_PROGRESS_DONE_TTL_MS);
|
|
65
|
+
removeTimer.unref?.();
|
|
66
|
+
};
|
|
67
|
+
let revealAt = 0;
|
|
68
|
+
const update = (event: TinyTitleProgressEvent): void => {
|
|
69
|
+
if (disposed || event.modelKey !== modelKey) return;
|
|
70
|
+
component.update(event);
|
|
71
|
+
if (revealAt === 0) revealAt = performance.now() + TINY_TITLE_PROGRESS_REVEAL_DELAY_MS;
|
|
72
|
+
const complete = component.isComplete();
|
|
73
|
+
// Reveal only for a download still in flight past the grace window. Cache hits
|
|
74
|
+
// either complete or fall silent (onnx init emits no events) before this fires.
|
|
75
|
+
if (!added && !complete && performance.now() >= revealAt) {
|
|
76
|
+
this.ctx.chatContainer.addChild(component);
|
|
77
|
+
added = true;
|
|
78
|
+
}
|
|
79
|
+
if (added) this.ctx.ui.requestRender();
|
|
80
|
+
if (complete) {
|
|
81
|
+
if (added) scheduleRemove();
|
|
82
|
+
else remove();
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const unsubscribe = tinyTitleClient.onProgress(update);
|
|
86
|
+
}
|
|
87
|
+
|
|
30
88
|
setupKeyHandlers(): void {
|
|
31
89
|
this.ctx.editor.setActionKeys("app.interrupt", this.ctx.keybindings.getKeys("app.interrupt"));
|
|
32
90
|
this.ctx.editor.shouldBypassAutocompleteOnEscape = () =>
|
|
@@ -329,6 +387,7 @@ export class InputController {
|
|
|
329
387
|
// Generate session title on first message
|
|
330
388
|
const hasUserMessages = this.ctx.session.messages.some((m: AgentMessage) => m.role === "user");
|
|
331
389
|
if (!hasUserMessages && !this.ctx.sessionManager.getSessionName() && !$env.PI_NO_TITLE) {
|
|
390
|
+
this.#showTinyTitleDownloadProgress(this.ctx.settings.get("providers.tinyModel"));
|
|
332
391
|
const registry = this.ctx.session.modelRegistry;
|
|
333
392
|
generateSessionTitle(
|
|
334
393
|
text,
|
|
@@ -712,27 +771,16 @@ export class InputController {
|
|
|
712
771
|
|
|
713
772
|
this.ctx.statusLine.invalidate();
|
|
714
773
|
this.ctx.updateEditorBorderColor();
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
const cycleSeparator = theme.fg("dim", " > ");
|
|
723
|
-
const cycleLabel = cycleOrder
|
|
724
|
-
.map(role => {
|
|
725
|
-
if (role === result.role) {
|
|
726
|
-
return theme.bold(theme.fg("accent", role));
|
|
727
|
-
}
|
|
728
|
-
return theme.fg("muted", role);
|
|
729
|
-
})
|
|
730
|
-
.join(cycleSeparator);
|
|
731
|
-
const orderLabel = ` (cycle: ${cycleLabel})`;
|
|
732
|
-
this.ctx.showStatus(
|
|
733
|
-
`Switched to ${roleLabelStyled}: ${result.model.name || result.model.id}${thinkingStr}${tempLabel}${orderLabel}`,
|
|
734
|
-
{ dim: false },
|
|
774
|
+
// The status line already reports the resolved model + thinking level, so
|
|
775
|
+
// the cycle status is just a status-line-style chip track (active role
|
|
776
|
+
// filled), matching the plan-approval model slider. A dim suffix flags a
|
|
777
|
+
// temporary switch since that isn't shown elsewhere.
|
|
778
|
+
const track = renderSegmentTrack(
|
|
779
|
+
cycleOrder.map(role => ({ label: role, color: getRoleInfo(role, settings).color })),
|
|
780
|
+
cycleOrder.indexOf(result.role),
|
|
735
781
|
);
|
|
782
|
+
const tempLabel = options?.temporary ? theme.fg("dim", " (temporary)") : "";
|
|
783
|
+
this.ctx.showStatus(`${track}${tempLabel}`, { dim: false });
|
|
736
784
|
} catch (error) {
|
|
737
785
|
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
738
786
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { theme } from "./theme/theme";
|
|
2
|
+
|
|
3
|
+
const FG_RESET = "\x1b[39m";
|
|
4
|
+
|
|
5
|
+
/** Declarative spec for {@link createGradientHighlighter}. */
|
|
6
|
+
export interface GradientHighlightSpec {
|
|
7
|
+
/** Cheap, stateless presence probe used to skip the boundary regex on most lines. Must be non-global. */
|
|
8
|
+
probe: RegExp;
|
|
9
|
+
/** Global, word-bounded match regex walked by `.replace`. */
|
|
10
|
+
highlight: RegExp;
|
|
11
|
+
/** Number of color stops swept across the gradient. */
|
|
12
|
+
stops: number;
|
|
13
|
+
/** Maps a normalized position `t` in [0, 1) to an HSL hue in degrees. */
|
|
14
|
+
hue: (t: number) => number;
|
|
15
|
+
/** HSL saturation percentage. Default 90. */
|
|
16
|
+
saturation?: number;
|
|
17
|
+
/** HSL lightness percentage. Default 62. */
|
|
18
|
+
lightness?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Build a stateless highlighter that paints each standalone match of `highlight`
|
|
23
|
+
* with a smooth HSL gradient for editor display. The returned function adds only
|
|
24
|
+
* zero-width SGR escapes — the visible width is unchanged — and returns the input
|
|
25
|
+
* untouched when `probe` does not match. The palette is compiled lazily and
|
|
26
|
+
* memoized per active color mode.
|
|
27
|
+
*/
|
|
28
|
+
export function createGradientHighlighter(spec: GradientHighlightSpec): (text: string) => string {
|
|
29
|
+
const { probe, highlight, stops, hue, saturation = 90, lightness = 62 } = spec;
|
|
30
|
+
|
|
31
|
+
let cachedMode: string | undefined;
|
|
32
|
+
let cachedPalette: readonly string[] | undefined;
|
|
33
|
+
|
|
34
|
+
/** Gradient foreground escapes for the active color mode, compiled once per mode. */
|
|
35
|
+
const palette = (): readonly string[] => {
|
|
36
|
+
const mode = theme.getColorMode();
|
|
37
|
+
if (cachedPalette && cachedMode === mode) return cachedPalette;
|
|
38
|
+
const format = mode === "truecolor" ? "ansi-16m" : "ansi-256";
|
|
39
|
+
const next: string[] = [];
|
|
40
|
+
for (let i = 0; i < stops; i++) {
|
|
41
|
+
next.push(Bun.color(`hsl(${Math.round(hue(i / stops))}, ${saturation}%, ${lightness}%)`, format) ?? "");
|
|
42
|
+
}
|
|
43
|
+
cachedMode = mode;
|
|
44
|
+
cachedPalette = next;
|
|
45
|
+
return next;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** Paint each character of `word` with the next gradient stop, resetting fg after. */
|
|
49
|
+
const paint = (word: string): string => {
|
|
50
|
+
const stopsArr = palette();
|
|
51
|
+
const n = word.length;
|
|
52
|
+
let out = "";
|
|
53
|
+
let prev = "";
|
|
54
|
+
for (let i = 0; i < n; i++) {
|
|
55
|
+
const color = stopsArr[Math.floor((i / n) * stopsArr.length)] ?? stopsArr[0] ?? "";
|
|
56
|
+
// Coalesce consecutive characters that resolve to the same stop.
|
|
57
|
+
if (color !== prev) {
|
|
58
|
+
out += color;
|
|
59
|
+
prev = color;
|
|
60
|
+
}
|
|
61
|
+
out += word[i];
|
|
62
|
+
}
|
|
63
|
+
return `${out}${FG_RESET}`;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return (text: string): string => {
|
|
67
|
+
if (!probe.test(text)) return text;
|
|
68
|
+
return text.replace(highlight, paint);
|
|
69
|
+
};
|
|
70
|
+
}
|