@oh-my-pi/pi-tui 18.4.1 → 18.4.3
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 +34 -0
- package/dist/types/chat/thinking-display.d.ts +2 -0
- package/dist/types/chrome/transcript-container.d.ts +15 -1
- package/dist/types/components/editor.d.ts +5 -0
- package/dist/types/loop-watchdog.d.ts +9 -0
- package/dist/types/overlays/model-browser.d.ts +15 -0
- package/dist/types/overlays/model-picker.d.ts +2 -1
- 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/setup/scenes/sign-in.d.ts +9 -9
- package/dist/types/setup/scenes/types.d.ts +0 -26
- package/dist/types/stdin-buffer.d.ts +10 -7
- package/dist/types/terminal.d.ts +16 -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/tools/web-search-types.d.ts +90 -0
- package/dist/types/tools/web-search.d.ts +1 -168
- package/dist/types/tui.d.ts +3 -2
- package/package.json +9 -9
- package/src/chat/assistant-message.ts +192 -40
- package/src/chat/thinking-display.ts +73 -11
- package/src/chat/tool-execution.ts +24 -4
- package/src/chrome/transcript-container.ts +178 -31
- package/src/components/editor.ts +51 -15
- package/src/components/image.ts +15 -7
- package/src/components/loader.ts +37 -17
- package/src/components/markdown.ts +110 -56
- package/src/loop-watchdog.ts +38 -10
- 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/model-picker.ts +9 -6
- package/src/overlays/rewind-selector.ts +10 -2
- package/src/overlays/settings-selector.ts +2 -10
- 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/setup/scenes/sign-in.ts +17 -12
- package/src/setup/scenes/types.ts +0 -27
- package/src/setup/wizard.ts +1 -1
- package/src/status-line/component.ts +229 -134
- package/src/status-line/segments.ts +34 -29
- package/src/stdin-buffer.ts +27 -21
- package/src/terminal.ts +60 -20
- package/src/theme/color.ts +15 -0
- package/src/theme/mermaid-cache.ts +10 -1
- 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/theme/tui-adapters.ts +14 -1
- package/src/tools/bash.ts +53 -13
- package/src/tools/edit.ts +110 -29
- package/src/tools/streaming-output.ts +79 -16
- package/src/tools/web-search-types.ts +99 -0
- package/src/tools/web-search.ts +1 -172
- package/src/tui.ts +143 -113
- package/src/utils.ts +15 -0
- package/dist/types/setup/scenes/providers.d.ts +0 -3
- package/dist/types/setup/scenes/web-search.d.ts +0 -22
- package/src/setup/scenes/providers.ts +0 -110
- package/src/setup/scenes/web-search.ts +0 -165
|
@@ -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,
|
|
@@ -48,6 +48,13 @@ import type {
|
|
|
48
48
|
StatusLineSettings,
|
|
49
49
|
} from "./types";
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Freshness window for the git segment's working-tree counts. A whole-worktree
|
|
53
|
+
* `git status` costs ~1 CPU-second on large repos and every open session polls
|
|
54
|
+
* it, so edits surface within this window; HEAD moves refetch immediately via
|
|
55
|
+
* {@link StatusLineComponent.invalidateGitCaches}.
|
|
56
|
+
*/
|
|
57
|
+
const GIT_STATUS_TTL_MS = 10_000;
|
|
51
58
|
const JJ_REFRESH_TTL_MS = 5000;
|
|
52
59
|
const JJ_COMMAND_TIMEOUT_MS = 5_000;
|
|
53
60
|
const WATCHER_FAILURE_POLL_TTL_MS = 5000;
|
|
@@ -255,57 +262,62 @@ interface ContextUsageMemo {
|
|
|
255
262
|
skillsRef: readonly any[] | undefined;
|
|
256
263
|
}
|
|
257
264
|
|
|
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
|
-
|
|
265
|
+
/**
|
|
266
|
+
* Snapshot of the cheap external inputs a cached bar depends on. Mutable and
|
|
267
|
+
* refilled in place: the per-frame cache probe writes into a recycled
|
|
268
|
+
* instance instead of allocating a fresh ~50-field object every frame.
|
|
269
|
+
*/
|
|
270
|
+
class StatusLineExternalInputs {
|
|
271
|
+
themeRef: unknown = undefined;
|
|
272
|
+
themeEpoch = 0;
|
|
273
|
+
projectDir = "";
|
|
274
|
+
sessionRef: StatusLineSession | undefined = undefined;
|
|
275
|
+
sessionFile: string | undefined = undefined;
|
|
276
|
+
sessionSettingsRef: unknown = undefined;
|
|
277
|
+
sessionSettingsRevision = 0;
|
|
278
|
+
globalSettingsRevision = 0;
|
|
279
|
+
stateRef: unknown = undefined;
|
|
280
|
+
stateMessagesRef: readonly AgentMessage[] = EMPTY_MESSAGES;
|
|
281
|
+
stateMessagesLength = 0;
|
|
282
|
+
stateLastMessageRef: AgentMessage | undefined = undefined;
|
|
283
|
+
stateLastMessageContentRef: unknown = undefined;
|
|
284
|
+
messagesRef: readonly AgentMessage[] = EMPTY_MESSAGES;
|
|
285
|
+
messagesLength = 0;
|
|
286
|
+
lastMessageRef: AgentMessage | undefined = undefined;
|
|
287
|
+
lastMessageRole: string | undefined = undefined;
|
|
288
|
+
lastMessageTimestamp = 0;
|
|
289
|
+
lastMessageContentSize = 0;
|
|
290
|
+
lastMessageBlockCount = 0;
|
|
291
|
+
lastMessageUsageTotal = 0;
|
|
292
|
+
lastMessageStopReason: string | undefined = undefined;
|
|
293
|
+
modelRef: unknown = undefined;
|
|
294
|
+
sessionModelRef: unknown = undefined;
|
|
295
|
+
sessionModelId: string | undefined = undefined;
|
|
296
|
+
sessionModelContextWindow: Model["contextWindow"] | undefined = undefined;
|
|
297
|
+
modelId: string | undefined = undefined;
|
|
298
|
+
modelName: string | undefined = undefined;
|
|
299
|
+
modelProvider: string | undefined = undefined;
|
|
300
|
+
modelContextWindow: Model["contextWindow"] | undefined = undefined;
|
|
301
|
+
modelThinking: Model["thinking"] = undefined;
|
|
302
|
+
thinkingLevel: unknown = undefined;
|
|
303
|
+
modelCostRef: unknown = undefined;
|
|
304
|
+
contextUsageRevision = 0;
|
|
305
|
+
systemPromptRef: unknown = undefined;
|
|
306
|
+
systemPromptLength = 0;
|
|
307
|
+
systemPromptContentSize = 0;
|
|
308
|
+
toolsRef: readonly unknown[] | undefined = undefined;
|
|
309
|
+
toolsLength = 0;
|
|
310
|
+
toolSchemaMetadataRevision = 0;
|
|
311
|
+
tokenizerRef: unknown = undefined;
|
|
312
|
+
skillsRef: unknown = undefined;
|
|
313
|
+
skillsLength = 0;
|
|
314
|
+
sessionName: string | undefined = undefined;
|
|
315
|
+
sessionId: string | undefined = undefined;
|
|
316
|
+
isStreaming: boolean | undefined = undefined;
|
|
317
|
+
isAutoThinking: boolean | undefined = undefined;
|
|
318
|
+
isFastModeActive = false;
|
|
319
|
+
anthropicSlowModeLabel: string | undefined = undefined;
|
|
320
|
+
compactionSpeculation: unknown = undefined;
|
|
309
321
|
}
|
|
310
322
|
|
|
311
323
|
interface CachedStatusLine {
|
|
@@ -445,6 +457,17 @@ function hasGitBackedSegment(segments: readonly StatusLineSegmentId[]): boolean
|
|
|
445
457
|
|
|
446
458
|
type StatusLineLayout = "box" | "band" | "plain-full" | "plain-left" | "plain-right";
|
|
447
459
|
|
|
460
|
+
/**
|
|
461
|
+
* Git views of immutable `VcsRepo` handles. `asGit()` mints a fresh napi
|
|
462
|
+
* wrapper around the same backend handle on every call, which the per-frame
|
|
463
|
+
* branch lookup would otherwise pay each render.
|
|
464
|
+
*/
|
|
465
|
+
const GIT_VIEW = Symbol("status-line.git-view");
|
|
466
|
+
|
|
467
|
+
interface GitViewTaggedRepo extends VcsRepo {
|
|
468
|
+
[GIT_VIEW]?: VcsGitRepo | null;
|
|
469
|
+
}
|
|
470
|
+
|
|
448
471
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
449
472
|
// StatusLineComponent
|
|
450
473
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -465,6 +488,22 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
465
488
|
};
|
|
466
489
|
#statusLineInputRevision = 0;
|
|
467
490
|
#statusLineClockTick = 0;
|
|
491
|
+
/** Reused probe buffer for {@link #readStatusLineExternalInputs}; handed to the new cache entry on a miss. */
|
|
492
|
+
#externalInputsProbe = new StatusLineExternalInputs();
|
|
493
|
+
/** `adjustHsv` result for the gauge's threshold tint; pure in its source hex. */
|
|
494
|
+
#dimmedAccentMemo: { sourceHex: string; hex: string } | undefined;
|
|
495
|
+
/** Gauge boundary markers; a pure function of the session's compaction settings, model, and window. */
|
|
496
|
+
#compactionBoundariesMemo:
|
|
497
|
+
| {
|
|
498
|
+
sessionRef: StatusLineSession;
|
|
499
|
+
contextWindow: number;
|
|
500
|
+
modelRef: unknown;
|
|
501
|
+
settingsRef: unknown;
|
|
502
|
+
settingsRevision: number;
|
|
503
|
+
globalSettingsRevision: number;
|
|
504
|
+
boundaries: CompactionBoundaries | null;
|
|
505
|
+
}
|
|
506
|
+
| undefined;
|
|
468
507
|
#settings: StatusLineSettings = {};
|
|
469
508
|
#effectiveSettings: EffectiveStatusLineSettings | undefined;
|
|
470
509
|
#cachedBranch: string | null | undefined = undefined;
|
|
@@ -554,6 +593,7 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
554
593
|
#cachedGitStatus: { staged: number; unstaged: number; untracked: number } | null = null;
|
|
555
594
|
#cachedGitStatusCwd: string | undefined = undefined;
|
|
556
595
|
#gitStatusLastFetch = 0;
|
|
596
|
+
#gitStatusGeneration = 0;
|
|
557
597
|
#gitStatusInFlightCwd: string | undefined = undefined;
|
|
558
598
|
#cachedJjBranch: string | null = null;
|
|
559
599
|
#jjBranchLastFetch = 0;
|
|
@@ -1070,8 +1110,7 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
1070
1110
|
const sessionName = sessionAccentEnabled ? this.session.sessionManager?.getSessionName() : undefined;
|
|
1071
1111
|
const idleHex = theme.getColorHex("dim");
|
|
1072
1112
|
const workingHex =
|
|
1073
|
-
(sessionName
|
|
1074
|
-
theme.getColorHex("accent");
|
|
1113
|
+
(sessionName && getSessionAccentHex(sessionName, theme.sessionAccentInputs)) || theme.getColorHex("accent");
|
|
1075
1114
|
const now = Date.now();
|
|
1076
1115
|
if (working !== this.#brandWorking) {
|
|
1077
1116
|
const previousTargetHex = this.#brandWorking ? workingHex : idleHex;
|
|
@@ -1223,6 +1262,11 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
1223
1262
|
this.#branchLastFetch = undefined;
|
|
1224
1263
|
this.#branchCacheGeneration++;
|
|
1225
1264
|
this.#cachedPrContext = undefined;
|
|
1265
|
+
// A HEAD move (commit, checkout, reset) changes the dirty counts; keep the
|
|
1266
|
+
// stale counts on screen but refetch on the next render. The generation
|
|
1267
|
+
// bump stops an in-flight pre-move result from re-stamping freshness.
|
|
1268
|
+
this.#gitStatusLastFetch = 0;
|
|
1269
|
+
this.#gitStatusGeneration++;
|
|
1226
1270
|
// jj label/status share the git segment's lifecycle: a HEAD move (e.g. a
|
|
1227
1271
|
// colocated `jj new`/bookmark move) must drop the throttled jj caches too,
|
|
1228
1272
|
// so the next render refetches.
|
|
@@ -1267,7 +1311,12 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
1267
1311
|
|
|
1268
1312
|
const gitCwd = activeRepoCache.effectiveGitCwd;
|
|
1269
1313
|
if (!repository) return null;
|
|
1270
|
-
const
|
|
1314
|
+
const taggedRepository: GitViewTaggedRepo = repository;
|
|
1315
|
+
let gitRepository = taggedRepository[GIT_VIEW];
|
|
1316
|
+
if (gitRepository === undefined) {
|
|
1317
|
+
gitRepository = repository.asGit();
|
|
1318
|
+
taggedRepository[GIT_VIEW] = gitRepository;
|
|
1319
|
+
}
|
|
1271
1320
|
if (!gitRepository) {
|
|
1272
1321
|
if (this.#jjBranchActive || Date.now() - this.#jjBranchLastFetch < JJ_REFRESH_TTL_MS) {
|
|
1273
1322
|
return this.#cachedJjBranch;
|
|
@@ -1457,11 +1506,12 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
1457
1506
|
if (this.#gitStatusInFlightCwd !== undefined) {
|
|
1458
1507
|
return this.#cachedGitStatusCwd === gitCwd ? this.#cachedGitStatus : null;
|
|
1459
1508
|
}
|
|
1460
|
-
if (this.#cachedGitStatusCwd === gitCwd && Date.now() - this.#gitStatusLastFetch <
|
|
1509
|
+
if (this.#cachedGitStatusCwd === gitCwd && Date.now() - this.#gitStatusLastFetch < GIT_STATUS_TTL_MS) {
|
|
1461
1510
|
return this.#cachedGitStatus;
|
|
1462
1511
|
}
|
|
1463
1512
|
|
|
1464
1513
|
this.#gitStatusInFlightCwd = gitCwd;
|
|
1514
|
+
const generation = this.#gitStatusGeneration;
|
|
1465
1515
|
|
|
1466
1516
|
(async () => {
|
|
1467
1517
|
let nextStatus: { staged: number; unstaged: number; untracked: number } | null = null;
|
|
@@ -1474,7 +1524,7 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
1474
1524
|
const prev = this.#cachedGitStatusCwd === gitCwd ? this.#cachedGitStatus : null;
|
|
1475
1525
|
this.#cachedGitStatus = nextStatus;
|
|
1476
1526
|
this.#cachedGitStatusCwd = gitCwd;
|
|
1477
|
-
this.#gitStatusLastFetch = Date.now();
|
|
1527
|
+
this.#gitStatusLastFetch = this.#gitStatusGeneration === generation ? Date.now() : 0;
|
|
1478
1528
|
this.#gitStatusInFlightCwd = undefined;
|
|
1479
1529
|
if (!this.#disposed && JSON.stringify(prev) !== JSON.stringify(nextStatus)) {
|
|
1480
1530
|
this.#invalidateStatusLineRenderCache();
|
|
@@ -2294,8 +2344,9 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
2294
2344
|
* going through this component's setters. This deliberately uses refs,
|
|
2295
2345
|
* scalars, and string lengths only: no schema estimation, serialization,
|
|
2296
2346
|
* usage aggregation, VCS probing, or segment getters run on a cache hit.
|
|
2347
|
+
* Fills `target` in place so the per-frame probe allocates nothing.
|
|
2297
2348
|
*/
|
|
2298
|
-
#readStatusLineExternalInputs(
|
|
2349
|
+
#readStatusLineExternalInputs(target: StatusLineExternalInputs): void {
|
|
2299
2350
|
const state = this.session.state;
|
|
2300
2351
|
const stateMessages = state.messages ?? EMPTY_MESSAGES;
|
|
2301
2352
|
const stateLastMessage = stateMessages[stateMessages.length - 1];
|
|
@@ -2346,62 +2397,61 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
2346
2397
|
if (typeof part === "string") systemPromptContentSize += part.length;
|
|
2347
2398
|
}
|
|
2348
2399
|
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
|
-
};
|
|
2400
|
+
const skills = this.session.skills;
|
|
2401
|
+
target.themeRef = theme;
|
|
2402
|
+
target.themeEpoch = getThemeEpoch();
|
|
2403
|
+
target.projectDir = getProjectDir();
|
|
2404
|
+
target.sessionRef = this.session;
|
|
2405
|
+
target.sessionFile = this.session.sessionFile;
|
|
2406
|
+
target.sessionSettingsRef = this.host.getSessionSettingsIdentity(this.session);
|
|
2407
|
+
target.sessionSettingsRevision = this.host.getSessionSettingsRevision(this.session);
|
|
2408
|
+
target.globalSettingsRevision = this.host.getSettingsRevision();
|
|
2409
|
+
target.stateRef = state;
|
|
2410
|
+
target.stateMessagesRef = stateMessages;
|
|
2411
|
+
target.stateMessagesLength = stateMessages.length;
|
|
2412
|
+
target.stateLastMessageRef = stateLastMessage;
|
|
2413
|
+
target.stateLastMessageContentRef = stateLastMessageContent;
|
|
2414
|
+
target.messagesRef = messages;
|
|
2415
|
+
target.messagesLength = messages.length;
|
|
2416
|
+
target.lastMessageRef = lastMessage;
|
|
2417
|
+
target.lastMessageRole = lastMessageView?.role;
|
|
2418
|
+
target.lastMessageTimestamp = lastMessageView?.timestamp ?? 0;
|
|
2419
|
+
target.lastMessageContentSize = lastMessageContentSize;
|
|
2420
|
+
target.lastMessageBlockCount = lastMessageBlockCount;
|
|
2421
|
+
target.lastMessageUsageTotal = lastMessageView?.usage?.totalTokens ?? 0;
|
|
2422
|
+
target.lastMessageStopReason = lastMessageView?.stopReason;
|
|
2423
|
+
target.modelRef = model;
|
|
2424
|
+
target.sessionModelRef = sessionModel;
|
|
2425
|
+
target.sessionModelId = sessionModel?.id;
|
|
2426
|
+
target.sessionModelContextWindow = sessionModel?.contextWindow;
|
|
2427
|
+
target.modelId = model?.id;
|
|
2428
|
+
target.modelName = model?.name;
|
|
2429
|
+
target.modelProvider = model?.provider;
|
|
2430
|
+
target.modelContextWindow = model?.contextWindow;
|
|
2431
|
+
target.modelThinking = model?.thinking;
|
|
2432
|
+
target.thinkingLevel = state.thinkingLevel;
|
|
2433
|
+
target.modelCostRef = model?.cost;
|
|
2434
|
+
target.contextUsageRevision = this.session.contextUsageRevision ?? 0;
|
|
2435
|
+
target.systemPromptRef = systemPrompt;
|
|
2436
|
+
target.systemPromptLength = systemPrompt?.length ?? 0;
|
|
2437
|
+
target.systemPromptContentSize = systemPromptContentSize;
|
|
2438
|
+
target.toolsRef = tools;
|
|
2439
|
+
target.toolsLength = tools?.length ?? 0;
|
|
2440
|
+
target.toolSchemaMetadataRevision = tools ? getToolSchemaMetadataRevision(tools) : 0;
|
|
2441
|
+
target.tokenizerRef = this.session.agent?.tokenizer;
|
|
2442
|
+
target.skillsRef = skills;
|
|
2443
|
+
target.skillsLength = skills?.length ?? 0;
|
|
2444
|
+
target.sessionName = this.session.sessionManager?.getSessionName?.();
|
|
2445
|
+
target.sessionId = this.session.sessionManager?.getSessionId?.();
|
|
2446
|
+
target.isStreaming = this.session.isStreaming;
|
|
2447
|
+
target.isAutoThinking = this.session.isAutoThinking;
|
|
2448
|
+
target.isFastModeActive =
|
|
2449
|
+
typeof this.session.isFastModeActive === "function" ? this.session.isFastModeActive() : false;
|
|
2450
|
+
target.anthropicSlowModeLabel =
|
|
2451
|
+
typeof this.session.getAnthropicSlowModeLabel === "function"
|
|
2452
|
+
? this.session.getAnthropicSlowModeLabel()
|
|
2453
|
+
: undefined;
|
|
2454
|
+
target.compactionSpeculation = this.session.compactionSpeculation;
|
|
2405
2455
|
}
|
|
2406
2456
|
|
|
2407
2457
|
#sameStatusLineExternalInputs(left: StatusLineExternalInputs, right: StatusLineExternalInputs): boolean {
|
|
@@ -2496,7 +2546,8 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
2496
2546
|
|
|
2497
2547
|
#buildStatusLine(width: number, layout: StatusLineLayout = "box", previewTitle?: string): CachedStatusLine {
|
|
2498
2548
|
const effectiveSettings = this.#resolveSettings();
|
|
2499
|
-
const externalInputs = this.#
|
|
2549
|
+
const externalInputs = this.#externalInputsProbe;
|
|
2550
|
+
this.#readStatusLineExternalInputs(externalInputs);
|
|
2500
2551
|
const nowMs = Date.now();
|
|
2501
2552
|
const clockTick = this.#statusLineClock(nowMs, effectiveSettings);
|
|
2502
2553
|
if (clockTick !== this.#statusLineClockTick) {
|
|
@@ -2516,6 +2567,8 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
2516
2567
|
return cached;
|
|
2517
2568
|
}
|
|
2518
2569
|
|
|
2570
|
+
// The probe now belongs to the new cache entry; later probes need their own buffer.
|
|
2571
|
+
this.#externalInputsProbe = new StatusLineExternalInputs();
|
|
2519
2572
|
const content = this.#renderStatusLine(width, layout, previewTitle, nowMs);
|
|
2520
2573
|
const result = {
|
|
2521
2574
|
content,
|
|
@@ -2642,8 +2695,11 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
2642
2695
|
if (subagentBadge) rightParts.unshift(subagentBadge);
|
|
2643
2696
|
}
|
|
2644
2697
|
const topFillWidth = Math.max(0, width);
|
|
2645
|
-
|
|
2646
|
-
const
|
|
2698
|
+
// These arrays are local to this render; overflow handling can mutate them.
|
|
2699
|
+
const left = leftParts;
|
|
2700
|
+
const right = rightParts;
|
|
2701
|
+
const leftWidths = left.map(part => visibleWidth(part));
|
|
2702
|
+
const rightWidths = right.map(part => visibleWidth(part));
|
|
2647
2703
|
|
|
2648
2704
|
const leftSepWidth = visibleWidth(separatorDef.left);
|
|
2649
2705
|
const rightSepWidth = visibleWidth(separatorDef.right);
|
|
@@ -2657,15 +2713,15 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
2657
2713
|
const bandCap = layout === "band" && separatorDef.endCaps && !transparentBg ? theme.sep.powerlineCapLeft : "";
|
|
2658
2714
|
const bandCapWidth = visibleWidth(bandCap);
|
|
2659
2715
|
|
|
2660
|
-
const groupWidth = (
|
|
2661
|
-
if (
|
|
2662
|
-
const partsWidth =
|
|
2663
|
-
const sepTotal =
|
|
2716
|
+
const groupWidth = (widths: number[], capWidth: number, sepWidth: number): number => {
|
|
2717
|
+
if (widths.length === 0) return 0;
|
|
2718
|
+
const partsWidth = widths.reduce((sum, partWidth) => sum + partWidth, 0);
|
|
2719
|
+
const sepTotal = (widths.length - 1) * (sepWidth + 2);
|
|
2664
2720
|
return partsWidth + sepTotal + 2 + capWidth;
|
|
2665
2721
|
};
|
|
2666
2722
|
|
|
2667
|
-
let leftWidth = groupWidth(
|
|
2668
|
-
let rightWidth = groupWidth(
|
|
2723
|
+
let leftWidth = groupWidth(leftWidths, leftCapWidth + bandCapWidth, leftSepWidth);
|
|
2724
|
+
let rightWidth = groupWidth(rightWidths, rightCapWidth, rightSepWidth);
|
|
2669
2725
|
// Embedded mode removes the standalone context segment before overflow
|
|
2670
2726
|
// handling, so the gauge must reserve enough room for both labels. Without
|
|
2671
2727
|
// this budget a long path/session title can leave a one-cell gap: the
|
|
@@ -2694,23 +2750,27 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
2694
2750
|
if (nameSegIdx >= 0 && totalWidth() > topFillWidth) {
|
|
2695
2751
|
// Badge/job parts were unshifted ahead of the tracked segment ids.
|
|
2696
2752
|
const nameIdx = nameSegIdx + (right.length - rightSegIds.length);
|
|
2697
|
-
const currentNameVW =
|
|
2753
|
+
const currentNameVW = rightWidths[nameIdx];
|
|
2698
2754
|
const minNameVW = 8;
|
|
2699
2755
|
const shrinkBy = Math.min(Math.max(0, currentNameVW - minNameVW), totalWidth() - topFillWidth);
|
|
2700
2756
|
if (shrinkBy > 0) {
|
|
2701
2757
|
right[nameIdx] = truncateToWidth(right[nameIdx], currentNameVW - shrinkBy);
|
|
2702
|
-
|
|
2758
|
+
const nameWidth = visibleWidth(right[nameIdx]);
|
|
2759
|
+
rightWidth += nameWidth - currentNameVW;
|
|
2760
|
+
rightWidths[nameIdx] = nameWidth;
|
|
2703
2761
|
}
|
|
2704
2762
|
}
|
|
2705
2763
|
while (totalWidth() > topFillWidth && right.length > 0) {
|
|
2764
|
+
const removedWidth = rightWidths[right.length - 1];
|
|
2706
2765
|
right.pop();
|
|
2707
|
-
|
|
2766
|
+
rightWidths.pop();
|
|
2767
|
+
rightWidth = right.length > 0 ? rightWidth - removedWidth - rightSepWidth - 2 : 0;
|
|
2708
2768
|
}
|
|
2709
2769
|
// Shrink path before dropping left segments — path is the only elastic segment
|
|
2710
2770
|
const pathIdx = leftSegIds.indexOf("path");
|
|
2711
2771
|
if (pathIdx >= 0 && totalWidth() > topFillWidth) {
|
|
2712
2772
|
const overflow = totalWidth() - topFillWidth;
|
|
2713
|
-
const currentPathVW =
|
|
2773
|
+
const currentPathVW = leftWidths[pathIdx];
|
|
2714
2774
|
const minPathVW = 8; // icon + ellipsis + a few chars
|
|
2715
2775
|
const shrinkable = currentPathVW - minPathVW;
|
|
2716
2776
|
if (shrinkable > 0) {
|
|
@@ -2723,9 +2783,10 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
2723
2783
|
});
|
|
2724
2784
|
let reRendered = renderSegment("path", pathCtx(newMaxLen));
|
|
2725
2785
|
if (reRendered.visible && reRendered.content) {
|
|
2786
|
+
let pathWidth = visibleWidth(reRendered.content);
|
|
2726
2787
|
// maxLength governs path text, not icon prefix; iterate to compensate
|
|
2727
2788
|
for (let i = 0; i < 8; i++) {
|
|
2728
|
-
const saved = currentPathVW -
|
|
2789
|
+
const saved = currentPathVW - pathWidth;
|
|
2729
2790
|
if (saved >= shrinkBy) break;
|
|
2730
2791
|
const nextMaxLen = Math.max(4, newMaxLen - (shrinkBy - saved));
|
|
2731
2792
|
if (nextMaxLen >= newMaxLen) break; // no progress or hit floor
|
|
@@ -2733,9 +2794,11 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
2733
2794
|
const adjusted = renderSegment("path", pathCtx(newMaxLen));
|
|
2734
2795
|
if (!adjusted.visible || !adjusted.content) break;
|
|
2735
2796
|
reRendered = adjusted;
|
|
2797
|
+
pathWidth = visibleWidth(adjusted.content);
|
|
2736
2798
|
}
|
|
2737
2799
|
left[pathIdx] = reRendered.content;
|
|
2738
|
-
leftWidth
|
|
2800
|
+
leftWidth += pathWidth - currentPathVW;
|
|
2801
|
+
leftWidths[pathIdx] = pathWidth;
|
|
2739
2802
|
}
|
|
2740
2803
|
}
|
|
2741
2804
|
}
|
|
@@ -2752,9 +2815,11 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
2752
2815
|
|
|
2753
2816
|
while (totalWidth() > topFillWidth && left.length > 0) {
|
|
2754
2817
|
const dropIdx = leftOverflowDropIndex();
|
|
2818
|
+
const removedWidth = leftWidths[dropIdx];
|
|
2755
2819
|
left.splice(dropIdx, 1);
|
|
2820
|
+
leftWidths.splice(dropIdx, 1);
|
|
2756
2821
|
leftSegIds.splice(dropIdx, 1);
|
|
2757
|
-
leftWidth =
|
|
2822
|
+
leftWidth = left.length > 0 ? leftWidth - removedWidth - leftSepWidth - 2 : 0;
|
|
2758
2823
|
}
|
|
2759
2824
|
}
|
|
2760
2825
|
|
|
@@ -2906,8 +2971,12 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
2906
2971
|
const speculationColor = theme.getFgAnsi("muted");
|
|
2907
2972
|
const overflowColor = theme.getFgAnsi("error");
|
|
2908
2973
|
const rawAccentHex = accentHex ?? theme.getColorHex("borderAccent");
|
|
2909
|
-
|
|
2910
|
-
|
|
2974
|
+
let dimmedAccent = this.#dimmedAccentMemo;
|
|
2975
|
+
if (dimmedAccent?.sourceHex !== rawAccentHex) {
|
|
2976
|
+
dimmedAccent = { sourceHex: rawAccentHex, hex: adjustHsv(rawAccentHex, { s: 0.7, v: 0.75 }) };
|
|
2977
|
+
this.#dimmedAccentMemo = dimmedAccent;
|
|
2978
|
+
}
|
|
2979
|
+
const thresholdColor = getSessionAccentAnsi(dimmedAccent.hex) ?? usedColor;
|
|
2911
2980
|
|
|
2912
2981
|
let out = "\x1b[49m";
|
|
2913
2982
|
let activeColor = "";
|
|
@@ -2938,9 +3007,35 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
2938
3007
|
|
|
2939
3008
|
/** Auto-compaction boundary percents, or null when unavailable (disabled, no window). */
|
|
2940
3009
|
#compactionBoundaries(contextWindow: number): CompactionBoundaries | null {
|
|
2941
|
-
const
|
|
3010
|
+
const session = this.session;
|
|
3011
|
+
const model = session.state?.model ?? session.model;
|
|
3012
|
+
const settingsRef = this.host.getSessionSettingsIdentity(session);
|
|
3013
|
+
const settingsRevision = this.host.getSessionSettingsRevision(session);
|
|
3014
|
+
const globalSettingsRevision = this.host.getSettingsRevision();
|
|
3015
|
+
const memo = this.#compactionBoundariesMemo;
|
|
3016
|
+
if (
|
|
3017
|
+
memo &&
|
|
3018
|
+
memo.sessionRef === session &&
|
|
3019
|
+
memo.contextWindow === contextWindow &&
|
|
3020
|
+
memo.modelRef === model &&
|
|
3021
|
+
memo.settingsRef === settingsRef &&
|
|
3022
|
+
memo.settingsRevision === settingsRevision &&
|
|
3023
|
+
memo.globalSettingsRevision === globalSettingsRevision
|
|
3024
|
+
) {
|
|
3025
|
+
return memo.boundaries;
|
|
3026
|
+
}
|
|
2942
3027
|
try {
|
|
2943
|
-
|
|
3028
|
+
const boundaries = this.host.computeCompactionBoundaries(session, contextWindow, model);
|
|
3029
|
+
this.#compactionBoundariesMemo = {
|
|
3030
|
+
sessionRef: session,
|
|
3031
|
+
contextWindow,
|
|
3032
|
+
modelRef: model,
|
|
3033
|
+
settingsRef,
|
|
3034
|
+
settingsRevision,
|
|
3035
|
+
globalSettingsRevision,
|
|
3036
|
+
boundaries,
|
|
3037
|
+
};
|
|
3038
|
+
return boundaries;
|
|
2944
3039
|
} catch {
|
|
2945
3040
|
return null;
|
|
2946
3041
|
}
|
|
@@ -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);
|