@oh-my-pi/pi-coding-agent 15.11.3 → 15.11.4
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 +54 -0
- package/dist/cli.js +353 -294
- package/dist/types/config/api-key-resolver.d.ts +9 -3
- package/dist/types/config/keybindings.d.ts +1 -1
- package/dist/types/config/model-discovery.d.ts +6 -4
- package/dist/types/config/model-registry.d.ts +7 -4
- package/dist/types/config/settings-schema.d.ts +458 -155
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/mnemopi/config.d.ts +3 -1
- package/dist/types/modes/components/settings-defs.d.ts +9 -2
- package/dist/types/modes/components/settings-selector.d.ts +9 -4
- package/dist/types/modes/components/tool-execution.d.ts +12 -1
- package/dist/types/modes/components/transcript-container.d.ts +12 -0
- package/dist/types/modes/controllers/input-controller.d.ts +9 -1
- package/dist/types/modes/theme/theme.d.ts +23 -3
- package/dist/types/session/agent-session.d.ts +14 -7
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/snapcompact-inline.d.ts +28 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +14 -0
- package/dist/types/system-prompt.d.ts +3 -1
- package/dist/types/task/render.d.ts +16 -6
- package/dist/types/tools/gh.d.ts +3 -0
- package/dist/types/tools/render-utils.d.ts +8 -16
- package/dist/types/utils/session-color.d.ts +15 -3
- package/dist/types/web/kagi.d.ts +1 -2
- package/dist/types/web/search/providers/codex.d.ts +1 -1
- package/dist/types/web/search/providers/gemini.d.ts +9 -6
- package/package.json +11 -11
- package/src/auto-thinking/classifier.ts +1 -5
- package/src/commit/model-selection.ts +3 -6
- package/src/config/api-key-resolver.ts +10 -3
- package/src/config/keybindings.ts +1 -1
- package/src/config/model-discovery.ts +60 -46
- package/src/config/model-registry.ts +21 -8
- package/src/config/model-resolver.ts +57 -3
- package/src/config/settings-schema.ts +601 -153
- package/src/eval/completion-bridge.ts +1 -5
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +13 -6
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/internal-urls/issue-pr-protocol.ts +10 -4
- package/src/memories/index.ts +2 -10
- package/src/mnemopi/backend.ts +30 -8
- package/src/mnemopi/config.ts +6 -1
- package/src/mnemopi/state.ts +6 -0
- package/src/modes/components/extensions/inspector-panel.ts +6 -2
- package/src/modes/components/plan-review-overlay.ts +15 -17
- package/src/modes/components/plugin-settings.ts +22 -5
- package/src/modes/components/settings-defs.ts +19 -4
- package/src/modes/components/settings-selector.ts +493 -93
- package/src/modes/components/status-line/component.ts +3 -1
- package/src/modes/components/status-line/segments.ts +3 -1
- package/src/modes/components/tool-execution.ts +69 -12
- package/src/modes/components/transcript-container.ts +26 -0
- package/src/modes/components/tree-selector.ts +16 -6
- package/src/modes/controllers/command-controller.ts +37 -7
- package/src/modes/controllers/event-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +68 -6
- package/src/modes/controllers/selector-controller.ts +81 -61
- package/src/modes/interactive-mode.ts +4 -2
- package/src/modes/rpc/rpc-mode.ts +2 -1
- package/src/modes/shared.ts +2 -0
- package/src/modes/theme/theme.ts +100 -7
- package/src/modes/utils/context-usage.ts +3 -1
- package/src/modes/utils/hotkeys-markdown.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +9 -5
- package/src/prompts/system/personalities/default.md +26 -0
- package/src/prompts/system/personalities/friendly.md +17 -0
- package/src/prompts/system/personalities/pragmatic.md +15 -0
- package/src/prompts/system/snapcompact-system-frames-note.md +1 -0
- package/src/prompts/system/snapcompact-system-stub.md +1 -0
- package/src/prompts/system/snapcompact-toolresult-note.md +1 -0
- package/src/prompts/system/system-prompt.md +5 -22
- package/src/prompts/tools/task.md +3 -3
- package/src/sdk.ts +22 -1
- package/src/session/agent-session.ts +91 -24
- package/src/session/auth-storage.ts +1 -0
- package/src/session/session-dump-format.ts +8 -1
- package/src/session/session-manager.ts +5 -5
- package/src/session/snapcompact-inline.ts +187 -0
- package/src/slash-commands/helpers/active-oauth-account.ts +44 -0
- package/src/slash-commands/helpers/usage-report.ts +24 -3
- package/src/system-prompt.ts +15 -1
- package/src/task/render.ts +29 -19
- package/src/tool-discovery/tool-index.ts +2 -0
- package/src/tools/bash.ts +10 -3
- package/src/tools/eval-render.ts +13 -8
- package/src/tools/gh.ts +39 -1
- package/src/tools/image-gen.ts +114 -78
- package/src/tools/inspect-image.ts +1 -5
- package/src/tools/job.ts +25 -5
- package/src/tools/read.ts +1 -57
- package/src/tools/render-utils.ts +29 -31
- package/src/tools/ssh.ts +3 -3
- package/src/tools/tts.ts +40 -20
- package/src/utils/clipboard.ts +56 -4
- package/src/utils/commit-message-generator.ts +1 -5
- package/src/utils/session-color.ts +83 -9
- package/src/utils/title-generator.ts +1 -1
- package/src/web/kagi.ts +26 -27
- package/src/web/search/providers/codex.ts +42 -40
- package/src/web/search/providers/gemini.ts +42 -22
- package/src/web/search/providers/perplexity.ts +22 -10
|
@@ -849,7 +849,9 @@ export class StatusLineComponent implements Component {
|
|
|
849
849
|
const gapWidth = Math.max(1, topFillWidth - leftWidth - rightWidth);
|
|
850
850
|
const sessionName =
|
|
851
851
|
effectiveSettings.sessionAccent !== false ? this.session.sessionManager?.getSessionName() : undefined;
|
|
852
|
-
const accentHex = sessionName
|
|
852
|
+
const accentHex = sessionName
|
|
853
|
+
? getSessionAccentHex(sessionName, theme.getMajorThemeColorHexes(), theme.accentSurfaceLuminance)
|
|
854
|
+
: undefined;
|
|
853
855
|
const gapColor = getSessionAccentAnsi(accentHex) ?? theme.getFgAnsi("border");
|
|
854
856
|
const gapFill = `${gapColor}${theme.boxRound.horizontal.repeat(gapWidth)}\x1b[39m`;
|
|
855
857
|
return leftGroup + gapFill + rightGroup;
|
|
@@ -486,7 +486,9 @@ const sessionNameSegment: StatusLineSegment = {
|
|
|
486
486
|
if (!name) return { content: "", visible: false };
|
|
487
487
|
|
|
488
488
|
const ansi =
|
|
489
|
-
getSessionAccentAnsi(
|
|
489
|
+
getSessionAccentAnsi(
|
|
490
|
+
getSessionAccentHex(name, theme.getMajorThemeColorHexes(), theme.accentSurfaceLuminance),
|
|
491
|
+
) ?? theme.getFgAnsi("accent");
|
|
490
492
|
return { content: `${ansi}${sanitizeStatusText(name)}\x1b[39m`, visible: true };
|
|
491
493
|
},
|
|
492
494
|
};
|
|
@@ -111,11 +111,23 @@ function getArgsWithStreamedTextInput(args: unknown): unknown {
|
|
|
111
111
|
return input === undefined ? args : { ...record, input };
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Transcript-side probe telling a block whether it is still inside the live
|
|
116
|
+
* (repaintable) region. Implemented by `TranscriptContainer`; injected rather
|
|
117
|
+
* than imported so the component stays decoupled from the transcript.
|
|
118
|
+
*/
|
|
119
|
+
export interface TranscriptLiveRegionProbe {
|
|
120
|
+
isBlockInLiveRegion(component: Component): boolean;
|
|
121
|
+
}
|
|
122
|
+
|
|
114
123
|
export interface ToolExecutionOptions {
|
|
115
124
|
snapshots?: SnapshotStore;
|
|
116
125
|
showImages?: boolean; // default: true (only used if terminal supports images)
|
|
117
126
|
editFuzzyThreshold?: number;
|
|
118
127
|
editAllowFuzzy?: boolean;
|
|
128
|
+
/** Live-region probe used to settle detached task progress once the block
|
|
129
|
+
* leaves the repaintable transcript region. */
|
|
130
|
+
liveRegion?: TranscriptLiveRegionProbe;
|
|
119
131
|
}
|
|
120
132
|
|
|
121
133
|
export interface ToolExecutionHandle {
|
|
@@ -133,10 +145,9 @@ export interface ToolExecutionHandle {
|
|
|
133
145
|
setExpanded(expanded: boolean): void;
|
|
134
146
|
}
|
|
135
147
|
|
|
136
|
-
/** Drive pending-tool redraws at 30fps
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* ~zero cost. */
|
|
148
|
+
/** Drive pending-tool redraws at 30fps for live tool headers and displaceable
|
|
149
|
+
* poll blocks. The TUI throttles at the same cadence, and static frames diff to
|
|
150
|
+
* a no-op redraw at ~zero cost. */
|
|
140
151
|
const SPINNER_RENDER_INTERVAL_MS = 1000 / 30;
|
|
141
152
|
/** Advance the spinner glyph at its classic ~12.5fps step, decoupled from the
|
|
142
153
|
* render cadence (mirrors `Loader`). */
|
|
@@ -200,6 +211,14 @@ export class ToolExecutionComponent extends Container {
|
|
|
200
211
|
// follow-up `job` call can displace it instead of stacking another
|
|
201
212
|
// "waiting on N jobs" frame. Cleared by `seal()`.
|
|
202
213
|
#displaceable = false;
|
|
214
|
+
// Probe into the owning transcript (absent outside the interactive
|
|
215
|
+
// transcript, e.g. in tests): whether this block is still repaintable.
|
|
216
|
+
#liveRegion?: TranscriptLiveRegionProbe;
|
|
217
|
+
// One-way latch for a detached (`async.state === "running"`) task block
|
|
218
|
+
// that left the transcript live region: its rows are commit-eligible
|
|
219
|
+
// history, so progress renders static gray and further partial snapshots are
|
|
220
|
+
// dropped (see #maybeFreezeBackgroundTask).
|
|
221
|
+
#backgroundTaskFrozen = false;
|
|
203
222
|
#renderState: {
|
|
204
223
|
spinnerFrame?: number;
|
|
205
224
|
expanded: boolean;
|
|
@@ -226,6 +245,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
226
245
|
this.#editFuzzyThreshold = options.editFuzzyThreshold;
|
|
227
246
|
this.#editAllowFuzzy = options.editAllowFuzzy;
|
|
228
247
|
this.#snapshots = options.snapshots;
|
|
248
|
+
this.#liveRegion = options.liveRegion;
|
|
229
249
|
this.#tool = tool;
|
|
230
250
|
this.#ui = ui;
|
|
231
251
|
this.#cwd = cwd;
|
|
@@ -363,6 +383,15 @@ export class ToolExecutionComponent extends Container {
|
|
|
363
383
|
isPartial = false,
|
|
364
384
|
_toolCallId?: string,
|
|
365
385
|
): void {
|
|
386
|
+
// A detached task spawn keeps streaming progress snapshots after the
|
|
387
|
+
// block froze (left the transcript live region). Drop them: the rows are
|
|
388
|
+
// static gray history now, and repainting would rewrite rows the engine
|
|
389
|
+
// may already have committed to native scrollback. The terminal snapshot
|
|
390
|
+
// (async completed/failed → isPartial=false) still applies so a block
|
|
391
|
+
// that is still on screen settles on real results.
|
|
392
|
+
if (isPartial && this.#toolName === "task" && this.#maybeFreezeBackgroundTask()) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
366
395
|
this.#result = result;
|
|
367
396
|
this.#isPartial = isPartial;
|
|
368
397
|
// A `job` poll that found every watched job still running is transient
|
|
@@ -436,10 +465,9 @@ export class ToolExecutionComponent extends Container {
|
|
|
436
465
|
(this.#result?.details as { async?: { state?: string } } | undefined)?.async?.state === "running";
|
|
437
466
|
const isBackgroundAsyncTask = this.#toolName === "task" && isBackgroundAsyncRunning;
|
|
438
467
|
const isPartialTask = this.#isPartial && this.#toolName === "task" && !isBackgroundAsyncTask;
|
|
439
|
-
//
|
|
440
|
-
//
|
|
441
|
-
//
|
|
442
|
-
// that a follow-up `job` call may remove.
|
|
468
|
+
// Detached async task progress rows are static now; progress snapshots
|
|
469
|
+
// still call #maybeFreezeBackgroundTask before applying so rows settle
|
|
470
|
+
// once the block leaves the live region.
|
|
443
471
|
const needsSpinner = isStreamingArgs || isPartialTask || this.isDisplaceableBlock();
|
|
444
472
|
if (needsSpinner && !this.#spinnerInterval) {
|
|
445
473
|
const now = performance.now();
|
|
@@ -450,12 +478,15 @@ export class ToolExecutionComponent extends Container {
|
|
|
450
478
|
this.#renderState.spinnerFrame = 0;
|
|
451
479
|
}
|
|
452
480
|
this.#spinnerInterval = setInterval(() => {
|
|
481
|
+
// If a detached task interval from an older render path is still live,
|
|
482
|
+
// stop it the instant the block leaves the repaintable region.
|
|
483
|
+
if (this.#maybeFreezeBackgroundTask()) return;
|
|
453
484
|
const now = performance.now();
|
|
454
485
|
const frameCount = theme.spinnerFrames.length;
|
|
455
|
-
// Redraw at 30fps
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
//
|
|
486
|
+
// Redraw at 30fps, but keep the spinner glyph phase-locked to its
|
|
487
|
+
// classic ~12.5fps cadence. Advancing the anchor by elapsed frames
|
|
488
|
+
// instead of resetting to `now` avoids the 30fps timer quantizing the
|
|
489
|
+
// glyph down to one step every three ticks.
|
|
459
490
|
if (frameCount > 0) {
|
|
460
491
|
const elapsed = now - this.#lastSpinnerAdvanceAt;
|
|
461
492
|
if (elapsed >= SPINNER_GLYPH_ADVANCE_MS) {
|
|
@@ -480,6 +511,26 @@ export class ToolExecutionComponent extends Container {
|
|
|
480
511
|
}
|
|
481
512
|
}
|
|
482
513
|
|
|
514
|
+
/**
|
|
515
|
+
* Freeze a detached (`async.state === "running"`) task block once it leaves
|
|
516
|
+
* the transcript's live region. Past that seam its rows are commit-eligible
|
|
517
|
+
* native-scrollback history: repaint the progress rows static gray and drop
|
|
518
|
+
* further partial snapshots. One-way — blocks never re-enter the live
|
|
519
|
+
* region. Returns whether the block is frozen.
|
|
520
|
+
*/
|
|
521
|
+
#maybeFreezeBackgroundTask(): boolean {
|
|
522
|
+
if (this.#backgroundTaskFrozen) return true;
|
|
523
|
+
if (this.#toolName !== "task" || this.#liveRegion === undefined) return false;
|
|
524
|
+
const asyncState = (this.#result?.details as { async?: { state?: string } } | undefined)?.async?.state;
|
|
525
|
+
if (asyncState !== "running") return false;
|
|
526
|
+
if (this.#liveRegion.isBlockInLiveRegion(this)) return false;
|
|
527
|
+
this.#backgroundTaskFrozen = true;
|
|
528
|
+
this.#updateSpinnerAnimation();
|
|
529
|
+
this.#updateDisplay();
|
|
530
|
+
this.#ui.requestRender();
|
|
531
|
+
return true;
|
|
532
|
+
}
|
|
533
|
+
|
|
483
534
|
#updateTodoStrikeAnimation(): void {
|
|
484
535
|
if (this.#toolName !== "todo" || this.#isPartial || this.#result?.isError) {
|
|
485
536
|
this.#stopTodoStrikeAnimation();
|
|
@@ -547,6 +598,9 @@ export class ToolExecutionComponent extends Container {
|
|
|
547
598
|
if (this.#sealed) return;
|
|
548
599
|
this.#sealed = true;
|
|
549
600
|
this.#displaceable = false;
|
|
601
|
+
// A sealed detached task is abandoned history: settle its progress rows
|
|
602
|
+
// on static gray.
|
|
603
|
+
this.#backgroundTaskFrozen = true;
|
|
550
604
|
this.stopAnimation();
|
|
551
605
|
this.#updateDisplay();
|
|
552
606
|
this.#ui.requestRender();
|
|
@@ -888,6 +942,9 @@ export class ToolExecutionComponent extends Container {
|
|
|
888
942
|
// draws every dispatched agent as a progress/result line, so tell
|
|
889
943
|
// `renderCall` to drop its duplicate streaming preview list.
|
|
890
944
|
context.hasResult = Boolean(this.#result);
|
|
945
|
+
// Out of the transcript live region: progress rows render static gray
|
|
946
|
+
// (see task/render.ts).
|
|
947
|
+
context.frozen = this.#backgroundTaskFrozen;
|
|
891
948
|
} else if (isEditLikeToolName(this.#toolName)) {
|
|
892
949
|
context.editMode = this.#editMode;
|
|
893
950
|
const previews = this.#editDiffPreview;
|
|
@@ -476,6 +476,32 @@ export class TranscriptContainer
|
|
|
476
476
|
return false;
|
|
477
477
|
}
|
|
478
478
|
|
|
479
|
+
/**
|
|
480
|
+
* Whether `component` is inside the live (repaintable) region exactly as
|
|
481
|
+
* {@link render} computes it: at/after the first still-mutating block, or
|
|
482
|
+
* the transcript tail when every block has finalized. Unlike
|
|
483
|
+
* {@link isWithinLiveRegion} (strictly below a still-mutating block, i.e.
|
|
484
|
+
* guaranteed-uncommitted), this also counts the trailing block that anchors
|
|
485
|
+
* the live region. Self-animating finalized blocks (a detached task's
|
|
486
|
+
* shimmering progress rows) poll this to stop animating — and settle on
|
|
487
|
+
* static bytes — the moment they sit above the seam, where their rows
|
|
488
|
+
* become commit-eligible native-scrollback history.
|
|
489
|
+
*/
|
|
490
|
+
isBlockInLiveRegion(component: Component): boolean {
|
|
491
|
+
const children = this.children;
|
|
492
|
+
const index = children.indexOf(component);
|
|
493
|
+
if (index < 0) return false;
|
|
494
|
+
for (let i = 0; i <= index; i++) {
|
|
495
|
+
if (!isBlockFinalized(children[i]!)) return true;
|
|
496
|
+
}
|
|
497
|
+
// Every block at/before `index` finalized: the live region starts at the
|
|
498
|
+
// first unfinalized block below it, or at the last child when none exists.
|
|
499
|
+
for (let i = index + 1; i < children.length; i++) {
|
|
500
|
+
if (!isBlockFinalized(children[i]!)) return false;
|
|
501
|
+
}
|
|
502
|
+
return index === children.length - 1;
|
|
503
|
+
}
|
|
504
|
+
|
|
479
505
|
override render(width: number): readonly string[] {
|
|
480
506
|
width = Math.max(1, width);
|
|
481
507
|
this.#nativeScrollbackLiveRegionStart = undefined;
|
|
@@ -518,7 +518,16 @@ class TreeList implements Component {
|
|
|
518
518
|
const renderedIndent = Math.min(displayIndent, maxIndentLevels);
|
|
519
519
|
const scrollOffset = displayIndent - renderedIndent;
|
|
520
520
|
const connectorPositionDisplay = hasConnector ? renderedIndent - 1 : -1;
|
|
521
|
-
|
|
521
|
+
// Chain rows (no connector of their own) under a last-sibling (`└─`)
|
|
522
|
+
// branch stay anchored by a vertical drawn one level RIGHT of the
|
|
523
|
+
// suppressed gutter — the column where the row's own connector would
|
|
524
|
+
// sit, directly below the branch head's content. Drawing it in the
|
|
525
|
+
// `└─` column itself contradicts the corner and leaves dangling,
|
|
526
|
+
// drifting verticals once the chain branches deeper (#2298, #2325).
|
|
527
|
+
// Chains under `├─` heads need no extra anchor: the sibling line
|
|
528
|
+
// (`show: true` gutter) already ties them to their branch.
|
|
529
|
+
const nearestGutter = !hasConnector ? flatNode.gutters[flatNode.gutters.length - 1] : undefined;
|
|
530
|
+
const chainAnchorLevel = nearestGutter && !nearestGutter.show ? nearestGutter.position + 1 : -1;
|
|
522
531
|
|
|
523
532
|
// Build prefix char by char, placing gutters and connector at their positions
|
|
524
533
|
const totalChars = renderedIndent * 3;
|
|
@@ -531,15 +540,16 @@ class TreeList implements Component {
|
|
|
531
540
|
// Check if there's a gutter at this level (translated to original tree depth)
|
|
532
541
|
const gutter = flatNode.gutters.find(g => g.position === originalLevel);
|
|
533
542
|
if (gutter) {
|
|
534
|
-
//
|
|
535
|
-
//
|
|
536
|
-
// stays anchored without reviving unrelated `└─` ancestors (#2298).
|
|
537
|
-
const showVertical = gutter.show || gutter === chainGutter;
|
|
543
|
+
// Gutters follow standard tree semantics: `│` only while more
|
|
544
|
+
// siblings continue below (`show`), space below a `└─`.
|
|
538
545
|
if (posInLevel === 0) {
|
|
539
|
-
prefixChars.push(
|
|
546
|
+
prefixChars.push(gutter.show ? theme.tree.vertical : " ");
|
|
540
547
|
} else {
|
|
541
548
|
prefixChars.push(" ");
|
|
542
549
|
}
|
|
550
|
+
} else if (originalLevel === chainAnchorLevel) {
|
|
551
|
+
// Chain anchor for rows under a `└─` branch head.
|
|
552
|
+
prefixChars.push(posInLevel === 0 ? theme.tree.vertical : " ");
|
|
543
553
|
} else if (hasConnector && level === connectorPositionDisplay) {
|
|
544
554
|
// Connector at this level
|
|
545
555
|
if (posInLevel === 0) {
|
|
@@ -36,9 +36,10 @@ import { computeContextBreakdown, renderContextUsage } from "../../modes/utils/c
|
|
|
36
36
|
import { buildHotkeysMarkdown } from "../../modes/utils/hotkeys-markdown";
|
|
37
37
|
import { buildToolsMarkdown } from "../../modes/utils/tools-markdown";
|
|
38
38
|
import type { AsyncJobSnapshotItem } from "../../session/agent-session";
|
|
39
|
-
import type { AuthStorage } from "../../session/auth-storage";
|
|
39
|
+
import type { AuthStorage, OAuthAccountIdentity } from "../../session/auth-storage";
|
|
40
40
|
import type { NewSessionOptions } from "../../session/session-manager";
|
|
41
41
|
import { formatShakeSummary, type ShakeMode, type ShakeResult } from "../../session/shake-types";
|
|
42
|
+
import { limitMatchesActiveAccount } from "../../slash-commands/helpers/active-oauth-account";
|
|
42
43
|
import { outputMeta } from "../../tools/output-meta";
|
|
43
44
|
import { resolveToCwd, stripOuterDoubleQuotes } from "../../tools/path-utils";
|
|
44
45
|
import { replaceTabs } from "../../tools/render-utils";
|
|
@@ -404,7 +405,16 @@ export class CommandController {
|
|
|
404
405
|
}
|
|
405
406
|
|
|
406
407
|
const availableWidth = Math.max(40, (this.ctx.ui.terminal.columns ?? 100) - 2);
|
|
407
|
-
const
|
|
408
|
+
const currentProvider = this.ctx.session.model?.provider;
|
|
409
|
+
const activeAccount = currentProvider
|
|
410
|
+
? this.ctx.session.modelRegistry.authStorage.getOAuthAccountIdentity(
|
|
411
|
+
currentProvider,
|
|
412
|
+
this.ctx.session.sessionId,
|
|
413
|
+
)
|
|
414
|
+
: undefined;
|
|
415
|
+
const output = renderUsageReports(usageReports, theme, Date.now(), availableWidth, provider =>
|
|
416
|
+
provider === currentProvider ? activeAccount : undefined,
|
|
417
|
+
);
|
|
408
418
|
this.ctx.present([new Spacer(1), new Text(output, 1, 0)]);
|
|
409
419
|
}
|
|
410
420
|
|
|
@@ -1311,12 +1321,17 @@ function formatAccountHeaderRow(
|
|
|
1311
1321
|
nowMs: number,
|
|
1312
1322
|
columnWidth: number,
|
|
1313
1323
|
uiTheme: typeof theme,
|
|
1324
|
+
activeAccount?: OAuthAccountIdentity,
|
|
1314
1325
|
): string[] {
|
|
1315
1326
|
const parts = limits.map((limit, index) => {
|
|
1316
1327
|
const reset = formatResetShort(limit, nowMs);
|
|
1328
|
+
const report = reports[index];
|
|
1329
|
+
const active = report !== undefined && limitMatchesActiveAccount(report, limit, activeAccount);
|
|
1330
|
+
const label = formatAccountLabel(limit, report, index);
|
|
1317
1331
|
return {
|
|
1318
|
-
label:
|
|
1332
|
+
label: active ? `● ${label}` : label,
|
|
1319
1333
|
suffix: reset ? `(${reset})` : "",
|
|
1334
|
+
active,
|
|
1320
1335
|
};
|
|
1321
1336
|
});
|
|
1322
1337
|
const maxSuffixWidth = parts.reduce((max, p) => Math.max(max, visibleWidth(p.suffix)), 0);
|
|
@@ -1327,16 +1342,18 @@ function formatAccountHeaderRow(
|
|
|
1327
1342
|
if (prefixBudget < 2) {
|
|
1328
1343
|
return parts.map(p => {
|
|
1329
1344
|
const full = p.suffix ? `${p.label} ${p.suffix}` : p.label;
|
|
1330
|
-
|
|
1345
|
+
const cell = padColumn(truncateJobLabel(full, columnWidth), columnWidth);
|
|
1346
|
+
return p.active ? uiTheme.fg("accent", cell) : cell;
|
|
1331
1347
|
});
|
|
1332
1348
|
}
|
|
1333
1349
|
|
|
1334
1350
|
return parts.map(p => {
|
|
1335
1351
|
const prefix = truncateJobLabel(p.label, prefixBudget);
|
|
1336
1352
|
const prefixCell = prefix + " ".repeat(prefixBudget - visibleWidth(prefix));
|
|
1337
|
-
|
|
1353
|
+
const styledPrefix = p.active ? uiTheme.fg("accent", prefixCell) : prefixCell;
|
|
1354
|
+
if (!p.suffix) return styledPrefix + " ".repeat(maxSuffixWidth + gap);
|
|
1338
1355
|
const suffixPad = " ".repeat(maxSuffixWidth - visibleWidth(p.suffix));
|
|
1339
|
-
return `${
|
|
1356
|
+
return `${styledPrefix} ${suffixPad}${uiTheme.fg("dim", p.suffix)}`;
|
|
1340
1357
|
});
|
|
1341
1358
|
}
|
|
1342
1359
|
|
|
@@ -1456,6 +1473,7 @@ function renderUsageReports(
|
|
|
1456
1473
|
uiTheme: typeof theme,
|
|
1457
1474
|
nowMs: number,
|
|
1458
1475
|
availableWidth: number,
|
|
1476
|
+
resolveActiveAccount?: (provider: string) => OAuthAccountIdentity | undefined,
|
|
1459
1477
|
): string {
|
|
1460
1478
|
const lines: string[] = [];
|
|
1461
1479
|
const latestFetchedAt = Math.max(...reports.map(report => report.fetchedAt ?? 0));
|
|
@@ -1481,6 +1499,7 @@ function renderUsageReports(
|
|
|
1481
1499
|
for (const { provider, providerReports } of providerEntries) {
|
|
1482
1500
|
lines.push("");
|
|
1483
1501
|
const providerName = formatProviderName(provider);
|
|
1502
|
+
const activeAccount = resolveActiveAccount?.(provider);
|
|
1484
1503
|
|
|
1485
1504
|
const limitGroups = new Map<
|
|
1486
1505
|
string,
|
|
@@ -1504,6 +1523,10 @@ function renderUsageReports(
|
|
|
1504
1523
|
}
|
|
1505
1524
|
|
|
1506
1525
|
lines.push(uiTheme.bold(uiTheme.fg("accent", providerName)));
|
|
1526
|
+
const activeAccountLabel = activeAccount?.email ?? activeAccount?.accountId ?? activeAccount?.projectId;
|
|
1527
|
+
if (activeAccountLabel) {
|
|
1528
|
+
lines.push(` ${uiTheme.fg("accent", "in use by this session:")} ${activeAccountLabel}`);
|
|
1529
|
+
}
|
|
1507
1530
|
|
|
1508
1531
|
const renderableGroups = Array.from(limitGroups.values()).map(group => {
|
|
1509
1532
|
const entries = group.limits.map((limit, index) => ({
|
|
@@ -1533,7 +1556,14 @@ function renderUsageReports(
|
|
|
1533
1556
|
|
|
1534
1557
|
const windowSuffix = formatWindowSuffix(group.label, group.windowLabel, uiTheme);
|
|
1535
1558
|
lines.push(`${statusIcon} ${uiTheme.bold(group.label)} ${windowSuffix}`.trim());
|
|
1536
|
-
const accountLabels = formatAccountHeaderRow(
|
|
1559
|
+
const accountLabels = formatAccountHeaderRow(
|
|
1560
|
+
sortedLimits,
|
|
1561
|
+
sortedReports,
|
|
1562
|
+
nowMs,
|
|
1563
|
+
sectionColumnWidth,
|
|
1564
|
+
uiTheme,
|
|
1565
|
+
activeAccount,
|
|
1566
|
+
);
|
|
1537
1567
|
lines.push(` ${accountLabels.join(" ")}`.trimEnd());
|
|
1538
1568
|
const bars = sortedLimits.map(limit =>
|
|
1539
1569
|
padColumn(renderUsageBar(limit, uiTheme, sectionColumnWidth), sectionColumnWidth),
|
|
@@ -673,6 +673,7 @@ export class EventController {
|
|
|
673
673
|
showImages: settings.get("terminal.showImages"),
|
|
674
674
|
editFuzzyThreshold: settings.get("edit.fuzzyThreshold"),
|
|
675
675
|
editAllowFuzzy: settings.get("edit.fuzzyMatch"),
|
|
676
|
+
liveRegion: this.ctx.chatContainer,
|
|
676
677
|
},
|
|
677
678
|
tool,
|
|
678
679
|
this.ctx.ui,
|
|
@@ -49,7 +49,14 @@ const TINY_TITLE_PROGRESS_DONE_TTL_MS = 3_000;
|
|
|
49
49
|
const TINY_TITLE_PROGRESS_REVEAL_DELAY_MS = 1_000;
|
|
50
50
|
|
|
51
51
|
export class InputController {
|
|
52
|
-
constructor(
|
|
52
|
+
constructor(
|
|
53
|
+
private ctx: InteractiveModeContext,
|
|
54
|
+
/** Injectable clipboard reads so tests can drive paste flows without a real clipboard. */
|
|
55
|
+
private clipboard: {
|
|
56
|
+
readImage: typeof readImageFromClipboard;
|
|
57
|
+
readText: typeof readTextFromClipboard;
|
|
58
|
+
} = { readImage: readImageFromClipboard, readText: readTextFromClipboard },
|
|
59
|
+
) {}
|
|
53
60
|
|
|
54
61
|
#enhancedPaste?: EnhancedPasteController;
|
|
55
62
|
|
|
@@ -523,6 +530,36 @@ export class InputController {
|
|
|
523
530
|
});
|
|
524
531
|
|
|
525
532
|
this.ctx.onInputCallback(submission);
|
|
533
|
+
} else {
|
|
534
|
+
// No input waiter: the main loop is between turns (post-turn
|
|
535
|
+
// epilogue, retry backoff, or a scheduled continue) with the agent
|
|
536
|
+
// momentarily idle. The editor already cleared itself on Enter, so
|
|
537
|
+
// falling through here would silently swallow the message. Queue it
|
|
538
|
+
// as a steer instead: the idle drain in #queueSteer delivers it
|
|
539
|
+
// immediately when the session is resumable, and a retry/continue
|
|
540
|
+
// run picks it up at loop start otherwise.
|
|
541
|
+
this.ctx.editor.imageLinks = undefined;
|
|
542
|
+
const images = inputImages && inputImages.length > 0 ? [...inputImages] : undefined;
|
|
543
|
+
this.ctx.pendingImages = [];
|
|
544
|
+
this.ctx.pendingImageLinks = [];
|
|
545
|
+
try {
|
|
546
|
+
await this.ctx.withLocalSubmission(text, () => this.ctx.session.steer(text, images), {
|
|
547
|
+
imageCount: images?.length ?? 0,
|
|
548
|
+
});
|
|
549
|
+
} catch (error) {
|
|
550
|
+
// Don't lose the message: hand the text and images back to the
|
|
551
|
+
// editor so the user can retry (e.g. steer() rejecting an
|
|
552
|
+
// extension command).
|
|
553
|
+
this.ctx.editor.setText(text);
|
|
554
|
+
if (images && images.length > 0) {
|
|
555
|
+
this.ctx.pendingImages = [...images];
|
|
556
|
+
this.ctx.pendingImageLinks = inputImageLinks ? [...inputImageLinks] : images.map(() => undefined);
|
|
557
|
+
this.ctx.editor.imageLinks = this.ctx.pendingImageLinks;
|
|
558
|
+
}
|
|
559
|
+
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
560
|
+
}
|
|
561
|
+
this.ctx.updatePendingMessagesDisplay();
|
|
562
|
+
this.ctx.ui.requestRender();
|
|
526
563
|
}
|
|
527
564
|
this.ctx.editor.addToHistory(text);
|
|
528
565
|
};
|
|
@@ -737,10 +774,19 @@ export class InputController {
|
|
|
737
774
|
}
|
|
738
775
|
return 0;
|
|
739
776
|
}
|
|
740
|
-
const queuedText = allQueued.join("\n\n");
|
|
777
|
+
const queuedText = allQueued.map(e => e.text).join("\n\n");
|
|
741
778
|
const currentText = options?.currentText ?? this.ctx.editor.getText();
|
|
742
779
|
const combinedText = [queuedText, currentText].filter(t => t.trim()).join("\n\n");
|
|
743
780
|
this.ctx.editor.setText(combinedText);
|
|
781
|
+
// Hand queued images back to the pending-image buffer (links are
|
|
782
|
+
// re-materialized lazily; the restored text already carries the
|
|
783
|
+
// `[Image #N, WxH]` markers).
|
|
784
|
+
const queuedImages = allQueued.flatMap(e => e.images ?? []);
|
|
785
|
+
if (queuedImages.length > 0) {
|
|
786
|
+
this.ctx.pendingImages.push(...queuedImages);
|
|
787
|
+
this.ctx.pendingImageLinks.push(...queuedImages.map(() => undefined));
|
|
788
|
+
this.ctx.editor.imageLinks = this.ctx.pendingImageLinks;
|
|
789
|
+
}
|
|
744
790
|
this.ctx.updatePendingMessagesDisplay();
|
|
745
791
|
if (options?.abort) {
|
|
746
792
|
this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
|
|
@@ -837,10 +883,25 @@ export class InputController {
|
|
|
837
883
|
|
|
838
884
|
async handleImagePaste(): Promise<boolean> {
|
|
839
885
|
try {
|
|
840
|
-
const image = await
|
|
886
|
+
const image = await this.clipboard.readImage();
|
|
841
887
|
if (!image) {
|
|
842
|
-
|
|
843
|
-
|
|
888
|
+
// Smart paste (#1628): no image on the clipboard — fall back to
|
|
889
|
+
// pasting its text so the same chord covers both payload kinds.
|
|
890
|
+
// Hosts that pre-empt the terminal's own paste (VS Code's
|
|
891
|
+
// integrated terminal, Win+V clipboard history) deliver only
|
|
892
|
+
// this keypress, so a miss here must not dead-end.
|
|
893
|
+
const text = await this.clipboard.readText();
|
|
894
|
+
if (!text) {
|
|
895
|
+
this.ctx.showStatus("Clipboard is empty");
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
898
|
+
// Route to the focused component when it accepts pastes (modal
|
|
899
|
+
// Input prompts), matching the enhanced-paste text path (#2127).
|
|
900
|
+
const focused = this.ctx.ui.getFocused();
|
|
901
|
+
const target = focused && focused !== this.ctx.editor && hasPasteText(focused) ? focused : this.ctx.editor;
|
|
902
|
+
target.pasteText(text);
|
|
903
|
+
this.ctx.ui.requestRender();
|
|
904
|
+
return true;
|
|
844
905
|
}
|
|
845
906
|
return await this.#normalizeAndInsertPastedImage(
|
|
846
907
|
{
|
|
@@ -858,10 +919,11 @@ export class InputController {
|
|
|
858
919
|
|
|
859
920
|
async handleClipboardTextRawPaste(): Promise<void> {
|
|
860
921
|
try {
|
|
861
|
-
const text = await
|
|
922
|
+
const text = await this.clipboard.readText();
|
|
862
923
|
if (text) {
|
|
863
924
|
this.ctx.editor.insertText(text);
|
|
864
925
|
this.ctx.ui.requestRender();
|
|
926
|
+
} else {
|
|
865
927
|
this.ctx.showStatus("No text in clipboard to paste raw");
|
|
866
928
|
}
|
|
867
929
|
} catch {
|
|
@@ -92,71 +92,86 @@ export class SelectorController {
|
|
|
92
92
|
|
|
93
93
|
showSettingsSelector(): void {
|
|
94
94
|
getAvailableThemes().then(availableThemes => {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
this.ctx.statusLine.
|
|
117
|
-
preset: settings.get("statusLine.preset"),
|
|
118
|
-
leftSegments: settings.get("statusLine.leftSegments"),
|
|
119
|
-
rightSegments: settings.get("statusLine.rightSegments"),
|
|
120
|
-
separator: settings.get("statusLine.separator"),
|
|
121
|
-
showHookStatus: settings.get("statusLine.showHookStatus"),
|
|
122
|
-
sessionAccent: settings.get("statusLine.sessionAccent"),
|
|
123
|
-
transparent: settings.get("statusLine.transparent"),
|
|
124
|
-
...previewSettings,
|
|
125
|
-
});
|
|
126
|
-
this.ctx.updateEditorTopBorder();
|
|
127
|
-
this.ctx.ui.requestRender();
|
|
128
|
-
},
|
|
129
|
-
getStatusLinePreview: () => {
|
|
130
|
-
// Return the rendered status line for inline preview
|
|
131
|
-
const availableWidth = this.ctx.editor.getTopBorderAvailableWidth(this.ctx.ui.terminal.columns);
|
|
132
|
-
return this.ctx.statusLine.getTopBorder(availableWidth).content;
|
|
133
|
-
},
|
|
134
|
-
onPluginsChanged: async () => {
|
|
135
|
-
const projectPath = await resolveActiveProjectRegistryPath(this.ctx.sessionManager.getCwd());
|
|
136
|
-
clearPluginRootsAndCaches(projectPath ? [projectPath] : undefined);
|
|
137
|
-
await this.ctx.refreshSlashCommandState();
|
|
138
|
-
await this.ctx.session.refreshSshTool({ activateIfAvailable: true });
|
|
139
|
-
this.ctx.ui.requestRender();
|
|
140
|
-
},
|
|
141
|
-
onCancel: () => {
|
|
142
|
-
done();
|
|
143
|
-
// Restore status line to saved settings
|
|
144
|
-
this.ctx.statusLine.updateSettings({
|
|
145
|
-
preset: settings.get("statusLine.preset"),
|
|
146
|
-
leftSegments: settings.get("statusLine.leftSegments"),
|
|
147
|
-
rightSegments: settings.get("statusLine.rightSegments"),
|
|
148
|
-
separator: settings.get("statusLine.separator"),
|
|
149
|
-
showHookStatus: settings.get("statusLine.showHookStatus"),
|
|
150
|
-
sessionAccent: settings.get("statusLine.sessionAccent"),
|
|
151
|
-
transparent: settings.get("statusLine.transparent"),
|
|
152
|
-
});
|
|
95
|
+
// Fullscreen settings editor on the alternate screen: the overlay
|
|
96
|
+
// enables mouse tracking (click/hover/wheel) for its lifetime and
|
|
97
|
+
// the transcript stays untouched underneath.
|
|
98
|
+
let overlayHandle: OverlayHandle | undefined;
|
|
99
|
+
const done = () => {
|
|
100
|
+
overlayHandle?.hide();
|
|
101
|
+
this.ctx.ui.setFocus(this.ctx.editor);
|
|
102
|
+
this.ctx.ui.requestRender();
|
|
103
|
+
};
|
|
104
|
+
const selector = new SettingsSelectorComponent(
|
|
105
|
+
{
|
|
106
|
+
availableThinkingLevels: [...this.ctx.session.getAvailableThinkingLevels()],
|
|
107
|
+
thinkingLevel: this.ctx.session.thinkingLevel,
|
|
108
|
+
availableThemes,
|
|
109
|
+
cwd: getProjectDir(),
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
onChange: (id, value) => this.handleSettingChange(id, value),
|
|
113
|
+
onThemePreview: async themeName => {
|
|
114
|
+
const result = await previewTheme(themeName);
|
|
115
|
+
if (result.success) {
|
|
116
|
+
this.ctx.statusLine.invalidate();
|
|
153
117
|
this.ctx.updateEditorTopBorder();
|
|
118
|
+
this.ctx.ui.invalidate();
|
|
154
119
|
this.ctx.ui.requestRender();
|
|
155
|
-
}
|
|
120
|
+
}
|
|
156
121
|
},
|
|
157
|
-
|
|
158
|
-
|
|
122
|
+
onStatusLinePreview: previewSettings => {
|
|
123
|
+
// Update status line with preview settings
|
|
124
|
+
this.ctx.statusLine.updateSettings({
|
|
125
|
+
preset: settings.get("statusLine.preset"),
|
|
126
|
+
leftSegments: settings.get("statusLine.leftSegments"),
|
|
127
|
+
rightSegments: settings.get("statusLine.rightSegments"),
|
|
128
|
+
separator: settings.get("statusLine.separator"),
|
|
129
|
+
showHookStatus: settings.get("statusLine.showHookStatus"),
|
|
130
|
+
sessionAccent: settings.get("statusLine.sessionAccent"),
|
|
131
|
+
transparent: settings.get("statusLine.transparent"),
|
|
132
|
+
...previewSettings,
|
|
133
|
+
});
|
|
134
|
+
this.ctx.updateEditorTopBorder();
|
|
135
|
+
this.ctx.ui.requestRender();
|
|
136
|
+
},
|
|
137
|
+
getStatusLinePreview: () => {
|
|
138
|
+
// Return the rendered status line for inline preview
|
|
139
|
+
const availableWidth = this.ctx.editor.getTopBorderAvailableWidth(this.ctx.ui.terminal.columns);
|
|
140
|
+
return this.ctx.statusLine.getTopBorder(availableWidth).content;
|
|
141
|
+
},
|
|
142
|
+
onPluginsChanged: async () => {
|
|
143
|
+
const projectPath = await resolveActiveProjectRegistryPath(this.ctx.sessionManager.getCwd());
|
|
144
|
+
clearPluginRootsAndCaches(projectPath ? [projectPath] : undefined);
|
|
145
|
+
await this.ctx.refreshSlashCommandState();
|
|
146
|
+
await this.ctx.session.refreshSshTool({ activateIfAvailable: true });
|
|
147
|
+
this.ctx.ui.requestRender();
|
|
148
|
+
},
|
|
149
|
+
onCancel: () => {
|
|
150
|
+
done();
|
|
151
|
+
// Restore status line to saved settings
|
|
152
|
+
this.ctx.statusLine.updateSettings({
|
|
153
|
+
preset: settings.get("statusLine.preset"),
|
|
154
|
+
leftSegments: settings.get("statusLine.leftSegments"),
|
|
155
|
+
rightSegments: settings.get("statusLine.rightSegments"),
|
|
156
|
+
separator: settings.get("statusLine.separator"),
|
|
157
|
+
showHookStatus: settings.get("statusLine.showHookStatus"),
|
|
158
|
+
sessionAccent: settings.get("statusLine.sessionAccent"),
|
|
159
|
+
transparent: settings.get("statusLine.transparent"),
|
|
160
|
+
});
|
|
161
|
+
this.ctx.updateEditorTopBorder();
|
|
162
|
+
this.ctx.ui.requestRender();
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
overlayHandle = this.ctx.ui.showOverlay(selector, {
|
|
167
|
+
anchor: "bottom-center",
|
|
168
|
+
width: "100%",
|
|
169
|
+
maxHeight: "100%",
|
|
170
|
+
margin: 0,
|
|
171
|
+
fullscreen: true,
|
|
159
172
|
});
|
|
173
|
+
this.ctx.ui.setFocus(selector);
|
|
174
|
+
this.ctx.ui.requestRender();
|
|
160
175
|
});
|
|
161
176
|
}
|
|
162
177
|
|
|
@@ -267,6 +282,11 @@ export class SelectorController {
|
|
|
267
282
|
this.ctx.statusLine.invalidate();
|
|
268
283
|
this.ctx.updateEditorBorderColor();
|
|
269
284
|
break;
|
|
285
|
+
case "personality":
|
|
286
|
+
void this.ctx.session.refreshBaseSystemPrompt().catch(err => {
|
|
287
|
+
this.ctx.showError(`Failed to apply personality: ${err}`);
|
|
288
|
+
});
|
|
289
|
+
break;
|
|
270
290
|
|
|
271
291
|
case "autocompleteMaxVisible":
|
|
272
292
|
this.ctx.editor.setAutocompleteMaxVisible(typeof value === "number" ? value : Number(value));
|