@oh-my-pi/pi-tui 18.4.1 → 18.4.2
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 +14 -0
- package/dist/types/chrome/transcript-container.d.ts +15 -1
- package/dist/types/overlays/model-browser.d.ts +15 -0
- package/dist/types/overlays/rewind-selector.d.ts +2 -1
- package/dist/types/prompt/composer-attachments.d.ts +7 -0
- package/dist/types/prompt/model-mention-autocomplete.d.ts +4 -1
- package/dist/types/render/width-aware-text.d.ts +6 -0
- package/dist/types/terminal.d.ts +7 -0
- package/dist/types/theme/color.d.ts +1 -0
- package/dist/types/theme/session-color.d.ts +3 -3
- package/dist/types/theme/shimmer.d.ts +1 -1
- package/dist/types/theme/theme-class.d.ts +1 -1
- package/dist/types/tui.d.ts +3 -2
- package/package.json +9 -9
- package/src/chat/tool-execution.ts +24 -4
- package/src/chrome/transcript-container.ts +76 -14
- package/src/components/editor.ts +28 -6
- package/src/components/image.ts +15 -7
- package/src/components/loader.ts +37 -17
- package/src/overlays/agent-hub.ts +16 -6
- package/src/overlays/copy-selector.ts +5 -0
- package/src/overlays/model-browser.ts +106 -25
- package/src/overlays/rewind-selector.ts +10 -2
- package/src/prompt/composer-attachments.ts +54 -7
- package/src/prompt/composer.ts +5 -2
- package/src/prompt/custom-editor.ts +24 -6
- package/src/prompt/model-mention-autocomplete.ts +22 -10
- package/src/render/width-aware-text.ts +9 -0
- package/src/status-line/component.ts +213 -132
- package/src/status-line/segments.ts +34 -29
- package/src/terminal.ts +37 -11
- package/src/theme/color.ts +15 -0
- package/src/theme/session-color.ts +26 -4
- package/src/theme/shimmer.ts +55 -76
- package/src/theme/theme-class.ts +29 -12
- package/src/tui.ts +142 -112
- package/src/utils.ts +15 -0
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from "@oh-my-pi/pi-ai/usage/google-antigravity";
|
|
8
8
|
import { getNextTimeBasedPricingTransition } from "@oh-my-pi/pi-catalog/models";
|
|
9
9
|
import type { Model, ModelCost } from "@oh-my-pi/pi-catalog/types";
|
|
10
|
-
import type { VcsRepo } from "@oh-my-pi/pi-natives";
|
|
10
|
+
import type { VcsGitRepo, VcsRepo } from "@oh-my-pi/pi-natives";
|
|
11
11
|
import * as vcs from "@oh-my-pi/pi-natives/vcs";
|
|
12
12
|
import {
|
|
13
13
|
type Component,
|
|
@@ -255,57 +255,62 @@ interface ContextUsageMemo {
|
|
|
255
255
|
skillsRef: readonly any[] | undefined;
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
258
|
+
/**
|
|
259
|
+
* Snapshot of the cheap external inputs a cached bar depends on. Mutable and
|
|
260
|
+
* refilled in place: the per-frame cache probe writes into a recycled
|
|
261
|
+
* instance instead of allocating a fresh ~50-field object every frame.
|
|
262
|
+
*/
|
|
263
|
+
class StatusLineExternalInputs {
|
|
264
|
+
themeRef: unknown = undefined;
|
|
265
|
+
themeEpoch = 0;
|
|
266
|
+
projectDir = "";
|
|
267
|
+
sessionRef: StatusLineSession | undefined = undefined;
|
|
268
|
+
sessionFile: string | undefined = undefined;
|
|
269
|
+
sessionSettingsRef: unknown = undefined;
|
|
270
|
+
sessionSettingsRevision = 0;
|
|
271
|
+
globalSettingsRevision = 0;
|
|
272
|
+
stateRef: unknown = undefined;
|
|
273
|
+
stateMessagesRef: readonly AgentMessage[] = EMPTY_MESSAGES;
|
|
274
|
+
stateMessagesLength = 0;
|
|
275
|
+
stateLastMessageRef: AgentMessage | undefined = undefined;
|
|
276
|
+
stateLastMessageContentRef: unknown = undefined;
|
|
277
|
+
messagesRef: readonly AgentMessage[] = EMPTY_MESSAGES;
|
|
278
|
+
messagesLength = 0;
|
|
279
|
+
lastMessageRef: AgentMessage | undefined = undefined;
|
|
280
|
+
lastMessageRole: string | undefined = undefined;
|
|
281
|
+
lastMessageTimestamp = 0;
|
|
282
|
+
lastMessageContentSize = 0;
|
|
283
|
+
lastMessageBlockCount = 0;
|
|
284
|
+
lastMessageUsageTotal = 0;
|
|
285
|
+
lastMessageStopReason: string | undefined = undefined;
|
|
286
|
+
modelRef: unknown = undefined;
|
|
287
|
+
sessionModelRef: unknown = undefined;
|
|
288
|
+
sessionModelId: string | undefined = undefined;
|
|
289
|
+
sessionModelContextWindow: Model["contextWindow"] | undefined = undefined;
|
|
290
|
+
modelId: string | undefined = undefined;
|
|
291
|
+
modelName: string | undefined = undefined;
|
|
292
|
+
modelProvider: string | undefined = undefined;
|
|
293
|
+
modelContextWindow: Model["contextWindow"] | undefined = undefined;
|
|
294
|
+
modelThinking: Model["thinking"] = undefined;
|
|
295
|
+
thinkingLevel: unknown = undefined;
|
|
296
|
+
modelCostRef: unknown = undefined;
|
|
297
|
+
contextUsageRevision = 0;
|
|
298
|
+
systemPromptRef: unknown = undefined;
|
|
299
|
+
systemPromptLength = 0;
|
|
300
|
+
systemPromptContentSize = 0;
|
|
301
|
+
toolsRef: readonly unknown[] | undefined = undefined;
|
|
302
|
+
toolsLength = 0;
|
|
303
|
+
toolSchemaMetadataRevision = 0;
|
|
304
|
+
tokenizerRef: unknown = undefined;
|
|
305
|
+
skillsRef: unknown = undefined;
|
|
306
|
+
skillsLength = 0;
|
|
307
|
+
sessionName: string | undefined = undefined;
|
|
308
|
+
sessionId: string | undefined = undefined;
|
|
309
|
+
isStreaming: boolean | undefined = undefined;
|
|
310
|
+
isAutoThinking: boolean | undefined = undefined;
|
|
311
|
+
isFastModeActive = false;
|
|
312
|
+
anthropicSlowModeLabel: string | undefined = undefined;
|
|
313
|
+
compactionSpeculation: unknown = undefined;
|
|
309
314
|
}
|
|
310
315
|
|
|
311
316
|
interface CachedStatusLine {
|
|
@@ -445,6 +450,17 @@ function hasGitBackedSegment(segments: readonly StatusLineSegmentId[]): boolean
|
|
|
445
450
|
|
|
446
451
|
type StatusLineLayout = "box" | "band" | "plain-full" | "plain-left" | "plain-right";
|
|
447
452
|
|
|
453
|
+
/**
|
|
454
|
+
* Git views of immutable `VcsRepo` handles. `asGit()` mints a fresh napi
|
|
455
|
+
* wrapper around the same backend handle on every call, which the per-frame
|
|
456
|
+
* branch lookup would otherwise pay each render.
|
|
457
|
+
*/
|
|
458
|
+
const GIT_VIEW = Symbol("status-line.git-view");
|
|
459
|
+
|
|
460
|
+
interface GitViewTaggedRepo extends VcsRepo {
|
|
461
|
+
[GIT_VIEW]?: VcsGitRepo | null;
|
|
462
|
+
}
|
|
463
|
+
|
|
448
464
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
449
465
|
// StatusLineComponent
|
|
450
466
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -465,6 +481,22 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
465
481
|
};
|
|
466
482
|
#statusLineInputRevision = 0;
|
|
467
483
|
#statusLineClockTick = 0;
|
|
484
|
+
/** Reused probe buffer for {@link #readStatusLineExternalInputs}; handed to the new cache entry on a miss. */
|
|
485
|
+
#externalInputsProbe = new StatusLineExternalInputs();
|
|
486
|
+
/** `adjustHsv` result for the gauge's threshold tint; pure in its source hex. */
|
|
487
|
+
#dimmedAccentMemo: { sourceHex: string; hex: string } | undefined;
|
|
488
|
+
/** Gauge boundary markers; a pure function of the session's compaction settings, model, and window. */
|
|
489
|
+
#compactionBoundariesMemo:
|
|
490
|
+
| {
|
|
491
|
+
sessionRef: StatusLineSession;
|
|
492
|
+
contextWindow: number;
|
|
493
|
+
modelRef: unknown;
|
|
494
|
+
settingsRef: unknown;
|
|
495
|
+
settingsRevision: number;
|
|
496
|
+
globalSettingsRevision: number;
|
|
497
|
+
boundaries: CompactionBoundaries | null;
|
|
498
|
+
}
|
|
499
|
+
| undefined;
|
|
468
500
|
#settings: StatusLineSettings = {};
|
|
469
501
|
#effectiveSettings: EffectiveStatusLineSettings | undefined;
|
|
470
502
|
#cachedBranch: string | null | undefined = undefined;
|
|
@@ -1070,8 +1102,7 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
1070
1102
|
const sessionName = sessionAccentEnabled ? this.session.sessionManager?.getSessionName() : undefined;
|
|
1071
1103
|
const idleHex = theme.getColorHex("dim");
|
|
1072
1104
|
const workingHex =
|
|
1073
|
-
(sessionName
|
|
1074
|
-
theme.getColorHex("accent");
|
|
1105
|
+
(sessionName && getSessionAccentHex(sessionName, theme.sessionAccentInputs)) || theme.getColorHex("accent");
|
|
1075
1106
|
const now = Date.now();
|
|
1076
1107
|
if (working !== this.#brandWorking) {
|
|
1077
1108
|
const previousTargetHex = this.#brandWorking ? workingHex : idleHex;
|
|
@@ -1267,7 +1298,12 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
1267
1298
|
|
|
1268
1299
|
const gitCwd = activeRepoCache.effectiveGitCwd;
|
|
1269
1300
|
if (!repository) return null;
|
|
1270
|
-
const
|
|
1301
|
+
const taggedRepository: GitViewTaggedRepo = repository;
|
|
1302
|
+
let gitRepository = taggedRepository[GIT_VIEW];
|
|
1303
|
+
if (gitRepository === undefined) {
|
|
1304
|
+
gitRepository = repository.asGit();
|
|
1305
|
+
taggedRepository[GIT_VIEW] = gitRepository;
|
|
1306
|
+
}
|
|
1271
1307
|
if (!gitRepository) {
|
|
1272
1308
|
if (this.#jjBranchActive || Date.now() - this.#jjBranchLastFetch < JJ_REFRESH_TTL_MS) {
|
|
1273
1309
|
return this.#cachedJjBranch;
|
|
@@ -2294,8 +2330,9 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
2294
2330
|
* going through this component's setters. This deliberately uses refs,
|
|
2295
2331
|
* scalars, and string lengths only: no schema estimation, serialization,
|
|
2296
2332
|
* usage aggregation, VCS probing, or segment getters run on a cache hit.
|
|
2333
|
+
* Fills `target` in place so the per-frame probe allocates nothing.
|
|
2297
2334
|
*/
|
|
2298
|
-
#readStatusLineExternalInputs(
|
|
2335
|
+
#readStatusLineExternalInputs(target: StatusLineExternalInputs): void {
|
|
2299
2336
|
const state = this.session.state;
|
|
2300
2337
|
const stateMessages = state.messages ?? EMPTY_MESSAGES;
|
|
2301
2338
|
const stateLastMessage = stateMessages[stateMessages.length - 1];
|
|
@@ -2346,62 +2383,61 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
2346
2383
|
if (typeof part === "string") systemPromptContentSize += part.length;
|
|
2347
2384
|
}
|
|
2348
2385
|
const tools = this.session.agent?.state?.tools;
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
};
|
|
2386
|
+
const skills = this.session.skills;
|
|
2387
|
+
target.themeRef = theme;
|
|
2388
|
+
target.themeEpoch = getThemeEpoch();
|
|
2389
|
+
target.projectDir = getProjectDir();
|
|
2390
|
+
target.sessionRef = this.session;
|
|
2391
|
+
target.sessionFile = this.session.sessionFile;
|
|
2392
|
+
target.sessionSettingsRef = this.host.getSessionSettingsIdentity(this.session);
|
|
2393
|
+
target.sessionSettingsRevision = this.host.getSessionSettingsRevision(this.session);
|
|
2394
|
+
target.globalSettingsRevision = this.host.getSettingsRevision();
|
|
2395
|
+
target.stateRef = state;
|
|
2396
|
+
target.stateMessagesRef = stateMessages;
|
|
2397
|
+
target.stateMessagesLength = stateMessages.length;
|
|
2398
|
+
target.stateLastMessageRef = stateLastMessage;
|
|
2399
|
+
target.stateLastMessageContentRef = stateLastMessageContent;
|
|
2400
|
+
target.messagesRef = messages;
|
|
2401
|
+
target.messagesLength = messages.length;
|
|
2402
|
+
target.lastMessageRef = lastMessage;
|
|
2403
|
+
target.lastMessageRole = lastMessageView?.role;
|
|
2404
|
+
target.lastMessageTimestamp = lastMessageView?.timestamp ?? 0;
|
|
2405
|
+
target.lastMessageContentSize = lastMessageContentSize;
|
|
2406
|
+
target.lastMessageBlockCount = lastMessageBlockCount;
|
|
2407
|
+
target.lastMessageUsageTotal = lastMessageView?.usage?.totalTokens ?? 0;
|
|
2408
|
+
target.lastMessageStopReason = lastMessageView?.stopReason;
|
|
2409
|
+
target.modelRef = model;
|
|
2410
|
+
target.sessionModelRef = sessionModel;
|
|
2411
|
+
target.sessionModelId = sessionModel?.id;
|
|
2412
|
+
target.sessionModelContextWindow = sessionModel?.contextWindow;
|
|
2413
|
+
target.modelId = model?.id;
|
|
2414
|
+
target.modelName = model?.name;
|
|
2415
|
+
target.modelProvider = model?.provider;
|
|
2416
|
+
target.modelContextWindow = model?.contextWindow;
|
|
2417
|
+
target.modelThinking = model?.thinking;
|
|
2418
|
+
target.thinkingLevel = state.thinkingLevel;
|
|
2419
|
+
target.modelCostRef = model?.cost;
|
|
2420
|
+
target.contextUsageRevision = this.session.contextUsageRevision ?? 0;
|
|
2421
|
+
target.systemPromptRef = systemPrompt;
|
|
2422
|
+
target.systemPromptLength = systemPrompt?.length ?? 0;
|
|
2423
|
+
target.systemPromptContentSize = systemPromptContentSize;
|
|
2424
|
+
target.toolsRef = tools;
|
|
2425
|
+
target.toolsLength = tools?.length ?? 0;
|
|
2426
|
+
target.toolSchemaMetadataRevision = tools ? getToolSchemaMetadataRevision(tools) : 0;
|
|
2427
|
+
target.tokenizerRef = this.session.agent?.tokenizer;
|
|
2428
|
+
target.skillsRef = skills;
|
|
2429
|
+
target.skillsLength = skills?.length ?? 0;
|
|
2430
|
+
target.sessionName = this.session.sessionManager?.getSessionName?.();
|
|
2431
|
+
target.sessionId = this.session.sessionManager?.getSessionId?.();
|
|
2432
|
+
target.isStreaming = this.session.isStreaming;
|
|
2433
|
+
target.isAutoThinking = this.session.isAutoThinking;
|
|
2434
|
+
target.isFastModeActive =
|
|
2435
|
+
typeof this.session.isFastModeActive === "function" ? this.session.isFastModeActive() : false;
|
|
2436
|
+
target.anthropicSlowModeLabel =
|
|
2437
|
+
typeof this.session.getAnthropicSlowModeLabel === "function"
|
|
2438
|
+
? this.session.getAnthropicSlowModeLabel()
|
|
2439
|
+
: undefined;
|
|
2440
|
+
target.compactionSpeculation = this.session.compactionSpeculation;
|
|
2405
2441
|
}
|
|
2406
2442
|
|
|
2407
2443
|
#sameStatusLineExternalInputs(left: StatusLineExternalInputs, right: StatusLineExternalInputs): boolean {
|
|
@@ -2496,7 +2532,8 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
2496
2532
|
|
|
2497
2533
|
#buildStatusLine(width: number, layout: StatusLineLayout = "box", previewTitle?: string): CachedStatusLine {
|
|
2498
2534
|
const effectiveSettings = this.#resolveSettings();
|
|
2499
|
-
const externalInputs = this.#
|
|
2535
|
+
const externalInputs = this.#externalInputsProbe;
|
|
2536
|
+
this.#readStatusLineExternalInputs(externalInputs);
|
|
2500
2537
|
const nowMs = Date.now();
|
|
2501
2538
|
const clockTick = this.#statusLineClock(nowMs, effectiveSettings);
|
|
2502
2539
|
if (clockTick !== this.#statusLineClockTick) {
|
|
@@ -2516,6 +2553,8 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
2516
2553
|
return cached;
|
|
2517
2554
|
}
|
|
2518
2555
|
|
|
2556
|
+
// The probe now belongs to the new cache entry; later probes need their own buffer.
|
|
2557
|
+
this.#externalInputsProbe = new StatusLineExternalInputs();
|
|
2519
2558
|
const content = this.#renderStatusLine(width, layout, previewTitle, nowMs);
|
|
2520
2559
|
const result = {
|
|
2521
2560
|
content,
|
|
@@ -2642,8 +2681,11 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
2642
2681
|
if (subagentBadge) rightParts.unshift(subagentBadge);
|
|
2643
2682
|
}
|
|
2644
2683
|
const topFillWidth = Math.max(0, width);
|
|
2645
|
-
|
|
2646
|
-
const
|
|
2684
|
+
// These arrays are local to this render; overflow handling can mutate them.
|
|
2685
|
+
const left = leftParts;
|
|
2686
|
+
const right = rightParts;
|
|
2687
|
+
const leftWidths = left.map(part => visibleWidth(part));
|
|
2688
|
+
const rightWidths = right.map(part => visibleWidth(part));
|
|
2647
2689
|
|
|
2648
2690
|
const leftSepWidth = visibleWidth(separatorDef.left);
|
|
2649
2691
|
const rightSepWidth = visibleWidth(separatorDef.right);
|
|
@@ -2657,15 +2699,15 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
2657
2699
|
const bandCap = layout === "band" && separatorDef.endCaps && !transparentBg ? theme.sep.powerlineCapLeft : "";
|
|
2658
2700
|
const bandCapWidth = visibleWidth(bandCap);
|
|
2659
2701
|
|
|
2660
|
-
const groupWidth = (
|
|
2661
|
-
if (
|
|
2662
|
-
const partsWidth =
|
|
2663
|
-
const sepTotal =
|
|
2702
|
+
const groupWidth = (widths: number[], capWidth: number, sepWidth: number): number => {
|
|
2703
|
+
if (widths.length === 0) return 0;
|
|
2704
|
+
const partsWidth = widths.reduce((sum, partWidth) => sum + partWidth, 0);
|
|
2705
|
+
const sepTotal = (widths.length - 1) * (sepWidth + 2);
|
|
2664
2706
|
return partsWidth + sepTotal + 2 + capWidth;
|
|
2665
2707
|
};
|
|
2666
2708
|
|
|
2667
|
-
let leftWidth = groupWidth(
|
|
2668
|
-
let rightWidth = groupWidth(
|
|
2709
|
+
let leftWidth = groupWidth(leftWidths, leftCapWidth + bandCapWidth, leftSepWidth);
|
|
2710
|
+
let rightWidth = groupWidth(rightWidths, rightCapWidth, rightSepWidth);
|
|
2669
2711
|
// Embedded mode removes the standalone context segment before overflow
|
|
2670
2712
|
// handling, so the gauge must reserve enough room for both labels. Without
|
|
2671
2713
|
// this budget a long path/session title can leave a one-cell gap: the
|
|
@@ -2694,23 +2736,27 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
2694
2736
|
if (nameSegIdx >= 0 && totalWidth() > topFillWidth) {
|
|
2695
2737
|
// Badge/job parts were unshifted ahead of the tracked segment ids.
|
|
2696
2738
|
const nameIdx = nameSegIdx + (right.length - rightSegIds.length);
|
|
2697
|
-
const currentNameVW =
|
|
2739
|
+
const currentNameVW = rightWidths[nameIdx];
|
|
2698
2740
|
const minNameVW = 8;
|
|
2699
2741
|
const shrinkBy = Math.min(Math.max(0, currentNameVW - minNameVW), totalWidth() - topFillWidth);
|
|
2700
2742
|
if (shrinkBy > 0) {
|
|
2701
2743
|
right[nameIdx] = truncateToWidth(right[nameIdx], currentNameVW - shrinkBy);
|
|
2702
|
-
|
|
2744
|
+
const nameWidth = visibleWidth(right[nameIdx]);
|
|
2745
|
+
rightWidth += nameWidth - currentNameVW;
|
|
2746
|
+
rightWidths[nameIdx] = nameWidth;
|
|
2703
2747
|
}
|
|
2704
2748
|
}
|
|
2705
2749
|
while (totalWidth() > topFillWidth && right.length > 0) {
|
|
2750
|
+
const removedWidth = rightWidths[right.length - 1];
|
|
2706
2751
|
right.pop();
|
|
2707
|
-
|
|
2752
|
+
rightWidths.pop();
|
|
2753
|
+
rightWidth = right.length > 0 ? rightWidth - removedWidth - rightSepWidth - 2 : 0;
|
|
2708
2754
|
}
|
|
2709
2755
|
// Shrink path before dropping left segments — path is the only elastic segment
|
|
2710
2756
|
const pathIdx = leftSegIds.indexOf("path");
|
|
2711
2757
|
if (pathIdx >= 0 && totalWidth() > topFillWidth) {
|
|
2712
2758
|
const overflow = totalWidth() - topFillWidth;
|
|
2713
|
-
const currentPathVW =
|
|
2759
|
+
const currentPathVW = leftWidths[pathIdx];
|
|
2714
2760
|
const minPathVW = 8; // icon + ellipsis + a few chars
|
|
2715
2761
|
const shrinkable = currentPathVW - minPathVW;
|
|
2716
2762
|
if (shrinkable > 0) {
|
|
@@ -2723,9 +2769,10 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
2723
2769
|
});
|
|
2724
2770
|
let reRendered = renderSegment("path", pathCtx(newMaxLen));
|
|
2725
2771
|
if (reRendered.visible && reRendered.content) {
|
|
2772
|
+
let pathWidth = visibleWidth(reRendered.content);
|
|
2726
2773
|
// maxLength governs path text, not icon prefix; iterate to compensate
|
|
2727
2774
|
for (let i = 0; i < 8; i++) {
|
|
2728
|
-
const saved = currentPathVW -
|
|
2775
|
+
const saved = currentPathVW - pathWidth;
|
|
2729
2776
|
if (saved >= shrinkBy) break;
|
|
2730
2777
|
const nextMaxLen = Math.max(4, newMaxLen - (shrinkBy - saved));
|
|
2731
2778
|
if (nextMaxLen >= newMaxLen) break; // no progress or hit floor
|
|
@@ -2733,9 +2780,11 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
2733
2780
|
const adjusted = renderSegment("path", pathCtx(newMaxLen));
|
|
2734
2781
|
if (!adjusted.visible || !adjusted.content) break;
|
|
2735
2782
|
reRendered = adjusted;
|
|
2783
|
+
pathWidth = visibleWidth(adjusted.content);
|
|
2736
2784
|
}
|
|
2737
2785
|
left[pathIdx] = reRendered.content;
|
|
2738
|
-
leftWidth
|
|
2786
|
+
leftWidth += pathWidth - currentPathVW;
|
|
2787
|
+
leftWidths[pathIdx] = pathWidth;
|
|
2739
2788
|
}
|
|
2740
2789
|
}
|
|
2741
2790
|
}
|
|
@@ -2752,9 +2801,11 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
2752
2801
|
|
|
2753
2802
|
while (totalWidth() > topFillWidth && left.length > 0) {
|
|
2754
2803
|
const dropIdx = leftOverflowDropIndex();
|
|
2804
|
+
const removedWidth = leftWidths[dropIdx];
|
|
2755
2805
|
left.splice(dropIdx, 1);
|
|
2806
|
+
leftWidths.splice(dropIdx, 1);
|
|
2756
2807
|
leftSegIds.splice(dropIdx, 1);
|
|
2757
|
-
leftWidth =
|
|
2808
|
+
leftWidth = left.length > 0 ? leftWidth - removedWidth - leftSepWidth - 2 : 0;
|
|
2758
2809
|
}
|
|
2759
2810
|
}
|
|
2760
2811
|
|
|
@@ -2906,8 +2957,12 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
2906
2957
|
const speculationColor = theme.getFgAnsi("muted");
|
|
2907
2958
|
const overflowColor = theme.getFgAnsi("error");
|
|
2908
2959
|
const rawAccentHex = accentHex ?? theme.getColorHex("borderAccent");
|
|
2909
|
-
|
|
2910
|
-
|
|
2960
|
+
let dimmedAccent = this.#dimmedAccentMemo;
|
|
2961
|
+
if (dimmedAccent?.sourceHex !== rawAccentHex) {
|
|
2962
|
+
dimmedAccent = { sourceHex: rawAccentHex, hex: adjustHsv(rawAccentHex, { s: 0.7, v: 0.75 }) };
|
|
2963
|
+
this.#dimmedAccentMemo = dimmedAccent;
|
|
2964
|
+
}
|
|
2965
|
+
const thresholdColor = getSessionAccentAnsi(dimmedAccent.hex) ?? usedColor;
|
|
2911
2966
|
|
|
2912
2967
|
let out = "\x1b[49m";
|
|
2913
2968
|
let activeColor = "";
|
|
@@ -2938,9 +2993,35 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
2938
2993
|
|
|
2939
2994
|
/** Auto-compaction boundary percents, or null when unavailable (disabled, no window). */
|
|
2940
2995
|
#compactionBoundaries(contextWindow: number): CompactionBoundaries | null {
|
|
2941
|
-
const
|
|
2996
|
+
const session = this.session;
|
|
2997
|
+
const model = session.state?.model ?? session.model;
|
|
2998
|
+
const settingsRef = this.host.getSessionSettingsIdentity(session);
|
|
2999
|
+
const settingsRevision = this.host.getSessionSettingsRevision(session);
|
|
3000
|
+
const globalSettingsRevision = this.host.getSettingsRevision();
|
|
3001
|
+
const memo = this.#compactionBoundariesMemo;
|
|
3002
|
+
if (
|
|
3003
|
+
memo &&
|
|
3004
|
+
memo.sessionRef === session &&
|
|
3005
|
+
memo.contextWindow === contextWindow &&
|
|
3006
|
+
memo.modelRef === model &&
|
|
3007
|
+
memo.settingsRef === settingsRef &&
|
|
3008
|
+
memo.settingsRevision === settingsRevision &&
|
|
3009
|
+
memo.globalSettingsRevision === globalSettingsRevision
|
|
3010
|
+
) {
|
|
3011
|
+
return memo.boundaries;
|
|
3012
|
+
}
|
|
2942
3013
|
try {
|
|
2943
|
-
|
|
3014
|
+
const boundaries = this.host.computeCompactionBoundaries(session, contextWindow, model);
|
|
3015
|
+
this.#compactionBoundariesMemo = {
|
|
3016
|
+
sessionRef: session,
|
|
3017
|
+
contextWindow,
|
|
3018
|
+
modelRef: model,
|
|
3019
|
+
settingsRef,
|
|
3020
|
+
settingsRevision,
|
|
3021
|
+
globalSettingsRevision,
|
|
3022
|
+
boundaries,
|
|
3023
|
+
};
|
|
3024
|
+
return boundaries;
|
|
2944
3025
|
} catch {
|
|
2945
3026
|
return null;
|
|
2946
3027
|
}
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
getProjectDir,
|
|
10
10
|
normalizePathForComparison,
|
|
11
11
|
relativePathWithinNormalizedRoot,
|
|
12
|
-
relativePathWithinRoot,
|
|
13
12
|
} from "@oh-my-pi/pi-utils";
|
|
14
13
|
import { type SymbolKey, type Theme, type ThemeColor, theme } from "../theme";
|
|
15
14
|
import { shortenPath, TRUNCATE_LENGTHS, truncateToWidth } from "../render/render-utils";
|
|
@@ -70,14 +69,6 @@ function leadingGlyph(display: string): string {
|
|
|
70
69
|
return space === -1 ? display : display.slice(0, space);
|
|
71
70
|
}
|
|
72
71
|
|
|
73
|
-
function stripDisplayRoot(pwd: string): string {
|
|
74
|
-
for (const root of [path.join(os.homedir(), "Projects"), "/work"]) {
|
|
75
|
-
const relative = relativePathWithinRoot(root, pwd);
|
|
76
|
-
if (relative) return relative;
|
|
77
|
-
}
|
|
78
|
-
return pwd;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
72
|
/**
|
|
82
73
|
* Single-field usage counter (`token_in`, `token_out`, `cache_read`,
|
|
83
74
|
* `cache_write`). Hidden on zero, startup-placeholder aware, icon omitted when
|
|
@@ -121,28 +112,50 @@ const NORMALIZED_SCRATCH_ROOTS: readonly string[] = (() => {
|
|
|
121
112
|
return [...new Set(Array.from(roots, normalizePathForComparison))];
|
|
122
113
|
})();
|
|
123
114
|
|
|
124
|
-
interface
|
|
115
|
+
interface ProjectDirDisplay {
|
|
116
|
+
projectDir: string;
|
|
117
|
+
homeDir: string;
|
|
118
|
+
displayRoots: readonly string[] | undefined;
|
|
125
119
|
scratch: boolean;
|
|
126
|
-
|
|
120
|
+
displayPath: string;
|
|
127
121
|
}
|
|
128
122
|
|
|
129
|
-
|
|
123
|
+
let projectDirDisplay: ProjectDirDisplay | undefined;
|
|
130
124
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Retain only the active directory: switching cwd re-resolves its aliases,
|
|
127
|
+
* while repeated paints reuse both scratch classification and root stripping.
|
|
128
|
+
* Display roots remain normalized while the home directory is unchanged.
|
|
129
|
+
*/
|
|
130
|
+
function getProjectDirDisplay(projectDir: string): ProjectDirDisplay {
|
|
131
|
+
const cached = projectDirDisplay;
|
|
132
|
+
if (cached?.projectDir === projectDir) return cached;
|
|
134
133
|
|
|
134
|
+
const homeDir = os.homedir();
|
|
135
|
+
let displayRoots = cached?.homeDir === homeDir ? cached.displayRoots : undefined;
|
|
135
136
|
const normalizedProjectDir = normalizePathForComparison(projectDir);
|
|
136
|
-
let
|
|
137
|
+
let scratch = false;
|
|
138
|
+
let displayPath = projectDir;
|
|
137
139
|
for (const normalizedRoot of NORMALIZED_SCRATCH_ROOTS) {
|
|
138
140
|
const relative = relativePathWithinNormalizedRoot(normalizedRoot, normalizedProjectDir);
|
|
139
141
|
if (relative !== null) {
|
|
140
|
-
|
|
142
|
+
scratch = true;
|
|
143
|
+
displayPath = relative || projectDir;
|
|
141
144
|
break;
|
|
142
145
|
}
|
|
143
146
|
}
|
|
144
|
-
|
|
145
|
-
|
|
147
|
+
if (!scratch) {
|
|
148
|
+
displayRoots ??= [path.join(homeDir, "Projects"), "/work"].map(normalizePathForComparison);
|
|
149
|
+
for (const root of displayRoots) {
|
|
150
|
+
const relative = relativePathWithinNormalizedRoot(root, normalizedProjectDir);
|
|
151
|
+
if (relative) {
|
|
152
|
+
displayPath = relative;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
projectDirDisplay = { projectDir, homeDir, displayRoots, scratch, displayPath };
|
|
158
|
+
return projectDirDisplay;
|
|
146
159
|
}
|
|
147
160
|
|
|
148
161
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -421,16 +434,8 @@ const pathSegment: StatusLineSegment = {
|
|
|
421
434
|
}
|
|
422
435
|
|
|
423
436
|
const projectDir = ctx.activeRepo?.cwd ?? getProjectDir();
|
|
424
|
-
const { scratch,
|
|
425
|
-
let pwd = projectDir;
|
|
426
|
-
|
|
427
|
-
if (stripPrefix) {
|
|
428
|
-
if (scratch) {
|
|
429
|
-
if (relative) pwd = relative;
|
|
430
|
-
} else {
|
|
431
|
-
pwd = stripDisplayRoot(pwd);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
437
|
+
const { scratch, displayPath } = getProjectDirDisplay(projectDir);
|
|
438
|
+
let pwd = stripPrefix ? displayPath : projectDir;
|
|
434
439
|
const repoSuffix = ctx.activeRepo ? ` ↳ ${ctx.activeRepo.relativeRepoRoot}` : "";
|
|
435
440
|
if (opts.abbreviate !== false) {
|
|
436
441
|
pwd = shortenPath(pwd);
|