@oh-my-pi/pi-coding-agent 16.1.7 → 16.1.9
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 +71 -0
- package/dist/cli.js +4266 -3446
- package/dist/types/cli/flag-tables.d.ts +17 -0
- package/dist/types/cli/session-picker.d.ts +0 -1
- package/dist/types/cli/tiny-models-cli.d.ts +2 -0
- package/dist/types/cli-commands.d.ts +4 -2
- package/dist/types/collab/protocol.d.ts +20 -1
- package/dist/types/config/settings-schema.d.ts +57 -6
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +5 -1
- package/dist/types/internal-urls/filesystem-resource.d.ts +9 -0
- package/dist/types/mcp/loader.d.ts +3 -2
- package/dist/types/mcp/manager.d.ts +4 -3
- package/dist/types/mcp/startup-events.d.ts +21 -4
- package/dist/types/mnemopi/config.d.ts +1 -0
- package/dist/types/modes/components/agent-hub.d.ts +1 -0
- package/dist/types/modes/components/agent-transcript-viewer.d.ts +1 -0
- package/dist/types/modes/components/assistant-message.d.ts +5 -1
- package/dist/types/modes/components/btw-panel.d.ts +2 -0
- package/dist/types/modes/components/chat-transcript-builder.d.ts +1 -0
- package/dist/types/modes/components/session-selector.d.ts +0 -2
- package/dist/types/modes/controllers/btw-controller.d.ts +2 -0
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/controllers/streaming-reveal.d.ts +3 -2
- package/dist/types/modes/interactive-mode.d.ts +4 -1
- package/dist/types/modes/types.d.ts +4 -1
- package/dist/types/session/agent-session.d.ts +18 -3
- package/dist/types/session/session-history-format.d.ts +25 -0
- package/dist/types/session/snapcompact-inline.d.ts +1 -1
- package/dist/types/slash-commands/builtin-registry.d.ts +7 -4
- package/dist/types/slash-commands/types.d.ts +2 -0
- package/dist/types/system-prompt.d.ts +1 -0
- package/dist/types/tiny/models.d.ts +11 -7
- package/dist/types/tools/browser/launch.d.ts +5 -0
- package/dist/types/tools/todo.d.ts +1 -0
- package/dist/types/utils/thinking-display.d.ts +2 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +104 -0
- package/src/advisor/runtime.ts +38 -2
- package/src/cli/args.ts +0 -1
- package/src/cli/flag-tables.ts +42 -0
- package/src/cli/profile-bootstrap.ts +1 -11
- package/src/cli/session-picker.ts +1 -2
- package/src/cli/tiny-models-cli.ts +7 -2
- package/src/cli-commands.ts +48 -3
- package/src/collab/guest.ts +172 -20
- package/src/collab/host.ts +47 -5
- package/src/collab/protocol.ts +16 -1
- package/src/config/model-registry.ts +6 -7
- package/src/config/settings-schema.ts +59 -5
- package/src/edit/renderer.ts +8 -12
- package/src/extensibility/legacy-pi-ai-shim.ts +16 -4
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/internal-urls/filesystem-resource.ts +34 -0
- package/src/internal-urls/local-protocol.ts +7 -1
- package/src/internal-urls/memory-protocol.ts +5 -1
- package/src/internal-urls/skill-protocol.ts +20 -4
- package/src/internal-urls/vault-protocol.ts +5 -2
- package/src/lsp/client.ts +39 -25
- package/src/main.ts +8 -8
- package/src/mcp/loader.ts +4 -3
- package/src/mcp/manager.ts +35 -15
- package/src/mcp/startup-events.ts +106 -11
- package/src/mnemopi/config.ts +2 -0
- package/src/mnemopi/state.ts +3 -1
- package/src/modes/components/agent-hub.ts +4 -0
- package/src/modes/components/agent-transcript-viewer.ts +2 -0
- package/src/modes/components/assistant-message.ts +217 -18
- package/src/modes/components/btw-panel.ts +15 -3
- package/src/modes/components/chat-transcript-builder.ts +8 -2
- package/src/modes/components/model-selector.ts +72 -9
- package/src/modes/components/omfg-panel.ts +1 -1
- package/src/modes/components/session-selector.ts +4 -9
- package/src/modes/components/snapcompact-shape-preview.ts +1 -1
- package/src/modes/components/tool-execution.ts +10 -2
- package/src/modes/controllers/btw-controller.ts +32 -7
- package/src/modes/controllers/command-controller.ts +18 -3
- package/src/modes/controllers/event-controller.ts +24 -4
- package/src/modes/controllers/input-controller.ts +43 -21
- package/src/modes/controllers/mcp-command-controller.ts +40 -12
- package/src/modes/controllers/selector-controller.ts +23 -13
- package/src/modes/controllers/streaming-reveal.ts +78 -20
- package/src/modes/controllers/todo-command-controller.ts +9 -10
- package/src/modes/interactive-mode.ts +84 -9
- package/src/modes/types.ts +4 -1
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/advisor/system.md +1 -1
- package/src/prompts/system/side-channel-no-tools.md +3 -0
- package/src/sdk.ts +12 -8
- package/src/session/agent-session.ts +385 -85
- package/src/session/history-storage.ts +15 -16
- package/src/session/session-history-format.ts +41 -1
- package/src/session/snapcompact-inline.ts +9 -6
- package/src/slash-commands/builtin-registry.ts +171 -87
- package/src/slash-commands/helpers/todo.ts +12 -6
- package/src/slash-commands/types.ts +2 -0
- package/src/system-prompt.ts +6 -11
- package/src/tiny/models.ts +7 -3
- package/src/tiny/title-client.ts +8 -2
- package/src/tiny/worker.ts +1 -0
- package/src/tools/browser/launch.ts +107 -31
- package/src/tools/puppeteer/00_stealth_tampering.txt +16 -35
- package/src/tools/puppeteer/01_stealth_activity.txt +79 -19
- package/src/tools/puppeteer/02_stealth_hairline.txt +57 -11
- package/src/tools/puppeteer/03_stealth_botd.txt +10 -14
- package/src/tools/puppeteer/04_stealth_iframe.txt +156 -63
- package/src/tools/puppeteer/05_stealth_webgl.txt +222 -64
- package/src/tools/puppeteer/06_stealth_screen.txt +255 -67
- package/src/tools/puppeteer/07_stealth_fonts.txt +17 -15
- package/src/tools/puppeteer/08_stealth_audio.txt +32 -20
- package/src/tools/puppeteer/09_stealth_locale.txt +45 -40
- package/src/tools/puppeteer/10_stealth_plugins.txt +12 -8
- package/src/tools/puppeteer/11_stealth_hardware.txt +57 -6
- package/src/tools/puppeteer/12_stealth_codecs.txt +20 -18
- package/src/tools/puppeteer/13_stealth_worker.txt +206 -45
- package/src/tools/search.ts +10 -1
- package/src/tools/sqlite-reader.ts +59 -3
- package/src/tools/todo.ts +6 -0
- package/src/tools/write.ts +4 -6
- package/src/utils/thinking-display.ts +78 -0
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
|
|
2
2
|
import { Container, Image, type ImageBudget, ImageProtocol, Markdown, Spacer, TERMINAL, Text } from "@oh-my-pi/pi-tui";
|
|
3
|
+
import { formatNumber } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import chalk from "chalk";
|
|
3
5
|
import type { AssistantThinkingRenderer } from "../../extensibility/extensions/types";
|
|
4
6
|
import { getMarkdownTheme, theme } from "../../modes/theme/theme";
|
|
5
7
|
import { resolveAbortLabel, shouldRenderAbortReason } from "../../session/messages";
|
|
6
8
|
import { getPreviewLines, resolveImageOptions, TRUNCATE_LENGTHS } from "../../tools/render-utils";
|
|
7
|
-
import { canonicalizeMessage } from "../../utils/thinking-display";
|
|
9
|
+
import { canonicalizeMessage, formatThinkingForDisplay, hasDisplayableThinking } from "../../utils/thinking-display";
|
|
8
10
|
import { type CacheInvalidation, CacheInvalidationMarkerComponent } from "./cache-invalidation-marker";
|
|
9
11
|
|
|
10
12
|
/**
|
|
@@ -28,6 +30,18 @@ const MARKDOWN_TABLE_DELIMITER = /^ {0,3}\|?(?:[ \t]*:?-+:?[ \t]*\|)+[ \t]*:?-*:
|
|
|
28
30
|
/** Opening or closing fence of a code block: ≥3 backticks/tildes plus info string. */
|
|
29
31
|
const CODE_FENCE_LINE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
|
30
32
|
|
|
33
|
+
type ThinkingContentBlock = Extract<AssistantMessage["content"][number], { type: "thinking" }>;
|
|
34
|
+
type DisplayThinkingContentBlock = ThinkingContentBlock & { rawThinking?: string };
|
|
35
|
+
|
|
36
|
+
function resolveThinkingDisplay(block: ThinkingContentBlock, proseOnly: boolean): { text: string; visible: boolean } {
|
|
37
|
+
const rawThinking = (block as DisplayThinkingContentBlock).rawThinking ?? block.thinking;
|
|
38
|
+
const formatted = formatThinkingForDisplay(block.thinking, proseOnly);
|
|
39
|
+
return {
|
|
40
|
+
text: formatted.trim(),
|
|
41
|
+
visible: hasDisplayableThinking(rawThinking, formatted),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
31
45
|
/**
|
|
32
46
|
* Whether `text` currently contains reflowing Markdown whose layout is not yet
|
|
33
47
|
* permanent: an open ` ```mermaid ` fence (the diagram reshapes as source
|
|
@@ -72,11 +86,96 @@ function detectLiveReflowingMarkdown(text: string): boolean {
|
|
|
72
86
|
/**
|
|
73
87
|
* Frames for the streaming "thinking" pulse rendered in place of a hidden
|
|
74
88
|
* thinking block while the model is still producing it. A single fixed-width
|
|
75
|
-
*
|
|
76
|
-
*
|
|
89
|
+
* starburst cycles through facets (✻ ✼ ❉ ❊ ✺ ✹ ✸ ✶) so the indicator animates
|
|
90
|
+
* in place without shifting the line or the trailing speed badge. The dwell per
|
|
91
|
+
* frame eases between {@link THINKING_DOTS_FRAME_MS_MIN} and
|
|
92
|
+
* {@link THINKING_DOTS_FRAME_MS_MAX} across each revolution (see
|
|
93
|
+
* {@link AssistantMessageComponent.thinkingDotsFrameDelay}).
|
|
77
94
|
*/
|
|
78
|
-
const THINKING_DOTS_FRAMES = ["
|
|
79
|
-
|
|
95
|
+
const THINKING_DOTS_FRAMES = ["✻", "✼", "❉", "❊", "✺", "✹", "✸", "✶"] as const;
|
|
96
|
+
/**
|
|
97
|
+
* Pulse cadence bounds (ms). Each frame's dwell eases between these on a
|
|
98
|
+
* raised-cosine "breath" — quickest at the cycle start, slowest at its midpoint —
|
|
99
|
+
* so the starburst accelerates and slows instead of ticking at one fixed rate.
|
|
100
|
+
* Mean ≈ 150ms, snappier than the previous flat 320ms.
|
|
101
|
+
*/
|
|
102
|
+
const THINKING_DOTS_FRAME_MS_MIN = 70;
|
|
103
|
+
const THINKING_DOTS_FRAME_MS_MAX = 230;
|
|
104
|
+
|
|
105
|
+
/** Rolling window (ms) over which streaming-rate observations are averaged. */
|
|
106
|
+
const SPEED_WINDOW_MS = 3000;
|
|
107
|
+
/** Color/clamp ceiling: a rate at or above this maps to the full accent color. */
|
|
108
|
+
const SPEED_MAX = 200;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Session-wide streaming-speed gauge. Only one thinking indicator animates at a
|
|
112
|
+
* time, so a single shared instance accumulates instantaneous tok/s observations
|
|
113
|
+
* and reports their windowed average — smoothing the jumpy per-delta numbers.
|
|
114
|
+
* Each thinking block resets the gauge on its first live sample (see
|
|
115
|
+
* {@link AssistantMessageComponent.updateContent}) so the average reflects only
|
|
116
|
+
* the active block, never a previous turn's trailing rate. Components feed it
|
|
117
|
+
* deltas (not cumulative totals), so a fresh turn restarting its token count at
|
|
118
|
+
* zero never produces a spike.
|
|
119
|
+
*/
|
|
120
|
+
class SpeedTracker {
|
|
121
|
+
#observations: Array<{ time: number; rate: number }> = [];
|
|
122
|
+
|
|
123
|
+
#prune(now: number): void {
|
|
124
|
+
const threshold = now - SPEED_WINDOW_MS;
|
|
125
|
+
while (this.#observations.length > 0 && this.#observations[0]!.time < threshold) {
|
|
126
|
+
this.#observations.shift();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Record one instantaneous tok/s reading, clamped to {@link SPEED_MAX} so a
|
|
131
|
+
* single oversized delta (e.g. a buffered reflow tick) can't poison the
|
|
132
|
+
* windowed average. Non-finite/negative rates ignored. */
|
|
133
|
+
observe(rate: number, now = performance.now()): void {
|
|
134
|
+
if (!Number.isFinite(rate) || rate < 0) return;
|
|
135
|
+
this.#observations.push({ time: now, rate: Math.min(rate, SPEED_MAX) });
|
|
136
|
+
this.#prune(now);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Windowed-average tok/s; 0 once observations age out of the window. */
|
|
140
|
+
getSpeed(now = performance.now()): number {
|
|
141
|
+
this.#prune(now);
|
|
142
|
+
if (this.#observations.length === 0) return 0;
|
|
143
|
+
let sum = 0;
|
|
144
|
+
for (const o of this.#observations) sum += o.rate;
|
|
145
|
+
return sum / this.#observations.length;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
reset(): void {
|
|
149
|
+
this.#observations = [];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** One gauge for the whole session — see {@link SpeedTracker}. */
|
|
154
|
+
const sharedSpeedTracker = new SpeedTracker();
|
|
155
|
+
|
|
156
|
+
/** Test-only: clear the shared gauge so observations don't leak across cases. */
|
|
157
|
+
export function resetThinkingSpeedTracker(): void {
|
|
158
|
+
sharedSpeedTracker.reset();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Linear-interpolate two `#rrggbb` colors in sRGB space. `t` clamps to [0,1]:
|
|
163
|
+
* `t = 0` → `from`, `t = 1` → `to`. Drives the streaming speed badge, fading
|
|
164
|
+
* from a dim gray toward the theme accent as tok/s rises.
|
|
165
|
+
*/
|
|
166
|
+
function lerpHex(from: string, to: string, t: number): string {
|
|
167
|
+
const k = t < 0 ? 0 : t > 1 ? 1 : t;
|
|
168
|
+
const fr = Number.parseInt(from.slice(1, 3), 16);
|
|
169
|
+
const fg = Number.parseInt(from.slice(3, 5), 16);
|
|
170
|
+
const fb = Number.parseInt(from.slice(5, 7), 16);
|
|
171
|
+
const tr = Number.parseInt(to.slice(1, 3), 16);
|
|
172
|
+
const tg = Number.parseInt(to.slice(3, 5), 16);
|
|
173
|
+
const tb = Number.parseInt(to.slice(5, 7), 16);
|
|
174
|
+
const r = Math.round(fr + (tr - fr) * k);
|
|
175
|
+
const g = Math.round(fg + (tg - fg) * k);
|
|
176
|
+
const b = Math.round(fb + (tb - fb) * k);
|
|
177
|
+
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
|
178
|
+
}
|
|
80
179
|
|
|
81
180
|
/**
|
|
82
181
|
* Component that renders a complete assistant message
|
|
@@ -128,6 +227,20 @@ export class AssistantMessageComponent extends Container {
|
|
|
128
227
|
#thinkingDots: Text | undefined;
|
|
129
228
|
#thinkingDotsTimer: NodeJS.Timeout | undefined;
|
|
130
229
|
#thinkingDotsFrame = 0;
|
|
230
|
+
/** Previous cumulative provider token count + timestamp, for deriving this
|
|
231
|
+
* block's instantaneous streaming rate fed into {@link sharedSpeedTracker}.
|
|
232
|
+
* Undefined until the first thinking update of this block. */
|
|
233
|
+
#lastTokenCount: number | undefined;
|
|
234
|
+
#lastTokenTime = 0;
|
|
235
|
+
/** Provider-reported tokens in the live thinking block — reasoning tokens when
|
|
236
|
+
* the provider streams them, else total output — shown dimmed beside the
|
|
237
|
+
* speed badge. 0 when no thinking is streaming. */
|
|
238
|
+
#thinkingTokens = 0;
|
|
239
|
+
/** Whether this block has observed a positive provider-token delta — i.e. it is
|
|
240
|
+
* genuinely streaming tokens right now. Gates the numeric speed badge so the
|
|
241
|
+
* session-wide {@link sharedSpeedTracker} can't surface a previous turn's rate
|
|
242
|
+
* on a fresh block that has no live token throughput of its own. */
|
|
243
|
+
#thinkingRateLive = false;
|
|
131
244
|
|
|
132
245
|
constructor(
|
|
133
246
|
message?: AssistantMessage,
|
|
@@ -135,6 +248,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
135
248
|
private readonly onImageUpdate?: () => void,
|
|
136
249
|
private readonly thinkingRenderers: readonly AssistantThinkingRenderer[] = [],
|
|
137
250
|
private readonly imageBudget?: ImageBudget,
|
|
251
|
+
private proseOnlyThinking = true,
|
|
138
252
|
) {
|
|
139
253
|
super();
|
|
140
254
|
this.#transcriptBlockFinalized = message !== undefined;
|
|
@@ -184,6 +298,10 @@ export class AssistantMessageComponent extends Container {
|
|
|
184
298
|
this.hideThinkingBlock = hide;
|
|
185
299
|
}
|
|
186
300
|
|
|
301
|
+
setProseOnlyThinking(proseOnly: boolean): void {
|
|
302
|
+
this.proseOnlyThinking = proseOnly;
|
|
303
|
+
}
|
|
304
|
+
|
|
187
305
|
override dispose(): void {
|
|
188
306
|
this.#stopThinkingAnimation();
|
|
189
307
|
super.dispose();
|
|
@@ -210,16 +328,51 @@ export class AssistantMessageComponent extends Container {
|
|
|
210
328
|
|
|
211
329
|
#thinkingDotsLabel(): string {
|
|
212
330
|
const glyph = THINKING_DOTS_FRAMES[this.#thinkingDotsFrame % THINKING_DOTS_FRAMES.length] ?? "…";
|
|
213
|
-
|
|
331
|
+
const coloredGlyph = theme.fg("thinkingText", glyph);
|
|
332
|
+
const rate = Math.min(SPEED_MAX, sharedSpeedTracker.getSpeed());
|
|
333
|
+
// The numeric badge ("<total> · <rate> toks/s") only renders while this block
|
|
334
|
+
// is genuinely streaming provider tokens. A block that has observed no token
|
|
335
|
+
// delta (e.g. a provider that reports usage only at turn end) or whose rate
|
|
336
|
+
// has decayed to zero (a streaming lull) drops it entirely — the bare pulse
|
|
337
|
+
// keeps signalling that the model is thinking. The liveness flag also stops
|
|
338
|
+
// the session-wide gauge from leaking a previous turn's rate onto a fresh
|
|
339
|
+
// token-less block.
|
|
340
|
+
if (!this.#thinkingRateLive || rate < 0.05) return coloredGlyph;
|
|
341
|
+
// Total provider tokens, dimmed, sit next to the pulse.
|
|
342
|
+
const totalSpan = this.#thinkingTokens > 0 ? theme.fg("dim", ` ${formatNumber(this.#thinkingTokens)}`) : "";
|
|
343
|
+
// Speed badge color: dim gray at rest, brightening toward the theme accent as
|
|
344
|
+
// streaming speed climbs (gray → bright accent). Ease (sqrt) so typical
|
|
345
|
+
// mid-stream rates already read as clearly accent-tinted instead of staying
|
|
346
|
+
// gray until the rarely-hit SPEED_MAX ceiling.
|
|
347
|
+
const ratio = Math.sqrt(rate / SPEED_MAX);
|
|
348
|
+
const hex = lerpHex(theme.getColorHex("dim"), theme.getAccentColorHex(), ratio);
|
|
349
|
+
const rateText = ` · ${rate.toFixed(1)} toks/s`;
|
|
350
|
+
const rateSpan = theme.getColorMode() === "truecolor" ? chalk.hex(hex)(rateText) : theme.fg("muted", rateText);
|
|
351
|
+
return coloredGlyph + totalSpan + rateSpan;
|
|
214
352
|
}
|
|
215
353
|
|
|
216
354
|
#startThinkingAnimation(): void {
|
|
217
355
|
if (this.#thinkingDotsTimer) return;
|
|
218
|
-
this.#
|
|
356
|
+
this.#scheduleThinkingFrame();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Eased dwell (ms) for the current pulse frame: a raised cosine over the
|
|
360
|
+
* 8-frame cycle, continuous across the wrap, so the rotation breathes rather
|
|
361
|
+
* than advancing at a fixed interval. */
|
|
362
|
+
#thinkingDotsFrameDelay(): number {
|
|
363
|
+
const phase = (1 - Math.cos((2 * Math.PI * this.#thinkingDotsFrame) / THINKING_DOTS_FRAMES.length)) / 2;
|
|
364
|
+
return THINKING_DOTS_FRAME_MS_MIN + (THINKING_DOTS_FRAME_MS_MAX - THINKING_DOTS_FRAME_MS_MIN) * phase;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Self-rescheduling timeout (not a fixed interval) so each frame can pick its
|
|
368
|
+
* own eased dwell. */
|
|
369
|
+
#scheduleThinkingFrame(): void {
|
|
370
|
+
this.#thinkingDotsTimer = setTimeout(() => this.#advanceThinkingDots(), this.#thinkingDotsFrameDelay());
|
|
219
371
|
this.#thinkingDotsTimer.unref?.();
|
|
220
372
|
}
|
|
221
373
|
|
|
222
374
|
#advanceThinkingDots(): void {
|
|
375
|
+
this.#thinkingDotsTimer = undefined;
|
|
223
376
|
if (!this.#thinkingDots) {
|
|
224
377
|
this.#stopThinkingAnimation();
|
|
225
378
|
return;
|
|
@@ -228,11 +381,12 @@ export class AssistantMessageComponent extends Container {
|
|
|
228
381
|
if (this.#thinkingDots.setText(this.#thinkingDotsLabel())) {
|
|
229
382
|
this.onImageUpdate?.();
|
|
230
383
|
}
|
|
384
|
+
this.#scheduleThinkingFrame();
|
|
231
385
|
}
|
|
232
386
|
|
|
233
387
|
#stopThinkingAnimation(): void {
|
|
234
388
|
if (this.#thinkingDotsTimer) {
|
|
235
|
-
|
|
389
|
+
clearTimeout(this.#thinkingDotsTimer);
|
|
236
390
|
this.#thinkingDotsTimer = undefined;
|
|
237
391
|
}
|
|
238
392
|
this.#thinkingDotsFrame = 0;
|
|
@@ -403,13 +557,13 @@ export class AssistantMessageComponent extends Container {
|
|
|
403
557
|
}
|
|
404
558
|
|
|
405
559
|
#computeShapeKey(message: AssistantMessage): string {
|
|
406
|
-
const parts: string[] = [`htb:${this.hideThinkingBlock ? 1 : 0}`];
|
|
560
|
+
const parts: string[] = [`htb:${this.hideThinkingBlock ? 1 : 0}|pot:${this.proseOnlyThinking ? 1 : 0}`];
|
|
407
561
|
for (const content of message.content) {
|
|
408
562
|
if (content.type === "text") {
|
|
409
563
|
parts.push(canonicalizeMessage(content.text) ? "T1" : "T0");
|
|
410
564
|
} else if (content.type === "thinking") {
|
|
411
|
-
const
|
|
412
|
-
if (!
|
|
565
|
+
const display = resolveThinkingDisplay(content, this.proseOnlyThinking);
|
|
566
|
+
if (!display.visible) parts.push("K0");
|
|
413
567
|
else if (this.hideThinkingBlock) parts.push("KH");
|
|
414
568
|
else parts.push("KV");
|
|
415
569
|
} else {
|
|
@@ -442,8 +596,10 @@ export class AssistantMessageComponent extends Container {
|
|
|
442
596
|
for (const item of this.#fastPathItems) {
|
|
443
597
|
if (item.blockType === "thinking") {
|
|
444
598
|
const content = message.content[item.contentIndex];
|
|
445
|
-
if (content?.type === "thinking"
|
|
446
|
-
|
|
599
|
+
if (content?.type === "thinking") {
|
|
600
|
+
const display = resolveThinkingDisplay(content, this.proseOnlyThinking);
|
|
601
|
+
if (display.text !== item.lastText) return false;
|
|
602
|
+
}
|
|
447
603
|
}
|
|
448
604
|
}
|
|
449
605
|
}
|
|
@@ -476,7 +632,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
476
632
|
if (item.blockType === "text" && content.type === "text") {
|
|
477
633
|
newText = content.text.trim();
|
|
478
634
|
} else if (item.blockType === "thinking" && content.type === "thinking") {
|
|
479
|
-
newText =
|
|
635
|
+
newText = resolveThinkingDisplay(content, this.proseOnlyThinking).text;
|
|
480
636
|
} else {
|
|
481
637
|
this.#fastPathKey = undefined;
|
|
482
638
|
this.#fastPathItems = undefined;
|
|
@@ -487,6 +643,11 @@ export class AssistantMessageComponent extends Container {
|
|
|
487
643
|
item.lastText = newText;
|
|
488
644
|
}
|
|
489
645
|
}
|
|
646
|
+
if (this.#thinkingDots) {
|
|
647
|
+
if (this.#thinkingDots.setText(this.#thinkingDotsLabel())) {
|
|
648
|
+
this.onImageUpdate?.();
|
|
649
|
+
}
|
|
650
|
+
}
|
|
490
651
|
return true;
|
|
491
652
|
}
|
|
492
653
|
|
|
@@ -495,6 +656,42 @@ export class AssistantMessageComponent extends Container {
|
|
|
495
656
|
this.#lastMessage = message;
|
|
496
657
|
this.#lastUpdateTransient = opts?.transient === true;
|
|
497
658
|
|
|
659
|
+
// Streaming-speed gauge: only a live, in-flight render of the single
|
|
660
|
+
// animating hidden-thinking block feeds the shared session tracker. The
|
|
661
|
+
// token count is the provider's own cumulative output — reasoning tokens when
|
|
662
|
+
// reported (Gemini's thoughtsTokenCount, OpenAI's reasoning_tokens), else
|
|
663
|
+
// total output tokens — never a character estimate, which undercounts when
|
|
664
|
+
// the provider streams a summarized reasoning trace. An instantaneous tok/s
|
|
665
|
+
// is derived from this block's delta and handed to the windowed averager.
|
|
666
|
+
// Only transient renders count: the final non-transient render at
|
|
667
|
+
// message_end carries the turn's end-of-stream usage, whose jump would spike
|
|
668
|
+
// the gauge and pollute the next block. Providers that report usage only at
|
|
669
|
+
// turn end leave the live count flat, so the rate stays 0 and the badge
|
|
670
|
+
// self-suppresses (see #thinkingDotsLabel).
|
|
671
|
+
const isThinkingNow = this.#lastUpdateTransient && this.#shouldAnimateThinking(message);
|
|
672
|
+
if (isThinkingNow) {
|
|
673
|
+
const currentTokens = message.usage.reasoningTokens ?? message.usage.output;
|
|
674
|
+
this.#thinkingTokens = currentTokens;
|
|
675
|
+
const now = performance.now();
|
|
676
|
+
if (this.#lastTokenCount !== undefined) {
|
|
677
|
+
const tokenDelta = currentTokens - this.#lastTokenCount;
|
|
678
|
+
const elapsedMs = now - this.#lastTokenTime;
|
|
679
|
+
if (tokenDelta > 0 && elapsedMs > 0) {
|
|
680
|
+
// First live sample of this block: drop the session gauge's prior-turn
|
|
681
|
+
// observations so the windowed average reflects only this block.
|
|
682
|
+
if (!this.#thinkingRateLive) sharedSpeedTracker.reset();
|
|
683
|
+
sharedSpeedTracker.observe((tokenDelta / elapsedMs) * 1000, now);
|
|
684
|
+
this.#thinkingRateLive = true;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
this.#lastTokenCount = currentTokens;
|
|
688
|
+
this.#lastTokenTime = now;
|
|
689
|
+
} else {
|
|
690
|
+
this.#lastTokenCount = undefined;
|
|
691
|
+
this.#thinkingTokens = 0;
|
|
692
|
+
this.#thinkingRateLive = false;
|
|
693
|
+
}
|
|
694
|
+
|
|
498
695
|
// Streaming reflowing Markdown (a mermaid diagram reshaping, a GFM table
|
|
499
696
|
// re-aligning columns) re-lays-out its body each frame; see
|
|
500
697
|
// isTranscriptBlockCommitStable. Detect it from raw text — a Markdown
|
|
@@ -520,7 +717,9 @@ export class AssistantMessageComponent extends Container {
|
|
|
520
717
|
const hasVisibleContent = message.content.some(
|
|
521
718
|
c =>
|
|
522
719
|
(c.type === "text" && canonicalizeMessage(c.text)) ||
|
|
523
|
-
(!this.hideThinkingBlock &&
|
|
720
|
+
(!this.hideThinkingBlock &&
|
|
721
|
+
c.type === "thinking" &&
|
|
722
|
+
resolveThinkingDisplay(c, this.proseOnlyThinking).visible),
|
|
524
723
|
);
|
|
525
724
|
|
|
526
725
|
// Render content in order
|
|
@@ -534,8 +733,8 @@ export class AssistantMessageComponent extends Container {
|
|
|
534
733
|
md.transientRenderCache = this.#lastUpdateTransient;
|
|
535
734
|
this.#contentContainer.addChild(md);
|
|
536
735
|
captureItems?.push({ md, contentIndex: i, blockType: "text", lastText: trimmed });
|
|
537
|
-
} else if (content.type === "thinking" &&
|
|
538
|
-
const thinkingText =
|
|
736
|
+
} else if (content.type === "thinking" && resolveThinkingDisplay(content, this.proseOnlyThinking).visible) {
|
|
737
|
+
const thinkingText = resolveThinkingDisplay(content, this.proseOnlyThinking).text;
|
|
539
738
|
if (this.hideThinkingBlock) {
|
|
540
739
|
thinkingIndex += 1;
|
|
541
740
|
continue;
|
|
@@ -547,7 +746,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
547
746
|
.some(
|
|
548
747
|
c =>
|
|
549
748
|
(c.type === "text" && canonicalizeMessage(c.text)) ||
|
|
550
|
-
(c.type === "thinking" &&
|
|
749
|
+
(c.type === "thinking" && resolveThinkingDisplay(c, this.proseOnlyThinking).visible),
|
|
551
750
|
);
|
|
552
751
|
|
|
553
752
|
// Thinking traces in thinkingText color, italic
|
|
@@ -16,6 +16,7 @@ export class BtwPanelComponent extends Container {
|
|
|
16
16
|
#state: BtwPanelState = "running";
|
|
17
17
|
#answer = "";
|
|
18
18
|
#errorMessage: string | undefined;
|
|
19
|
+
#visibleAnswer = "";
|
|
19
20
|
#closed = false;
|
|
20
21
|
|
|
21
22
|
constructor(options: BtwPanelComponentOptions) {
|
|
@@ -28,12 +29,14 @@ export class BtwPanelComponent extends Container {
|
|
|
28
29
|
appendText(delta: string): void {
|
|
29
30
|
if (!delta || this.#closed) return;
|
|
30
31
|
this.#answer += delta;
|
|
32
|
+
this.#visibleAnswer = replaceTabs(this.#answer).trim();
|
|
31
33
|
this.#rebuild();
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
setAnswer(text: string): void {
|
|
35
37
|
if (this.#closed) return;
|
|
36
38
|
this.#answer = text;
|
|
39
|
+
this.#visibleAnswer = replaceTabs(text).trim();
|
|
37
40
|
this.#rebuild();
|
|
38
41
|
}
|
|
39
42
|
|
|
@@ -59,7 +62,16 @@ export class BtwPanelComponent extends Container {
|
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
isBranchable(): boolean {
|
|
62
|
-
return this
|
|
65
|
+
return this.isCopyable();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
isCopyable(): boolean {
|
|
69
|
+
return this.#state === "complete" && this.#visibleAnswer.length > 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
getCopyText(): string | undefined {
|
|
73
|
+
if (!this.isCopyable()) return undefined;
|
|
74
|
+
return this.#visibleAnswer;
|
|
63
75
|
}
|
|
64
76
|
|
|
65
77
|
close(): void {
|
|
@@ -89,7 +101,7 @@ export class BtwPanelComponent extends Container {
|
|
|
89
101
|
case "running":
|
|
90
102
|
return theme.fg("muted", "Esc cancel /btw");
|
|
91
103
|
case "complete":
|
|
92
|
-
return theme.fg("muted", this.
|
|
104
|
+
return theme.fg("muted", this.isCopyable() ? "c copy · b branch to chat · Esc dismiss" : "Esc dismiss");
|
|
93
105
|
case "aborted":
|
|
94
106
|
return theme.fg("warning", `${theme.status.warning} Cancelled · Esc dismiss`);
|
|
95
107
|
case "error":
|
|
@@ -101,7 +113,7 @@ export class BtwPanelComponent extends Container {
|
|
|
101
113
|
if (this.#state === "error") {
|
|
102
114
|
return new Text(theme.fg("error", replaceTabs(this.#errorMessage ?? "Unknown error")), 1, 0);
|
|
103
115
|
}
|
|
104
|
-
const text =
|
|
116
|
+
const text = this.#visibleAnswer;
|
|
105
117
|
if (!text) {
|
|
106
118
|
const waiting =
|
|
107
119
|
this.#state === "running" ? `${theme.status.pending} Waiting for response…` : "No text returned.";
|
|
@@ -59,6 +59,7 @@ export interface ChatTranscriptBuilderDeps {
|
|
|
59
59
|
getMessageRenderer?: (customType: string) => MessageRenderer | undefined;
|
|
60
60
|
cwd: string;
|
|
61
61
|
hideThinkingBlock?: () => boolean;
|
|
62
|
+
proseOnlyThinking?: () => boolean;
|
|
62
63
|
requestRender: () => void;
|
|
63
64
|
}
|
|
64
65
|
|
|
@@ -247,8 +248,13 @@ export class ChatTranscriptBuilder {
|
|
|
247
248
|
}
|
|
248
249
|
|
|
249
250
|
#appendAssistantMessage(message: Extract<AgentMessage, { role: "assistant" }>): void {
|
|
250
|
-
const assistantComponent = new AssistantMessageComponent(
|
|
251
|
-
|
|
251
|
+
const assistantComponent = new AssistantMessageComponent(
|
|
252
|
+
message,
|
|
253
|
+
this.deps.hideThinkingBlock?.() ?? false,
|
|
254
|
+
() => this.deps.requestRender(),
|
|
255
|
+
this.deps.getMessageRenderer ? undefined : [], // placeholder for thinkingRenderers
|
|
256
|
+
undefined, // placeholder for imageBudget
|
|
257
|
+
this.deps.proseOnlyThinking ? this.deps.proseOnlyThinking() : true,
|
|
252
258
|
);
|
|
253
259
|
this.container.addChild(assistantComponent);
|
|
254
260
|
|
|
@@ -144,6 +144,9 @@ function formatProviderTabLabel(providerId: string): string {
|
|
|
144
144
|
function createProviderTab(providerId: string): ProviderTabState {
|
|
145
145
|
return { id: providerId, label: formatProviderTabLabel(providerId), providerId };
|
|
146
146
|
}
|
|
147
|
+
const TEMPORARY_MODEL_PICKER_HINT =
|
|
148
|
+
"Temporary model selection is session-only. Use Alt+M or /model for role models (default/smol/plan/task/slow/custom roles).";
|
|
149
|
+
|
|
147
150
|
/**
|
|
148
151
|
* Component that renders a model selector with provider tabs and context menu.
|
|
149
152
|
* - Tab/Arrow Left/Right: Switch between provider tabs
|
|
@@ -230,6 +233,10 @@ export class ModelSelectorComponent extends Container {
|
|
|
230
233
|
: "Only showing models with configured API keys (see README for details)";
|
|
231
234
|
this.addChild(new Text(theme.fg("warning", hintText), 0, 0));
|
|
232
235
|
this.addChild(new Spacer(1));
|
|
236
|
+
if (this.#temporaryOnly) {
|
|
237
|
+
this.addChild(new Text(theme.fg("muted", TEMPORARY_MODEL_PICKER_HINT), 0, 0));
|
|
238
|
+
this.addChild(new Spacer(1));
|
|
239
|
+
}
|
|
233
240
|
|
|
234
241
|
// Create header container for tab bar
|
|
235
242
|
this.#headerContainer = new Container();
|
|
@@ -698,11 +705,23 @@ export class ModelSelectorComponent extends Container {
|
|
|
698
705
|
return this.#getActiveTabId() === CANONICAL_TAB;
|
|
699
706
|
}
|
|
700
707
|
|
|
701
|
-
#
|
|
708
|
+
#isModelOverCurrentContext(model: Model): boolean {
|
|
702
709
|
const contextWindow = model.contextWindow ?? 0;
|
|
703
710
|
return this.#currentContextTokens > 0 && contextWindow > 0 && this.#currentContextTokens > contextWindow;
|
|
704
711
|
}
|
|
705
712
|
|
|
713
|
+
#isModelOverContextLimit(model: Model): boolean {
|
|
714
|
+
return this.#temporaryOnly && this.#isModelOverCurrentContext(model);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
#isDefaultRoleActionOverContextLimit(action: MenuRoleAction, model: Model): boolean {
|
|
718
|
+
return action.role === "default" && this.#isModelOverCurrentContext(model);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
#formatCurrentContextLimitSuffix(model: Model): string {
|
|
722
|
+
return ` ${theme.status.disabled} context>${formatNumber(model.contextWindow ?? 0).toLowerCase()}`;
|
|
723
|
+
}
|
|
724
|
+
|
|
706
725
|
#isItemDisabled(item: ModelItem | CanonicalModelItem): boolean {
|
|
707
726
|
return this.#isModelOverContextLimit(item.model);
|
|
708
727
|
}
|
|
@@ -711,7 +730,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
711
730
|
if (!this.#isModelOverContextLimit(model)) {
|
|
712
731
|
return "";
|
|
713
732
|
}
|
|
714
|
-
return
|
|
733
|
+
return this.#formatCurrentContextLimitSuffix(model);
|
|
715
734
|
}
|
|
716
735
|
|
|
717
736
|
#getVisibleItems(): ReadonlyArray<ModelItem | CanonicalModelItem> {
|
|
@@ -1046,6 +1065,50 @@ export class ModelSelectorComponent extends Container {
|
|
|
1046
1065
|
: this.#filteredModels[this.#selectedIndex];
|
|
1047
1066
|
}
|
|
1048
1067
|
|
|
1068
|
+
#isMenuRoleIndexDisabled(index: number, selectedItem: ModelItem | CanonicalModelItem): boolean {
|
|
1069
|
+
const action = this.#menuRoleActions[index];
|
|
1070
|
+
return action ? this.#isDefaultRoleActionOverContextLimit(action, selectedItem.model) : false;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
#coerceMenuSelectedIndex(index: number, selectedItem: ModelItem | CanonicalModelItem): number {
|
|
1074
|
+
const maxIndex = this.#menuRoleActions.length - 1;
|
|
1075
|
+
if (maxIndex < 0) {
|
|
1076
|
+
return 0;
|
|
1077
|
+
}
|
|
1078
|
+
const clamped = Math.max(0, Math.min(index, maxIndex));
|
|
1079
|
+
if (!this.#isMenuRoleIndexDisabled(clamped, selectedItem)) {
|
|
1080
|
+
return clamped;
|
|
1081
|
+
}
|
|
1082
|
+
for (let i = clamped + 1; i <= maxIndex; i++) {
|
|
1083
|
+
if (!this.#isMenuRoleIndexDisabled(i, selectedItem)) {
|
|
1084
|
+
return i;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
for (let i = clamped - 1; i >= 0; i--) {
|
|
1088
|
+
if (!this.#isMenuRoleIndexDisabled(i, selectedItem)) {
|
|
1089
|
+
return i;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
return clamped;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
#moveMenuSelection(delta: number, selectedItem: ModelItem | CanonicalModelItem, optionCount: number): void {
|
|
1096
|
+
let index = this.#menuSelectedIndex;
|
|
1097
|
+
for (let step = 0; step < optionCount; step++) {
|
|
1098
|
+
index = (index + delta + optionCount) % optionCount;
|
|
1099
|
+
if (this.#menuStep !== "role" || !this.#isMenuRoleIndexDisabled(index, selectedItem)) {
|
|
1100
|
+
this.#menuSelectedIndex = index;
|
|
1101
|
+
this.#updateMenu();
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
this.#menuSelectedIndex =
|
|
1106
|
+
this.#menuStep === "role"
|
|
1107
|
+
? this.#coerceMenuSelectedIndex(this.#menuSelectedIndex, selectedItem)
|
|
1108
|
+
: this.#menuSelectedIndex;
|
|
1109
|
+
this.#updateMenu();
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1049
1112
|
#openMenu(): void {
|
|
1050
1113
|
const selectedItem = this.#getSelectedItem();
|
|
1051
1114
|
if (!selectedItem || this.#isItemDisabled(selectedItem)) return;
|
|
@@ -1053,7 +1116,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
1053
1116
|
this.#isMenuOpen = true;
|
|
1054
1117
|
this.#menuStep = "role";
|
|
1055
1118
|
this.#menuSelectedRole = null;
|
|
1056
|
-
this.#menuSelectedIndex = 0;
|
|
1119
|
+
this.#menuSelectedIndex = this.#coerceMenuSelectedIndex(0, selectedItem);
|
|
1057
1120
|
// Collapse the model list while the action/thinking menu is open so the
|
|
1058
1121
|
// menu owns the full viewport instead of stacking below a now-irrelevant
|
|
1059
1122
|
// (and often off-screen) list.
|
|
@@ -1086,7 +1149,9 @@ export class ModelSelectorComponent extends Container {
|
|
|
1086
1149
|
})
|
|
1087
1150
|
: this.#menuRoleActions.map((action, index) => {
|
|
1088
1151
|
const prefix = index === this.#menuSelectedIndex ? ` ${theme.nav.cursor} ` : " ";
|
|
1089
|
-
|
|
1152
|
+
const disabled = this.#isDefaultRoleActionOverContextLimit(action, selectedItem.model);
|
|
1153
|
+
const suffix = disabled ? this.#formatCurrentContextLimitSuffix(selectedItem.model) : "";
|
|
1154
|
+
return `${prefix}${action.label}${suffix}`;
|
|
1090
1155
|
});
|
|
1091
1156
|
|
|
1092
1157
|
const selectedRoleName = this.#menuSelectedRole ? getRoleInfo(this.#menuSelectedRole, this.#settings).name : "";
|
|
@@ -1224,21 +1289,19 @@ export class ModelSelectorComponent extends Container {
|
|
|
1224
1289
|
if (optionCount === 0) return;
|
|
1225
1290
|
|
|
1226
1291
|
if (matchesSelectUp(keyData)) {
|
|
1227
|
-
this.#
|
|
1228
|
-
this.#updateMenu();
|
|
1292
|
+
this.#moveMenuSelection(-1, selectedItem, optionCount);
|
|
1229
1293
|
return;
|
|
1230
1294
|
}
|
|
1231
1295
|
|
|
1232
1296
|
if (matchesSelectDown(keyData)) {
|
|
1233
|
-
this.#
|
|
1234
|
-
this.#updateMenu();
|
|
1297
|
+
this.#moveMenuSelection(1, selectedItem, optionCount);
|
|
1235
1298
|
return;
|
|
1236
1299
|
}
|
|
1237
1300
|
|
|
1238
1301
|
if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
1239
1302
|
if (this.#menuStep === "role") {
|
|
1240
1303
|
const action = this.#menuRoleActions[this.#menuSelectedIndex];
|
|
1241
|
-
if (!action) return;
|
|
1304
|
+
if (!action || this.#isDefaultRoleActionOverContextLimit(action, selectedItem.model)) return;
|
|
1242
1305
|
this.#menuSelectedRole = action.role;
|
|
1243
1306
|
this.#menuStep = "thinking";
|
|
1244
1307
|
this.#menuSelectedIndex = this.#getThinkingPreselectIndex(action.role, selectedItem.model);
|
|
@@ -117,7 +117,7 @@ export class OmfgPanelComponent extends Container {
|
|
|
117
117
|
case "saved":
|
|
118
118
|
return theme.fg(
|
|
119
119
|
"success",
|
|
120
|
-
`${theme.status.success} Registered live · ${replaceTabs(this.#savedPath ?? "saved")}`,
|
|
120
|
+
`${theme.status.success} Registered live · ${replaceTabs(this.#savedPath ?? "saved")} · Esc dismiss`,
|
|
121
121
|
);
|
|
122
122
|
case "rejected":
|
|
123
123
|
return theme.fg("warning", `${theme.status.warning} Not saved · Esc dismiss`);
|
|
@@ -454,8 +454,6 @@ export interface SessionSelectorOptions {
|
|
|
454
454
|
loadAllSessions?: () => Promise<SessionInfo[]>;
|
|
455
455
|
/** Preloaded all-projects list; cached so the first Tab toggle is instant. */
|
|
456
456
|
allSessions?: SessionInfo[];
|
|
457
|
-
/** Open directly in all-projects scope (e.g. the current folder has no sessions). */
|
|
458
|
-
startInAllScope?: boolean;
|
|
459
457
|
/**
|
|
460
458
|
* Reads the live terminal height so the visible window fits the viewport.
|
|
461
459
|
* Omitted only in tests; defaults to a conservative 24 rows.
|
|
@@ -493,11 +491,6 @@ export class SessionSelectorComponent extends Container {
|
|
|
493
491
|
this.#loadAllSessions = options.loadAllSessions;
|
|
494
492
|
this.#folderSessions = sessions;
|
|
495
493
|
this.#globalSessions = options.allSessions ?? null;
|
|
496
|
-
// Open in all-projects scope when asked and we already have that list
|
|
497
|
-
// (e.g. the current folder has no sessions to show).
|
|
498
|
-
const startAll = options.startInAllScope === true && this.#globalSessions !== null;
|
|
499
|
-
this.#scope = startAll ? "all" : "folder";
|
|
500
|
-
const initialSessions = startAll ? this.#globalSessions! : sessions;
|
|
501
494
|
// Add header
|
|
502
495
|
this.addChild(new Spacer(1));
|
|
503
496
|
this.#headerText = new Text(this.#headerLabel(), 1, 0);
|
|
@@ -506,8 +499,10 @@ export class SessionSelectorComponent extends Container {
|
|
|
506
499
|
this.addChild(new DynamicBorder());
|
|
507
500
|
this.addChild(new Spacer(1));
|
|
508
501
|
this.addChild(this.#messageContainer);
|
|
509
|
-
// Create session list
|
|
510
|
-
|
|
502
|
+
// Create session list in folder scope; the empty-state hint invites the
|
|
503
|
+
// user to Tab into all-projects rather than silently surfacing other
|
|
504
|
+
// projects' history (issue #3099).
|
|
505
|
+
this.#sessionList = new SessionList(sessions, false, options.historyMatcher, options.getTerminalRows);
|
|
511
506
|
this.#sessionList.onSelect = onSelect;
|
|
512
507
|
this.#sessionList.onCancel = onCancel;
|
|
513
508
|
this.#sessionList.onExit = onExit;
|
|
@@ -148,7 +148,7 @@ export class SnapcompactShapePreview implements Component {
|
|
|
148
148
|
while (normalize(text).length < capacity) {
|
|
149
149
|
text += ` ${PREVIEW_TEXT}`;
|
|
150
150
|
}
|
|
151
|
-
const frame = renderMany(text, { shape, frameSize: SRC_FRAME_PX, maxFrames: 1 })[0];
|
|
151
|
+
const frame = (await renderMany(text, { shape, frameSize: SRC_FRAME_PX, maxFrames: 1 }))[0];
|
|
152
152
|
if (!frame) throw new Error("empty sample frame");
|
|
153
153
|
const edgePx = SRC_FRAME_PX * ZOOM_SCALE;
|
|
154
154
|
const zoomed = await new Bun.Image(Buffer.from(frame.data, "base64"))
|