@oh-my-pi/pi-tui 18.8.7 → 18.8.8
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 +28 -64
- package/dist/types/chat/execution-shared.d.ts +6 -5
- package/dist/types/keybinding-matchers.d.ts +3 -0
- package/dist/types/overlays/annotation-overlay.d.ts +12 -0
- package/dist/types/overlays/annotation-types.d.ts +2 -0
- package/dist/types/overlays/hook-selector.d.ts +9 -4
- package/dist/types/overlays/model-browser.d.ts +2 -2
- package/dist/types/overlays/model-hub.d.ts +21 -4
- package/dist/types/overlays/session-observer-registry.d.ts +2 -0
- package/dist/types/overlays/usage-dashboard.d.ts +3 -1
- package/dist/types/status-line/types.d.ts +1 -1
- package/dist/types/theme/symbols.d.ts +1 -1
- package/dist/types/tools/eval.d.ts +1 -1
- package/dist/types/tools/native-view.d.ts +47 -0
- package/dist/types/tools/renderer.d.ts +15 -2
- package/dist/types/tui.d.ts +20 -0
- package/dist/types/vim.d.ts +19 -5
- package/package.json +9 -9
- package/src/chat/execution-shared.ts +42 -25
- package/src/chat/tool-execution.ts +9 -2
- package/src/components/editor.ts +182 -5
- package/src/keybinding-matchers.ts +6 -2
- package/src/overlays/agent-hub.ts +7 -3
- package/src/overlays/annotation-overlay.ts +87 -6
- package/src/overlays/annotation-types.ts +2 -0
- package/src/overlays/hook-selector.ts +12 -5
- package/src/overlays/model-browser.ts +3 -3
- package/src/overlays/model-hub.ts +107 -27
- package/src/overlays/session-observer-registry.ts +10 -2
- package/src/overlays/usage-dashboard.ts +70 -23
- package/src/prompt/composer.ts +1 -0
- package/src/status-line/segments.ts +3 -0
- package/src/status-line/types.ts +1 -1
- package/src/theme/symbols.ts +5 -0
- package/src/tools/bash.ts +82 -31
- package/src/tools/eval.ts +149 -123
- package/src/tools/native-view.ts +102 -1
- package/src/tools/renderer.ts +15 -2
- package/src/tui.ts +68 -0
- package/src/vim.ts +333 -18
|
@@ -126,8 +126,8 @@ export interface ResolvedModelRoleValue {
|
|
|
126
126
|
export interface ModelCompactionPoint {
|
|
127
127
|
/** Context tokens that trigger auto-compaction; undefined when auto-compaction is off or the window is unknown. */
|
|
128
128
|
tokens: number | undefined;
|
|
129
|
-
/**
|
|
130
|
-
|
|
129
|
+
/** Why it triggers there, in a few words: `fixed`, `85% of 400K base`, `80% of window`. */
|
|
130
|
+
basis?: string;
|
|
131
131
|
/** What sets it: the matching `compaction.modelThresholds` key, `global`, or `default`. */
|
|
132
132
|
source: string;
|
|
133
133
|
/** The model's own exact entry as editable text (`90k`, `80%`); absent when it has none. */
|
|
@@ -2174,7 +2174,7 @@ export class ModelBrowser implements Component {
|
|
|
2174
2174
|
const value =
|
|
2175
2175
|
compaction.tokens === undefined
|
|
2176
2176
|
? "off"
|
|
2177
|
-
: `${compaction.tokens.toLocaleString("en-US")}${compaction.
|
|
2177
|
+
: `${compaction.tokens.toLocaleString("en-US")}${compaction.basis !== undefined ? ` · ${compaction.basis}` : ""}`;
|
|
2178
2178
|
facts.push({
|
|
2179
2179
|
k: [span("Compacts at", "muted")],
|
|
2180
2180
|
v: [span(value, "mono"), span(` · ${compaction.source}`, "dim")],
|
|
@@ -48,7 +48,7 @@ import type {
|
|
|
48
48
|
ResolvedModelRoleValue,
|
|
49
49
|
} from "./model-browser";
|
|
50
50
|
import { AUTO_THINKING, type ConfiguredThinkingLevel, getConfiguredThinkingLevelMetadata } from "../thinking";
|
|
51
|
-
import { thinkingLevelGlyph } from "../render/render-utils";
|
|
51
|
+
import { sanitizeDisplayWarning, thinkingLevelGlyph } from "../render/render-utils";
|
|
52
52
|
import { theme } from "../theme/theme";
|
|
53
53
|
import { matchesSelectCancel, matchesSelectDown, matchesSelectUp } from "../keybinding-matchers";
|
|
54
54
|
import {
|
|
@@ -171,14 +171,30 @@ export interface ModelHubCallbacks {
|
|
|
171
171
|
/** Persist a new quick-switch cycle order (the ctrl+p role cycle). */
|
|
172
172
|
onCycleOrderChange?: (order: string[]) => void;
|
|
173
173
|
/**
|
|
174
|
-
* Persist typed text as `model`'s own compaction
|
|
175
|
-
* `
|
|
176
|
-
*
|
|
174
|
+
* Persist typed text as `model`'s own compaction limit: a token base the
|
|
175
|
+
* policy scales (`400k`), a fixed trigger (`f400k`), a percentage (`80%`), or
|
|
176
|
+
* empty to reset. Rejected input returns `{ kind: "error" }`; input that
|
|
177
|
+
* needs acknowledgement (one that opens the extended window) returns
|
|
178
|
+
* `{ kind: "confirm" }` until called again with the same text and
|
|
179
|
+
* `confirmed`. Either keeps the field open.
|
|
177
180
|
*/
|
|
178
|
-
onCompactionPointChange?: (
|
|
181
|
+
onCompactionPointChange?: (
|
|
182
|
+
model: Model,
|
|
183
|
+
input: string,
|
|
184
|
+
confirmed: boolean,
|
|
185
|
+
) => CompactionPointChangeResult | undefined;
|
|
186
|
+
/**
|
|
187
|
+
* One short line on where `model` would compact with the typed limit
|
|
188
|
+
* (`compacts at 340K · 85% of 400K base`), shown beside the field while
|
|
189
|
+
* typing; undefined for input that does not parse or fit.
|
|
190
|
+
*/
|
|
191
|
+
previewCompactionPoint?: (model: Model, input: string) => string | undefined;
|
|
179
192
|
onCancel: () => void;
|
|
180
193
|
}
|
|
181
194
|
|
|
195
|
+
/** Why {@link ModelHubCallbacks.onCompactionPointChange} kept the field open. */
|
|
196
|
+
export type CompactionPointChangeResult = { kind: "error"; message: string } | { kind: "confirm"; message: string };
|
|
197
|
+
|
|
182
198
|
export interface ModelHubOptions {
|
|
183
199
|
/** Preselect this provider's sidebar entry (e.g. when reopening after /login). */
|
|
184
200
|
initialProviderId?: string;
|
|
@@ -232,12 +248,18 @@ type StripState =
|
|
|
232
248
|
input: Input;
|
|
233
249
|
}
|
|
234
250
|
| {
|
|
235
|
-
/**
|
|
251
|
+
/**
|
|
252
|
+
* Footer text input setting `model`'s compaction limit; `error` is the last
|
|
253
|
+
* rejection, `confirm` the warning a second Enter on the same `value`
|
|
254
|
+
* accepts, `preview` the host's live line for the typed `value`.
|
|
255
|
+
*/
|
|
236
256
|
kind: "name";
|
|
237
257
|
purpose: "compaction";
|
|
238
258
|
model: Model;
|
|
239
259
|
input: Input;
|
|
240
260
|
error?: string;
|
|
261
|
+
confirm?: { value: string; message: string };
|
|
262
|
+
preview?: { value: string; text: string | undefined };
|
|
241
263
|
};
|
|
242
264
|
|
|
243
265
|
/** A Roles-view command; keys and the picker's action bar both run {@link ModelHubComponent}'s `#runRolesAction`. */
|
|
@@ -302,8 +324,41 @@ function providerInitials(providerId: string): string {
|
|
|
302
324
|
|
|
303
325
|
const PROVIDER_REFRESH_DEBOUNCE_MS = 120;
|
|
304
326
|
const RECENT_LIMIT = 15;
|
|
305
|
-
/** Accepted compaction
|
|
306
|
-
const COMPACTION_INPUT_HINT = "
|
|
327
|
+
/** Accepted compaction limit input, shown beside the field: a scaled base, a fixed trigger, or a percentage. */
|
|
328
|
+
const COMPACTION_INPUT_HINT = "400k base · f400k fixed · 80% · empty resets";
|
|
329
|
+
|
|
330
|
+
type CompactionStrip = Extract<StripState, { purpose: "compaction" }>;
|
|
331
|
+
|
|
332
|
+
/** Whether the compaction field still holds the text its pending warning was raised for. */
|
|
333
|
+
function compactionConfirmPending(strip: CompactionStrip): boolean {
|
|
334
|
+
return strip.confirm !== undefined && strip.confirm.value === strip.input.getValue();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* The note beside the compaction field: the last error, a pending warning, the
|
|
339
|
+
* host's live preview of the typed limit, or (empty or unparsed input) the
|
|
340
|
+
* input syntax. Host text embeds model ids and config text, so it is
|
|
341
|
+
* sanitized for every render path (terminal footer, Tern picker and strip).
|
|
342
|
+
*/
|
|
343
|
+
function compactionNotice(
|
|
344
|
+
strip: CompactionStrip,
|
|
345
|
+
preview: ((model: Model, input: string) => string | undefined) | undefined,
|
|
346
|
+
): { text: string; style: "error" | "warning" | "dim" } {
|
|
347
|
+
if (strip.error) return { text: sanitizeDisplayWarning(strip.error), style: "error" };
|
|
348
|
+
// The footer hint and the primary action already say Enter accepts; the
|
|
349
|
+
// notice stays short so it fits beside the field.
|
|
350
|
+
if (strip.confirm && compactionConfirmPending(strip)) {
|
|
351
|
+
return { text: sanitizeDisplayWarning(strip.confirm.message), style: "warning" };
|
|
352
|
+
}
|
|
353
|
+
const value = strip.input.getValue();
|
|
354
|
+
if (preview && value.trim().length > 0) {
|
|
355
|
+
// Renders repeat per frame; ask the host once per typed value.
|
|
356
|
+
if (strip.preview?.value !== value) strip.preview = { value, text: preview(strip.model, value) };
|
|
357
|
+
if (strip.preview.text) return { text: sanitizeDisplayWarning(strip.preview.text), style: "dim" };
|
|
358
|
+
}
|
|
359
|
+
return { text: COMPACTION_INPUT_HINT, style: "dim" };
|
|
360
|
+
}
|
|
361
|
+
|
|
307
362
|
const MODEL_KIND_TABS: ReadonlyArray<"all" | ModelKind> = ["all", ...MODEL_KINDS];
|
|
308
363
|
const ROLE_TABS = ["all", "chat", "kind"] as const;
|
|
309
364
|
type RoleTab = (typeof ROLE_TABS)[number];
|
|
@@ -1800,9 +1855,16 @@ export class ModelHubComponent implements Component {
|
|
|
1800
1855
|
const strip = this.#strip;
|
|
1801
1856
|
if (strip?.kind !== "name") return;
|
|
1802
1857
|
if (strip.purpose === "compaction") {
|
|
1803
|
-
const
|
|
1804
|
-
|
|
1805
|
-
|
|
1858
|
+
const value = strip.input.getValue();
|
|
1859
|
+
const result = this.#callbacks.onCompactionPointChange?.(strip.model, value, compactionConfirmPending(strip));
|
|
1860
|
+
if (result?.kind === "error") {
|
|
1861
|
+
strip.error = result.message;
|
|
1862
|
+
strip.confirm = undefined;
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
if (result?.kind === "confirm") {
|
|
1866
|
+
strip.error = undefined;
|
|
1867
|
+
strip.confirm = { value, message: result.message };
|
|
1806
1868
|
return;
|
|
1807
1869
|
}
|
|
1808
1870
|
this.#strip = null;
|
|
@@ -2721,7 +2783,9 @@ export class ModelHubComponent implements Component {
|
|
|
2721
2783
|
const strip = this.#strip;
|
|
2722
2784
|
if (strip) {
|
|
2723
2785
|
if (strip.kind === "name") {
|
|
2724
|
-
if (strip.purpose === "compaction")
|
|
2786
|
+
if (strip.purpose === "compaction") {
|
|
2787
|
+
return `${enter} ${compactionConfirmPending(strip) ? "accept" : "set"} compaction point · ${cancel} cancel`;
|
|
2788
|
+
}
|
|
2725
2789
|
if (strip.purpose === "preset") {
|
|
2726
2790
|
return `${enter} save preset · ${cancel} cancel`;
|
|
2727
2791
|
}
|
|
@@ -2808,19 +2872,23 @@ export class ModelHubComponent implements Component {
|
|
|
2808
2872
|
if (strip.kind === "name") {
|
|
2809
2873
|
const labelText =
|
|
2810
2874
|
strip.purpose === "compaction"
|
|
2811
|
-
?
|
|
2875
|
+
? `${strip.model.id} limit:`
|
|
2812
2876
|
: strip.purpose === "preset"
|
|
2813
2877
|
? "Preset name:"
|
|
2814
2878
|
: "New role name:";
|
|
2815
2879
|
const label = theme.fg("accent", labelText);
|
|
2816
|
-
|
|
2880
|
+
// A compaction point is at most a few characters (`1500k`, `12.5%`); a
|
|
2881
|
+
// narrow field leaves the row for its notice.
|
|
2882
|
+
const maxInputWidth = strip.purpose === "compaction" ? 10 : 32;
|
|
2883
|
+
const inputWidth = Math.max(8, Math.min(maxInputWidth, width - visibleWidth(labelText) - 24));
|
|
2817
2884
|
const inputLine = strip.input.render(inputWidth)[0] ?? "";
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2885
|
+
let hint: string;
|
|
2886
|
+
if (strip.purpose === "compaction") {
|
|
2887
|
+
const notice = compactionNotice(strip, this.#callbacks.previewCompactionPoint);
|
|
2888
|
+
hint = theme.fg(notice.style, notice.text);
|
|
2889
|
+
} else {
|
|
2890
|
+
hint = theme.fg("dim", "(letters, digits, - and _)");
|
|
2891
|
+
}
|
|
2824
2892
|
return truncateToWidth(`${label} ${inputLine} ${hint}`, width);
|
|
2825
2893
|
}
|
|
2826
2894
|
|
|
@@ -3475,7 +3543,12 @@ export class ModelHubComponent implements Component {
|
|
|
3475
3543
|
const apply =
|
|
3476
3544
|
strip.kind === "name"
|
|
3477
3545
|
? strip.purpose === "compaction"
|
|
3478
|
-
? pickerAction(
|
|
3546
|
+
? pickerAction(
|
|
3547
|
+
"compactionPoint",
|
|
3548
|
+
compactionConfirmPending(strip) ? "Accept compaction point" : "Set compaction point",
|
|
3549
|
+
"enter",
|
|
3550
|
+
{ primary: true },
|
|
3551
|
+
)
|
|
3479
3552
|
: pickerAction(
|
|
3480
3553
|
strip.purpose === "preset" ? "presetName" : "roleName",
|
|
3481
3554
|
strip.purpose === "preset" ? "Save preset" : "Create role",
|
|
@@ -3579,14 +3652,14 @@ export class ModelHubComponent implements Component {
|
|
|
3579
3652
|
#pickerStrip(strip: StripState): NonNullable<TspPickerProps["strip"]> {
|
|
3580
3653
|
if (strip.kind === "name") {
|
|
3581
3654
|
if (strip.purpose === "compaction") {
|
|
3655
|
+
const notice = compactionNotice(strip, this.#callbacks.previewCompactionPoint);
|
|
3582
3656
|
return {
|
|
3583
3657
|
label: [
|
|
3584
|
-
span("Compact ", "muted"),
|
|
3585
3658
|
span(strip.model.id, "mono"),
|
|
3586
|
-
span("
|
|
3659
|
+
span(" limit ", "muted"),
|
|
3587
3660
|
span(strip.input.getValue(), "mono"),
|
|
3588
3661
|
span("▏", "accent"),
|
|
3589
|
-
span(` ${
|
|
3662
|
+
span(` ${notice.text}`, notice.style),
|
|
3590
3663
|
],
|
|
3591
3664
|
items: [],
|
|
3592
3665
|
};
|
|
@@ -3965,13 +4038,14 @@ export class ModelHubComponent implements Component {
|
|
|
3965
4038
|
if (!strip) return undefined;
|
|
3966
4039
|
if (strip.kind === "name") {
|
|
3967
4040
|
if (strip.purpose === "compaction") {
|
|
4041
|
+
const notice = compactionNotice(strip, this.#callbacks.previewCompactionPoint);
|
|
3968
4042
|
return node(
|
|
3969
4043
|
"row",
|
|
3970
4044
|
{ gap: "sm", align: "center" },
|
|
3971
4045
|
[
|
|
3972
|
-
text([span(
|
|
4046
|
+
text([span(`${strip.model.id} limit:`, "accent")]),
|
|
3973
4047
|
col([strip.input], { grow: 1 }),
|
|
3974
|
-
text([span(
|
|
4048
|
+
text([span(notice.text, notice.style)], { wrap: "word" }),
|
|
3975
4049
|
],
|
|
3976
4050
|
"compactionPoint",
|
|
3977
4051
|
);
|
|
@@ -4030,7 +4104,13 @@ export class ModelHubComponent implements Component {
|
|
|
4030
4104
|
switch (strip.kind) {
|
|
4031
4105
|
case "name":
|
|
4032
4106
|
return strip.purpose === "compaction"
|
|
4033
|
-
? [
|
|
4107
|
+
? [
|
|
4108
|
+
keys(
|
|
4109
|
+
compactionConfirmPending(strip) ? "accept compaction point" : "set compaction point",
|
|
4110
|
+
"enter",
|
|
4111
|
+
),
|
|
4112
|
+
cancel("cancel"),
|
|
4113
|
+
]
|
|
4034
4114
|
: strip.purpose === "preset"
|
|
4035
4115
|
? [keys("save preset", "enter"), cancel("cancel")]
|
|
4036
4116
|
: [keys("create + pick model", "enter"), cancel("cancel")];
|
|
@@ -64,6 +64,8 @@ export interface ObservableSession {
|
|
|
64
64
|
lastUpdate: number;
|
|
65
65
|
/** Latest progress snapshot from the subagent executor */
|
|
66
66
|
progress?: AgentProgress;
|
|
67
|
+
/** `Date.now()` when {@link progress} arrived; its `durationMs` was current then. */
|
|
68
|
+
progressAt?: number;
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
/** Coarse source of an observer change; callers use it to separate lifecycle work from high-frequency progress. */
|
|
@@ -225,6 +227,9 @@ export class SessionObserverRegistry {
|
|
|
225
227
|
existing.detached = payload.detached ?? existing.detached;
|
|
226
228
|
if (payload.description) existing.description = payload.description;
|
|
227
229
|
if (payload.sessionFile) existing.sessionFile = payload.sessionFile;
|
|
230
|
+
// A kept-alive or parked agent's next turn restarts its clock; the prior turn's
|
|
231
|
+
// snapshot must not be aged across the idle gap before the new turn's first frame.
|
|
232
|
+
if (payload.status === "started") existing.progressAt = undefined;
|
|
228
233
|
} else {
|
|
229
234
|
this.#sessions.set(payload.id, {
|
|
230
235
|
id: payload.id,
|
|
@@ -256,12 +261,14 @@ export class SessionObserverRegistry {
|
|
|
256
261
|
|
|
257
262
|
const sortOrder = this.#ensureSortOrder(id);
|
|
258
263
|
this.#ensureParentSortOrder(payload.parentToolCallId, sortOrder);
|
|
264
|
+
const now = Date.now();
|
|
259
265
|
if (existing) {
|
|
260
|
-
existing.lastUpdate =
|
|
266
|
+
existing.lastUpdate = now;
|
|
261
267
|
existing.index = payload.index;
|
|
262
268
|
existing.parentToolCallId = payload.parentToolCallId ?? existing.parentToolCallId;
|
|
263
269
|
existing.detached = payload.detached ?? existing.detached;
|
|
264
270
|
existing.progress = progress;
|
|
271
|
+
existing.progressAt = now;
|
|
265
272
|
if (progress.description) existing.description = progress.description;
|
|
266
273
|
if (payload.sessionFile) existing.sessionFile = payload.sessionFile;
|
|
267
274
|
} else {
|
|
@@ -276,8 +283,9 @@ export class SessionObserverRegistry {
|
|
|
276
283
|
parentToolCallId: payload.parentToolCallId,
|
|
277
284
|
detached: payload.detached,
|
|
278
285
|
index: payload.index,
|
|
279
|
-
lastUpdate:
|
|
286
|
+
lastUpdate: now,
|
|
280
287
|
progress,
|
|
288
|
+
progressAt: now,
|
|
281
289
|
});
|
|
282
290
|
}
|
|
283
291
|
this.#notifyListeners("progress");
|
|
@@ -86,8 +86,10 @@ export interface ProviderCard {
|
|
|
86
86
|
unavailableAccounts: string[];
|
|
87
87
|
/** Window rows in provider-declared order (e.g. 5h → weekly → monthly); the fullest CARD_MAX_WINDOWS lead. */
|
|
88
88
|
windows: CardWindowRow[];
|
|
89
|
-
/** True when every account reports no limits (e.g. enterprise plans). */
|
|
89
|
+
/** True when every account reports no limits (e.g. enterprise plans, providers without a quota API). */
|
|
90
90
|
unlimited: boolean;
|
|
91
|
+
/** Distinct report notes across accounts (e.g. why a provider exposes no quota windows). */
|
|
92
|
+
notes: string[];
|
|
91
93
|
/** True when nothing is used anywhere (or there are no limits): collapses to a tick. */
|
|
92
94
|
idle: boolean;
|
|
93
95
|
resetCredits?: {
|
|
@@ -262,6 +264,7 @@ export function buildProviderCards(
|
|
|
262
264
|
unavailableAccounts: unavailable,
|
|
263
265
|
windows,
|
|
264
266
|
unlimited: windows.length === 0 && unavailable.length === 0,
|
|
267
|
+
notes: [...new Set(providerReports.flatMap(report => report.notes ?? []))],
|
|
265
268
|
idle:
|
|
266
269
|
unavailable.length === 0 &&
|
|
267
270
|
!resetCredits &&
|
|
@@ -451,6 +454,25 @@ function mutedText(content: string, wrap = false): NativeNode {
|
|
|
451
454
|
return text([span(content, "muted")], wrap ? { wrap: "word" } : { truncate: "end" });
|
|
452
455
|
}
|
|
453
456
|
|
|
457
|
+
/**
|
|
458
|
+
* A quiet line under the provider grid naming the providers that get no
|
|
459
|
+
* frame (`Untouched: Kimi Code, Zai`); `title` says why on hover.
|
|
460
|
+
*/
|
|
461
|
+
function footnote(key: string, label: string, cards: readonly ProviderCard[], title?: string): NativeNode {
|
|
462
|
+
const names = cards.map(card => card.name).join(", ");
|
|
463
|
+
return node(
|
|
464
|
+
"text",
|
|
465
|
+
{
|
|
466
|
+
spans: [span(`${label}: `, "dim"), span(names)],
|
|
467
|
+
wrap: "word",
|
|
468
|
+
role: "omp.usage.footnote",
|
|
469
|
+
...(title ? { title: sanitizeDisplayLine(title) } : {}),
|
|
470
|
+
},
|
|
471
|
+
undefined,
|
|
472
|
+
key,
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
454
476
|
/** Stable, human account name for a report in the detail view. */
|
|
455
477
|
function reportAccountLabel(report: UsageReport, limit: UsageLimit | undefined, index: number): string {
|
|
456
478
|
const metadata = report.metadata;
|
|
@@ -1020,36 +1042,59 @@ export class UsageDashboardComponent implements Component {
|
|
|
1020
1042
|
const children: NativeChild[] = [];
|
|
1021
1043
|
if (this.#cards.length === 0) {
|
|
1022
1044
|
children.push(
|
|
1023
|
-
node("text", { spans: [span("No usage data available.")], role: "omp.usage.
|
|
1045
|
+
node("text", { spans: [span("No usage data available.")], role: "omp.usage.footnote" }, undefined, "none"),
|
|
1024
1046
|
);
|
|
1025
1047
|
} else {
|
|
1026
|
-
//
|
|
1027
|
-
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1048
|
+
// Only providers with something to show get a frame: the rest are
|
|
1049
|
+
// named in footnotes, whether untouched, unreadable or quota-less.
|
|
1050
|
+
const framed: ProviderCard[] = [];
|
|
1051
|
+
const untouched: ProviderCard[] = [];
|
|
1052
|
+
const unreported: ProviderCard[] = [];
|
|
1053
|
+
const unmetered: ProviderCard[] = [];
|
|
1054
|
+
for (const entry of this.#cards) {
|
|
1055
|
+
const shown =
|
|
1056
|
+
entry.windows.length > 0 || entry.resetCredits !== undefined || entry.daybreakAccounts !== undefined;
|
|
1057
|
+
if (!shown) (entry.unavailableAccounts.length > 0 ? unreported : unmetered).push(entry);
|
|
1058
|
+
else if (entry.idle) untouched.push(entry);
|
|
1059
|
+
else framed.push(entry);
|
|
1060
|
+
}
|
|
1061
|
+
if (framed.length > 0) {
|
|
1030
1062
|
children.push(
|
|
1031
1063
|
node(
|
|
1032
1064
|
"row",
|
|
1033
1065
|
{ wrap: true, gap: "md", role: "omp.usage.grid" },
|
|
1034
|
-
|
|
1066
|
+
framed.map(entry => this.#describeCard(entry, meter)),
|
|
1035
1067
|
"providers",
|
|
1036
1068
|
),
|
|
1037
1069
|
);
|
|
1038
1070
|
}
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1071
|
+
const notes: NativeNode[] = [];
|
|
1072
|
+
if (untouched.length > 0) notes.push(footnote("idle", "Untouched", untouched));
|
|
1073
|
+
if (unreported.length > 0) {
|
|
1074
|
+
const accounts = unreported.flatMap(entry =>
|
|
1075
|
+
entry.unavailableAccounts.map(account => `${entry.name} (${account})`),
|
|
1076
|
+
);
|
|
1077
|
+
notes.push(
|
|
1078
|
+
footnote(
|
|
1079
|
+
"unreported",
|
|
1080
|
+
"No usage data",
|
|
1081
|
+
unreported,
|
|
1082
|
+
`No usage report came back for ${accounts.join(", ")}: the sign-in expired, the request failed, or the plan has no quotas`,
|
|
1050
1083
|
),
|
|
1051
1084
|
);
|
|
1052
1085
|
}
|
|
1086
|
+
if (unmetered.length > 0) {
|
|
1087
|
+
const why = unmetered.flatMap(entry => entry.notes.map(note => `${entry.name}: ${note}`));
|
|
1088
|
+
notes.push(
|
|
1089
|
+
footnote(
|
|
1090
|
+
"unmetered",
|
|
1091
|
+
"No quotas reported",
|
|
1092
|
+
unmetered,
|
|
1093
|
+
why.length > 0 ? why.join(" • ") : "These providers report no quota windows to track",
|
|
1094
|
+
),
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
if (notes.length > 0) children.push(node("col", { gap: "xs" }, notes, "footnotes"));
|
|
1053
1098
|
}
|
|
1054
1099
|
children.push(this.#describeActivity(chart));
|
|
1055
1100
|
return children;
|
|
@@ -1100,10 +1145,10 @@ export class UsageDashboardComponent implements Component {
|
|
|
1100
1145
|
);
|
|
1101
1146
|
}
|
|
1102
1147
|
for (const account of entry.unavailableAccounts) {
|
|
1103
|
-
children.push(mutedText(`${sanitizeDisplayLine(account)}: usage
|
|
1148
|
+
children.push(mutedText(`${sanitizeDisplayLine(account)}: no usage data`, true));
|
|
1104
1149
|
}
|
|
1105
1150
|
if (entry.unlimited) {
|
|
1106
|
-
children.push(mutedText("No
|
|
1151
|
+
children.push(mutedText("No quotas reported"));
|
|
1107
1152
|
} else {
|
|
1108
1153
|
for (const [index, window] of entry.windows.slice(0, CARD_MAX_WINDOWS).entries()) {
|
|
1109
1154
|
const label: TspSpan[] = [span(sanitizeDisplayLine(window.label))];
|
|
@@ -1112,7 +1157,9 @@ export class UsageDashboardComponent implements Component {
|
|
|
1112
1157
|
text(label, { role: "omp.usage.label", truncate: "middle", title: sanitizeDisplayLine(window.label) }),
|
|
1113
1158
|
];
|
|
1114
1159
|
if (window.fraction === undefined) {
|
|
1115
|
-
cells.push(
|
|
1160
|
+
cells.push(
|
|
1161
|
+
text([span(window.usedText ?? "No data", "muted")], { role: "omp.usage.amount", truncate: "end" }),
|
|
1162
|
+
);
|
|
1116
1163
|
} else {
|
|
1117
1164
|
const token =
|
|
1118
1165
|
window.status === "exhausted" ? "error" : window.status === "warning" ? "warning" : undefined;
|
|
@@ -1229,7 +1276,7 @@ export class UsageDashboardComponent implements Component {
|
|
|
1229
1276
|
const children: NativeChild[] = [];
|
|
1230
1277
|
for (const account of unavailable) {
|
|
1231
1278
|
if (account.provider !== entry.provider) continue;
|
|
1232
|
-
children.push(mutedText(`${sanitizeDisplayLine(account.label)}: usage
|
|
1279
|
+
children.push(mutedText(`${sanitizeDisplayLine(account.label)}: no usage data`, true));
|
|
1233
1280
|
}
|
|
1234
1281
|
|
|
1235
1282
|
const facts: { k: TspText; v: TspText }[] = [];
|
|
@@ -1321,7 +1368,7 @@ export class UsageDashboardComponent implements Component {
|
|
|
1321
1368
|
children.push(node("table", { cols, rows }, undefined, "limits"));
|
|
1322
1369
|
for (const note of new Set(limitNotes)) children.push(mutedText(note, true));
|
|
1323
1370
|
} else if (unavailable.every(account => account.provider !== entry.provider)) {
|
|
1324
|
-
children.push(mutedText("No
|
|
1371
|
+
children.push(mutedText("No quotas reported"));
|
|
1325
1372
|
}
|
|
1326
1373
|
sections.push(
|
|
1327
1374
|
node(
|
package/src/prompt/composer.ts
CHANGED
|
@@ -899,6 +899,7 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
|
|
|
899
899
|
|
|
900
900
|
/** Update the canonical editor reference after InteractiveMode remounts a custom editor. */
|
|
901
901
|
setEditor(editor: CustomEditor): void {
|
|
902
|
+
this.ui.replaceHeldFocus(this.#editor, editor);
|
|
902
903
|
this.#editor = editor;
|
|
903
904
|
}
|
|
904
905
|
|
|
@@ -1085,6 +1085,7 @@ const VIM_MODE_LABELS: Record<NonNullable<SegmentContext["vim"]>["mode"], string
|
|
|
1085
1085
|
normal: "NORMAL",
|
|
1086
1086
|
visual: "VISUAL",
|
|
1087
1087
|
"visual-line": "V-LINE",
|
|
1088
|
+
replace: "REPLACE",
|
|
1088
1089
|
};
|
|
1089
1090
|
|
|
1090
1091
|
/**
|
|
@@ -1097,6 +1098,7 @@ const VIM_MODE_ICON_KEYS: Record<NonNullable<SegmentContext["vim"]>["mode"], Sym
|
|
|
1097
1098
|
normal: "icon.vimNormal",
|
|
1098
1099
|
visual: "icon.vimVisual",
|
|
1099
1100
|
"visual-line": "icon.vimVisualLine",
|
|
1101
|
+
replace: "icon.vimReplace",
|
|
1100
1102
|
};
|
|
1101
1103
|
|
|
1102
1104
|
const VIM_MODE_COLORS: Record<NonNullable<SegmentContext["vim"]>["mode"], ThemeColor> = {
|
|
@@ -1104,6 +1106,7 @@ const VIM_MODE_COLORS: Record<NonNullable<SegmentContext["vim"]>["mode"], ThemeC
|
|
|
1104
1106
|
normal: "accent",
|
|
1105
1107
|
visual: "warning",
|
|
1106
1108
|
"visual-line": "warning",
|
|
1109
|
+
replace: "accent",
|
|
1107
1110
|
};
|
|
1108
1111
|
|
|
1109
1112
|
const vimSegment: StatusLineSegment = {
|
package/src/status-line/types.ts
CHANGED
|
@@ -114,7 +114,7 @@ export interface SegmentContext {
|
|
|
114
114
|
} | null;
|
|
115
115
|
/** Modal editing state, or null when `tui.vimMode` is off. */
|
|
116
116
|
vim: {
|
|
117
|
-
mode: "insert" | "normal" | "visual" | "visual-line";
|
|
117
|
+
mode: "insert" | "normal" | "visual" | "visual-line" | "replace";
|
|
118
118
|
/** Half-typed operator/count (`"2d"`), empty when nothing is pending. */
|
|
119
119
|
pending: string;
|
|
120
120
|
/** Lines spanned by the active Visual selection; 0 outside Visual modes. */
|
package/src/theme/symbols.ts
CHANGED
|
@@ -129,6 +129,7 @@ export type SymbolKey =
|
|
|
129
129
|
| "icon.vimInsert"
|
|
130
130
|
| "icon.vimVisual"
|
|
131
131
|
| "icon.vimVisualLine"
|
|
132
|
+
| "icon.vimReplace"
|
|
132
133
|
// Slash-command type indicators (autocomplete); names without an existing
|
|
133
134
|
// icon.* equivalent — see SlashCommandIconName for the full vocabulary.
|
|
134
135
|
| "cmd.action"
|
|
@@ -511,6 +512,7 @@ const UNICODE_SYMBOLS: SymbolMap = {
|
|
|
511
512
|
"icon.vimInsert": "▎",
|
|
512
513
|
"icon.vimVisual": "◉",
|
|
513
514
|
"icon.vimVisualLine": "≡",
|
|
515
|
+
"icon.vimReplace": "▁",
|
|
514
516
|
// Slash-command type indicators
|
|
515
517
|
"cmd.action": "❯",
|
|
516
518
|
"cmd.prompt": "✎",
|
|
@@ -921,6 +923,8 @@ const NERD_SYMBOLS: SymbolMap = {
|
|
|
921
923
|
"icon.vimVisual": "\uf06e",
|
|
922
924
|
// pick: (nf-fa-bars) | alt: (nf-fa-align_left)
|
|
923
925
|
"icon.vimVisualLine": "\uf0c9",
|
|
926
|
+
// pick: (nf-fa-exchange) | alt: (nf-fa-repeat)
|
|
927
|
+
"icon.vimReplace": "\uf0ec",
|
|
924
928
|
// Slash-command type indicators
|
|
925
929
|
// pick: (nf-cod-terminal) | alt: (nf-fa-terminal)
|
|
926
930
|
"cmd.action": "\uea85",
|
|
@@ -1289,6 +1293,7 @@ const ASCII_SYMBOLS: SymbolMap = {
|
|
|
1289
1293
|
"icon.vimInsert": "I",
|
|
1290
1294
|
"icon.vimVisual": "V",
|
|
1291
1295
|
"icon.vimVisualLine": "L",
|
|
1296
|
+
"icon.vimReplace": "R",
|
|
1292
1297
|
// Slash-command type indicators — unused; the icon column is disabled in ASCII mode
|
|
1293
1298
|
"cmd.action": "",
|
|
1294
1299
|
"cmd.prompt": "",
|