@oh-my-pi/pi-tui 18.14.2 → 18.16.0
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 +64 -50
- package/README.md +1 -1
- package/dist/types/app-keybindings.d.ts +5 -0
- package/dist/types/apps/session-picker.d.ts +5 -3
- package/dist/types/chat/bash-execution.d.ts +4 -1
- package/dist/types/chat/eval-execution.d.ts +2 -0
- package/dist/types/chat/execution-shared.d.ts +14 -8
- package/dist/types/chat/transcript-render-helpers.d.ts +8 -0
- package/dist/types/chrome/dynamic-border.d.ts +6 -0
- package/dist/types/chrome/transcript-status.d.ts +2 -2
- package/dist/types/components/editor.d.ts +15 -6
- package/dist/types/components/loader.d.ts +2 -0
- package/dist/types/keybinding-matchers.d.ts +7 -0
- package/dist/types/overlays/btw-history-panel.d.ts +13 -0
- package/dist/types/overlays/btw-history.d.ts +24 -0
- package/dist/types/overlays/btw-inject.d.ts +22 -0
- package/dist/types/overlays/btw-panel.d.ts +7 -0
- package/dist/types/overlays/hook-editor.d.ts +2 -0
- package/dist/types/overlays/jobs-panel.d.ts +4 -0
- package/dist/types/overlays/session-selector.d.ts +24 -3
- package/dist/types/render/render-utils.d.ts +21 -1
- package/dist/types/status-line/startup.d.ts +0 -15
- package/dist/types/terminal-capabilities.d.ts +1 -1
- package/dist/types/theme/symbols.d.ts +3 -1
- package/dist/types/theme/theme-class.d.ts +1 -0
- package/dist/types/tools/grouped-file-output.d.ts +76 -8
- package/dist/types/tools/hashline-format.d.ts +5 -0
- package/dist/types/tools/native-view.d.ts +2 -0
- package/dist/types/tools/output-meta.d.ts +27 -0
- package/dist/types/tools/read.d.ts +10 -0
- package/dist/types/tools/wait.d.ts +2 -0
- package/dist/types/utils.d.ts +4 -0
- package/package.json +9 -9
- package/src/app-keybindings.ts +5 -0
- package/src/apps/session-picker.ts +7 -3
- package/src/chat/bash-execution.ts +20 -9
- package/src/chat/chat-transcript-builder.ts +5 -2
- package/src/chat/eval-execution.ts +14 -7
- package/src/chat/execution-shared.ts +39 -18
- package/src/chat/read-tool-group.ts +56 -36
- package/src/chat/transcript-render-helpers.ts +52 -6
- package/src/chrome/dynamic-border.ts +28 -2
- package/src/chrome/transcript-status.ts +5 -3
- package/src/components/editor.ts +123 -24
- package/src/components/key-value-list.ts +9 -9
- package/src/components/loader.ts +7 -0
- package/src/components/section.ts +1 -9
- package/src/components/table.ts +1 -9
- package/src/keybinding-matchers.ts +13 -0
- package/src/overlays/btw-history-panel.ts +98 -14
- package/src/overlays/btw-history.ts +65 -0
- package/src/overlays/btw-inject.ts +91 -0
- package/src/overlays/btw-panel.ts +53 -12
- package/src/overlays/hook-editor.ts +6 -0
- package/src/overlays/jobs-panel.ts +45 -19
- package/src/overlays/model-browser.ts +65 -8
- package/src/overlays/session-selector.ts +131 -26
- package/src/overlays/usage-dashboard.ts +2 -2
- package/src/render/code-cell.ts +1 -1
- package/src/render/file-list.ts +4 -7
- package/src/render/render-utils.ts +36 -6
- package/src/setup/scenes/glyph.ts +1 -1
- package/src/status-line/startup.ts +36 -70
- package/src/terminal-capabilities.ts +5 -0
- package/src/theme/symbols.ts +16 -9
- package/src/theme/theme-class.ts +3 -0
- package/src/tmux.ts +1 -0
- package/src/tools/agent-tree.ts +13 -11
- package/src/tools/ask.ts +13 -6
- package/src/tools/ast-edit.ts +48 -93
- package/src/tools/ast-grep.ts +22 -66
- package/src/tools/bash.ts +7 -0
- package/src/tools/default-renderer.ts +1 -1
- package/src/tools/edit.ts +50 -43
- package/src/tools/fetch.ts +2 -1
- package/src/tools/find.ts +6 -3
- package/src/tools/github.ts +14 -46
- package/src/tools/glob.ts +2 -1
- package/src/tools/grep.ts +43 -287
- package/src/tools/grouped-file-output.ts +250 -64
- package/src/tools/hashline-format.ts +5 -0
- package/src/tools/json-tree.ts +1 -5
- package/src/tools/lsp.ts +53 -25
- package/src/tools/native-view.ts +11 -5
- package/src/tools/output-meta.ts +68 -4
- package/src/tools/proc-render.ts +3 -3
- package/src/tools/read.ts +14 -0
- package/src/tools/task.ts +34 -77
- package/src/tools/vibe.ts +4 -4
- package/src/tools/wait.ts +8 -5
- package/src/tools/write.ts +17 -27
- package/src/tools/yield.ts +1 -1
- package/src/utils.ts +10 -0
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
* statusLine.attachToEditor(editor, getComposerStyle(shape));
|
|
14
14
|
* ```
|
|
15
15
|
*/
|
|
16
|
+
import { type } from "@oh-my-pi/omptype";
|
|
16
17
|
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
17
18
|
import type { Model } from "@oh-my-pi/pi-catalog/types";
|
|
18
|
-
import { isRecord } from "@oh-my-pi/pi-utils/type-guards";
|
|
19
19
|
import { parseThinkingLevel } from "../thinking";
|
|
20
20
|
import { StatusLineComponent } from "./component";
|
|
21
21
|
import type { CompactionBoundaries } from "./context-usage";
|
|
@@ -100,82 +100,48 @@ export function createStartupStatusLine(data: StatusLineStartupData): StatusLine
|
|
|
100
100
|
return statusLine;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
return typeof value === "string" && values.some(candidate => candidate === value);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function isOptionalBoolean(value: unknown): boolean {
|
|
108
|
-
return value === undefined || typeof value === "boolean";
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function isSegmentList(value: unknown): boolean {
|
|
112
|
-
return value === undefined || (Array.isArray(value) && value.every(id => isOneOf(STATUS_LINE_SEGMENT_IDS, id)));
|
|
113
|
-
}
|
|
103
|
+
const SegmentIds = type.enumerated(...STATUS_LINE_SEGMENT_IDS).array();
|
|
114
104
|
|
|
115
105
|
/** Shallow check of persisted settings; nested segment options are trusted as written by this version. */
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
isOptionalBoolean(value.compactThinkingLevel)
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** Check the model fields the status line reads; the rest is trusted as serialized by this version. */
|
|
133
|
-
function isPersistedModel(value: unknown): value is Model {
|
|
134
|
-
return (
|
|
135
|
-
isRecord(value) &&
|
|
136
|
-
typeof value.id === "string" &&
|
|
137
|
-
typeof value.name === "string" &&
|
|
138
|
-
typeof value.provider === "string" &&
|
|
139
|
-
typeof value.api === "string" &&
|
|
140
|
-
typeof value.contextWindow === "number"
|
|
141
|
-
);
|
|
142
|
-
}
|
|
106
|
+
const StatusLineSettingsShape = type({
|
|
107
|
+
"preset?": type.enumerated(...STATUS_LINE_PRESET_VALUES),
|
|
108
|
+
"leftSegments?": SegmentIds,
|
|
109
|
+
"rightSegments?": SegmentIds,
|
|
110
|
+
"separator?": type.enumerated(...STATUS_LINE_SEPARATOR_VALUES),
|
|
111
|
+
"segmentOptions?": "Record<string, unknown>",
|
|
112
|
+
"contextLine?": type.enumerated(...CONTEXT_LINE_MODE_VALUES),
|
|
113
|
+
"showHookStatus?": "boolean",
|
|
114
|
+
"sessionAccent?": "boolean",
|
|
115
|
+
"transparent?": "boolean",
|
|
116
|
+
"compactThinkingLevel?": "boolean",
|
|
117
|
+
});
|
|
143
118
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
119
|
+
const StartupDataShape = type({
|
|
120
|
+
"+": "delete",
|
|
121
|
+
settings: StatusLineSettingsShape,
|
|
122
|
+
gitEnabled: "boolean",
|
|
123
|
+
// Only the fields the status line reads; the rest is trusted as serialized by this version.
|
|
124
|
+
"model?": { id: "string", name: "string", provider: "string", api: "string", contextWindow: "number" },
|
|
125
|
+
"thinkingLevel?": "string",
|
|
126
|
+
autoThinking: "boolean",
|
|
127
|
+
fastMode: "boolean",
|
|
128
|
+
usingSubscription: "boolean",
|
|
129
|
+
autoCompactEnabled: "boolean",
|
|
130
|
+
compactionBoundaries: type({ "+": "delete", thresholdPercent: "number", speculationPercent: "number | null" }).or(
|
|
131
|
+
"null",
|
|
132
|
+
),
|
|
133
|
+
});
|
|
151
134
|
|
|
152
135
|
/** Validate persisted {@link StatusLineStartupData}; `undefined` for anything malformed. */
|
|
153
136
|
export function readStatusLineStartupData(value: unknown): StatusLineStartupData | undefined {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (value.thinkingLevel !== undefined && thinkingLevel === undefined) return undefined;
|
|
159
|
-
const compactionBoundaries = readCompactionBoundaries(value.compactionBoundaries);
|
|
160
|
-
if (
|
|
161
|
-
typeof value.gitEnabled !== "boolean" ||
|
|
162
|
-
typeof value.autoThinking !== "boolean" ||
|
|
163
|
-
typeof value.fastMode !== "boolean" ||
|
|
164
|
-
typeof value.usingSubscription !== "boolean" ||
|
|
165
|
-
typeof value.autoCompactEnabled !== "boolean" ||
|
|
166
|
-
compactionBoundaries === undefined
|
|
167
|
-
) {
|
|
168
|
-
return undefined;
|
|
169
|
-
}
|
|
137
|
+
const data = StartupDataShape(value);
|
|
138
|
+
if (data instanceof type.errors) return undefined;
|
|
139
|
+
const thinkingLevel = data.thinkingLevel === undefined ? undefined : parseThinkingLevel(data.thinkingLevel);
|
|
140
|
+
if (data.thinkingLevel !== undefined && thinkingLevel === undefined) return undefined;
|
|
170
141
|
return {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
model,
|
|
142
|
+
...data,
|
|
143
|
+
settings: data.settings as StatusLineSettings,
|
|
144
|
+
model: data.model as Model | undefined,
|
|
174
145
|
thinkingLevel,
|
|
175
|
-
autoThinking: value.autoThinking,
|
|
176
|
-
fastMode: value.fastMode,
|
|
177
|
-
usingSubscription: value.usingSubscription,
|
|
178
|
-
autoCompactEnabled: value.autoCompactEnabled,
|
|
179
|
-
compactionBoundaries,
|
|
180
146
|
};
|
|
181
147
|
}
|
|
@@ -67,6 +67,7 @@ export type TerminalId =
|
|
|
67
67
|
| "rio"
|
|
68
68
|
| "tern"
|
|
69
69
|
| "monstar"
|
|
70
|
+
| "rex"
|
|
70
71
|
| "base"
|
|
71
72
|
| "trueColor";
|
|
72
73
|
|
|
@@ -626,6 +627,8 @@ const KNOWN_TERMINALS = Object.freeze({
|
|
|
626
627
|
// OSC 99, and OSC 66 text sizing are outside rio's supported set and keep
|
|
627
628
|
// the conservative defaults.
|
|
628
629
|
rio: new TerminalInfo("rio", ImageProtocol.Kitty, true, true),
|
|
630
|
+
// Rex (TERM_PROGRAM=rex / TERM=xterm-rex): only Kitty graphics is confirmed.
|
|
631
|
+
rex: new TerminalInfo("rex", ImageProtocol.Kitty, true, false),
|
|
629
632
|
// Tern (Stencil's terminal, `stencil-term`) sets TERM_PROGRAM=tern and
|
|
630
633
|
// implements Kitty graphics, OSC 8 and OSC 9/99 notifications. Whether omp
|
|
631
634
|
// renders natively (Tern Surface Protocol) is decided by the `hello`
|
|
@@ -666,6 +669,7 @@ export function detectTerminalId(env: NodeJS.ProcessEnv = Bun.env): TerminalId {
|
|
|
666
669
|
if (caseEq(program, "rio")) return "rio";
|
|
667
670
|
if (caseEq(program, "tern")) return "tern";
|
|
668
671
|
if (caseEq(program, "monstar")) return "monstar";
|
|
672
|
+
if (caseEq(program, "rex")) return "rex";
|
|
669
673
|
return null;
|
|
670
674
|
}
|
|
671
675
|
|
|
@@ -717,6 +721,7 @@ export function detectTerminalId(env: NodeJS.ProcessEnv = Bun.env): TerminalId {
|
|
|
717
721
|
|
|
718
722
|
if (TERM?.toLowerCase().includes("ghostty")) return "ghostty";
|
|
719
723
|
if (TERM && caseEq(TERM, "monstar")) return "monstar";
|
|
724
|
+
if (TERM && caseEq(TERM, "xterm-rex")) return "rex";
|
|
720
725
|
|
|
721
726
|
if (COLORTERM) {
|
|
722
727
|
if (caseEq(COLORTERM, "truecolor") || caseEq(COLORTERM, "24bit")) return "trueColor";
|
package/src/theme/symbols.ts
CHANGED
|
@@ -19,6 +19,7 @@ export type SymbolKey =
|
|
|
19
19
|
| "status.running"
|
|
20
20
|
| "status.shadowed"
|
|
21
21
|
| "status.aborted"
|
|
22
|
+
/** @deprecated Alias of `status.success`; kept so extensions and theme overrides that name it keep working. */
|
|
22
23
|
| "status.done"
|
|
23
24
|
// Navigation
|
|
24
25
|
| "nav.cursor"
|
|
@@ -212,6 +213,8 @@ export type SymbolKey =
|
|
|
212
213
|
| "advisor.rail"
|
|
213
214
|
// Skill callout rail (user turn that opened with a skill chip)
|
|
214
215
|
| "skill.rail"
|
|
216
|
+
// /btw steering-prompt rail (the <inject> block a side answer drafted)
|
|
217
|
+
| "btw.rail"
|
|
215
218
|
// Language/file type icons
|
|
216
219
|
| "lang.default"
|
|
217
220
|
| "lang.typescript"
|
|
@@ -404,7 +407,7 @@ const UNICODE_SYMBOLS: SymbolMap = {
|
|
|
404
407
|
"status.running": "⟳",
|
|
405
408
|
"status.shadowed": "○",
|
|
406
409
|
"status.aborted": "⏹",
|
|
407
|
-
"status.done": "
|
|
410
|
+
"status.done": "✔",
|
|
408
411
|
// Navigation
|
|
409
412
|
"nav.cursor": "❯",
|
|
410
413
|
"nav.selected": "➤",
|
|
@@ -598,6 +601,7 @@ const UNICODE_SYMBOLS: SymbolMap = {
|
|
|
598
601
|
// Advisor note rail (heavier than md.quoteBorder so notes read as a distinct voice)
|
|
599
602
|
"advisor.rail": "▎",
|
|
600
603
|
"skill.rail": "▎",
|
|
604
|
+
"btw.rail": "▎",
|
|
601
605
|
// Language/file icons (emoji-centric, no Nerd Font required)
|
|
602
606
|
"lang.default": "⌘",
|
|
603
607
|
"lang.typescript": "🟦",
|
|
@@ -721,8 +725,8 @@ const NERD_SYMBOLS: SymbolMap = {
|
|
|
721
725
|
"status.shadowed": "\uf10c",
|
|
722
726
|
// pick: | alt:
|
|
723
727
|
"status.aborted": "\uf04d",
|
|
724
|
-
//
|
|
725
|
-
"status.done": "
|
|
728
|
+
// alias of status.success
|
|
729
|
+
"status.done": "\uf00c",
|
|
726
730
|
// Navigation
|
|
727
731
|
// pick: | alt:
|
|
728
732
|
"nav.cursor": "\uf054",
|
|
@@ -1072,6 +1076,7 @@ const NERD_SYMBOLS: SymbolMap = {
|
|
|
1072
1076
|
// pick: ▎ | alt: ┃ │
|
|
1073
1077
|
"advisor.rail": "▎",
|
|
1074
1078
|
"skill.rail": "▎",
|
|
1079
|
+
"btw.rail": "▎",
|
|
1075
1080
|
// Language icons (nerd font devicons)
|
|
1076
1081
|
"lang.default": "",
|
|
1077
1082
|
"lang.typescript": "\u{E628}",
|
|
@@ -1186,17 +1191,17 @@ const NERD_SYMBOLS: SymbolMap = {
|
|
|
1186
1191
|
|
|
1187
1192
|
const ASCII_SYMBOLS: SymbolMap = {
|
|
1188
1193
|
// Status Indicators
|
|
1189
|
-
"status.success": "[
|
|
1190
|
-
"status.error": "[
|
|
1194
|
+
"status.success": "[+]",
|
|
1195
|
+
"status.error": "[-]",
|
|
1191
1196
|
"status.warning": "[!]",
|
|
1192
1197
|
"status.info": "[i]",
|
|
1193
|
-
"status.pending": "[
|
|
1198
|
+
"status.pending": "[ ]",
|
|
1194
1199
|
"status.disabled": "[ ]",
|
|
1195
1200
|
"status.enabled": "[x]",
|
|
1196
1201
|
"status.running": "[~]",
|
|
1197
1202
|
"status.shadowed": "[/]",
|
|
1198
|
-
"status.aborted": "[
|
|
1199
|
-
"status.done": "
|
|
1203
|
+
"status.aborted": "[#]",
|
|
1204
|
+
"status.done": "[+]",
|
|
1200
1205
|
// Navigation
|
|
1201
1206
|
"nav.cursor": ">",
|
|
1202
1207
|
"nav.selected": "->",
|
|
@@ -1386,6 +1391,7 @@ const ASCII_SYMBOLS: SymbolMap = {
|
|
|
1386
1391
|
"md.colorSwatch": "[]",
|
|
1387
1392
|
"advisor.rail": "|",
|
|
1388
1393
|
"skill.rail": "|",
|
|
1394
|
+
"btw.rail": "|",
|
|
1389
1395
|
// Language icons (ASCII uses abbreviations)
|
|
1390
1396
|
"lang.default": "code",
|
|
1391
1397
|
"lang.typescript": "ts",
|
|
@@ -1502,7 +1508,8 @@ export const SPINNER_FRAMES: Record<SymbolPreset, Record<SpinnerType, string[]>>
|
|
|
1502
1508
|
activity: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
|
|
1503
1509
|
},
|
|
1504
1510
|
ascii: {
|
|
1505
|
-
status
|
|
1511
|
+
// Bracketed like the `status.*` glyphs, so a settling call keeps its column width.
|
|
1512
|
+
status: ["[|]", "[/]", "[-]", "[\\]"],
|
|
1506
1513
|
activity: ["-", "\\", "|", "/"],
|
|
1507
1514
|
},
|
|
1508
1515
|
};
|
package/src/theme/theme-class.ts
CHANGED
|
@@ -243,6 +243,8 @@ export class Theme {
|
|
|
243
243
|
logger.debug("Invalid symbol key in override", { key, availableKeys: Object.keys(this.#symbols) });
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
|
+
// `status.done` aliases `status.success` unless a theme overrides it explicitly.
|
|
247
|
+
if (!("status.done" in symbolOverrides)) this.#symbols["status.done"] = this.#symbols["status.success"];
|
|
246
248
|
this.#spinnerFramesOverrides = spinnerFramesOverrides;
|
|
247
249
|
}
|
|
248
250
|
|
|
@@ -528,6 +530,7 @@ export class Theme {
|
|
|
528
530
|
running: this.#symbols["status.running"],
|
|
529
531
|
shadowed: this.#symbols["status.shadowed"],
|
|
530
532
|
aborted: this.#symbols["status.aborted"],
|
|
533
|
+
/** @deprecated Alias of `success`. */
|
|
531
534
|
done: this.#symbols["status.done"],
|
|
532
535
|
};
|
|
533
536
|
}
|
package/src/tmux.ts
CHANGED
|
@@ -21,6 +21,7 @@ const CLIENT_TERMINAL_TIMEOUT_MS = 2_000;
|
|
|
21
21
|
const CLIENT_TERMNAME_PROGRAMS: ReadonlyArray<readonly [RegExp, string]> = [
|
|
22
22
|
[/^xterm-kitty$/u, "kitty"],
|
|
23
23
|
[/^xterm-ghostty$/u, "ghostty"],
|
|
24
|
+
[/^xterm-rex$/u, "rex"],
|
|
24
25
|
[/^wezterm$/u, "wezterm"],
|
|
25
26
|
];
|
|
26
27
|
let cachedClientTerminalNames: readonly string[] | undefined;
|
package/src/tools/agent-tree.ts
CHANGED
|
@@ -74,20 +74,22 @@ export function renderAgentTreeRow(
|
|
|
74
74
|
const task = options.presentation === "task";
|
|
75
75
|
const live = status === "pending" || status === "running";
|
|
76
76
|
const failed = status === "failed" || status === "aborted";
|
|
77
|
-
const iconColor = status === "completed" ? "success" : failed ? "error" : "accent";
|
|
78
77
|
const nameColor = task && live && options.frozen ? "dim" : task && status === "completed" ? "text" : "accent";
|
|
79
|
-
const iconStatus = status === "failed" ? "error" : status === "completed" ? "
|
|
78
|
+
const iconStatus = status === "failed" ? "error" : status === "completed" ? "success" : status;
|
|
79
|
+
// Task rows draw a static glyph, so their bytes hold across spinner frames; eval rows animate.
|
|
80
|
+
const spinnerFrame = !task && status === "running" ? options.spinnerFrame : undefined;
|
|
81
|
+
// formatStatusIcon colors every glyph except a raw spinner frame, which eval rows paint accent.
|
|
82
|
+
// A frozen snapshot of a live Task row dims its glyph with its name, so it stops reading as in flight.
|
|
80
83
|
const icon =
|
|
81
|
-
task &&
|
|
82
|
-
? theme.styledSymbol("
|
|
83
|
-
:
|
|
84
|
-
? theme.
|
|
85
|
-
:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
);
|
|
84
|
+
!task && status === "completed"
|
|
85
|
+
? theme.styledSymbol("tool.eval", "accent")
|
|
86
|
+
: task && live && options.frozen
|
|
87
|
+
? theme.fg("dim", status === "running" ? theme.status.running : theme.status.pending)
|
|
88
|
+
: spinnerFrame === undefined
|
|
89
|
+
? formatStatusIcon(iconStatus, theme)
|
|
90
|
+
: theme.fg("accent", formatStatusIcon(iconStatus, theme, spinnerFrame));
|
|
89
91
|
const lead = `${options.prefix}${options.prefix || !task ? " " : ""}${icon} `;
|
|
90
|
-
const statusBadge = options.statusBadge ?? (failed ? ` ${formatBadge(status,
|
|
92
|
+
const statusBadge = options.statusBadge ?? (failed ? ` ${formatBadge(status, "error", theme)}` : "");
|
|
91
93
|
const id = Number.isFinite(width)
|
|
92
94
|
? truncateToWidth(options.id, Math.max(0, width - visibleWidth(lead) - visibleWidth(statusBadge)))
|
|
93
95
|
: options.id;
|
package/src/tools/ask.ts
CHANGED
|
@@ -25,7 +25,13 @@ import { outputBlockContentWidth, renderStatusLine } from "../render";
|
|
|
25
25
|
import { framedToolCard } from "../render/tool-card";
|
|
26
26
|
import { wrapPrefixedLine } from "../render/hanging-wrap";
|
|
27
27
|
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
formatErrorMessage,
|
|
30
|
+
formatMeta,
|
|
31
|
+
formatStatusIcon,
|
|
32
|
+
formatTitle,
|
|
33
|
+
sanitizeCarriageReturns,
|
|
34
|
+
} from "../render/render-utils";
|
|
29
35
|
|
|
30
36
|
/** Result for a single question */
|
|
31
37
|
export interface QuestionResult {
|
|
@@ -176,10 +182,11 @@ function normalizeRenderQuestions(raw: unknown): NonNullable<AskRenderArgs["ques
|
|
|
176
182
|
/** Render a custom free-text answer as a status line plus indented continuation rows. */
|
|
177
183
|
function renderCustomInputLines(uiTheme: Theme, customInput: string): string[] {
|
|
178
184
|
const lines = customInput.split("\n");
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
185
|
+
const icon = formatStatusIcon("success", uiTheme);
|
|
186
|
+
// Continuation rows line up under the answer text, past the glyph's own width.
|
|
187
|
+
const indent = " ".repeat(visibleWidth(icon) + 2);
|
|
188
|
+
const out: string[] = [` ${icon} ${uiTheme.fg("toolOutput", lines[0] ?? "")}`];
|
|
189
|
+
for (let i = 1; i < lines.length; i++) out.push(`${indent}${uiTheme.fg("toolOutput", lines[i])}`);
|
|
183
190
|
return out;
|
|
184
191
|
}
|
|
185
192
|
|
|
@@ -282,7 +289,7 @@ function renderAnswerOptionLines(
|
|
|
282
289
|
|
|
283
290
|
// Nothing was chosen (and no custom answer) → a lone cancelled marker.
|
|
284
291
|
if (selected.size === 0 && customInput === undefined && note === undefined) {
|
|
285
|
-
return [` ${
|
|
292
|
+
return [` ${formatStatusIcon("warning", uiTheme)} ${uiTheme.fg("warning", "Cancelled")}`];
|
|
286
293
|
}
|
|
287
294
|
|
|
288
295
|
const out: string[] = [];
|
package/src/tools/ast-edit.ts
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
|
-
import { isRecord, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
2
1
|
import type { Component } from "../tui";
|
|
3
2
|
import { Text } from "../components/text";
|
|
4
|
-
import { replaceTabs } from "../utils";
|
|
5
3
|
import type { Theme } from "../theme/theme";
|
|
6
|
-
import { Ellipsis,
|
|
4
|
+
import { Ellipsis, renderStatusLine, truncateToWidth } from "../render";
|
|
7
5
|
import { framedToolCard } from "../render/tool-card";
|
|
8
6
|
import {
|
|
9
7
|
appendParseErrorsBulletList,
|
|
10
8
|
formatCount,
|
|
11
9
|
formatErrorDetail,
|
|
12
|
-
formatMoreItems,
|
|
13
10
|
formatParseErrorsCountLabel,
|
|
14
11
|
PREVIEW_LIMITS,
|
|
15
12
|
} from "../render/render-utils";
|
|
16
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
classifyGroupedLines,
|
|
15
|
+
createGroupedRowStyler,
|
|
16
|
+
fitGroupsToBudget,
|
|
17
|
+
groupedLinkTargets,
|
|
18
|
+
groupLineIndicesByBlank,
|
|
19
|
+
groupsByFile,
|
|
20
|
+
walkGroupedOutput,
|
|
21
|
+
} from "./grouped-file-output";
|
|
17
22
|
import type { OutputMeta } from "./output-meta";
|
|
18
23
|
import type {
|
|
19
24
|
NativeToolHead,
|
|
@@ -59,34 +64,24 @@ interface AstEditRenderArgs {
|
|
|
59
64
|
const COLLAPSED_CHANGE_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
|
|
60
65
|
|
|
61
66
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* noise. Collapsed mode always shows at least the first group, then fills up
|
|
66
|
-
* to `budget` lines before summarizing the rest as `… N more changes`.
|
|
67
|
+
* Frame body lines for per-file change groups, styled on demand: files apart by
|
|
68
|
+
* a blank line and without tree guides (the frame border is the container), fit
|
|
69
|
+
* to `budget` with the rest summarized as `… N more files`.
|
|
67
70
|
*/
|
|
68
71
|
function buildChangeBody(
|
|
69
72
|
groups: readonly (readonly number[])[],
|
|
70
73
|
styleLine: (index: number) => string,
|
|
71
|
-
|
|
74
|
+
isFileRow: (index: number) => boolean,
|
|
72
75
|
budget: number,
|
|
73
76
|
theme: Theme,
|
|
74
77
|
): string[] {
|
|
78
|
+
const { shown, summary } = fitGroupsToBudget(groups, budget, { isUnit: isFileRow, unit: "file", gap: 1 });
|
|
75
79
|
const lines: string[] = [];
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const group = groups[i]!;
|
|
79
|
-
const separator = shown > 0 ? 1 : 0;
|
|
80
|
-
const remainingAfter = groups.length - (i + 1);
|
|
81
|
-
const reserved = !expanded && remainingAfter > 0 ? 1 : 0;
|
|
82
|
-
// Always emit the first group; budget only gates subsequent ones.
|
|
83
|
-
if (!expanded && shown > 0 && lines.length + separator + group.length + reserved > budget) break;
|
|
84
|
-
if (separator) lines.push("");
|
|
80
|
+
for (const group of shown) {
|
|
81
|
+
if (lines.length > 0) lines.push("");
|
|
85
82
|
for (const index of group) lines.push(styleLine(index));
|
|
86
|
-
shown++;
|
|
87
83
|
}
|
|
88
|
-
|
|
89
|
-
if (!expanded && remaining > 0) lines.push(theme.fg("muted", formatMoreItems(remaining, "change")));
|
|
84
|
+
if (summary) lines.push(theme.fg("muted", summary));
|
|
90
85
|
return lines;
|
|
91
86
|
}
|
|
92
87
|
|
|
@@ -98,9 +93,7 @@ function patternPreview(pat: string | undefined): string | undefined {
|
|
|
98
93
|
return collapsed || undefined;
|
|
99
94
|
}
|
|
100
95
|
|
|
101
|
-
const AST_EDIT_HEADER_RE = /^(#+)\s+(.*)$/;
|
|
102
96
|
const AST_EDIT_CHANGE_LINE_RE = /^([+\- ])\s*(\d+)(?:│|[:|])(.*)$/;
|
|
103
|
-
const AST_EDIT_HEADER_SUFFIX_RE = /\s+\([^)]*\)\s*$/;
|
|
104
97
|
|
|
105
98
|
/** One file's proposed rewrite: its unified diff text and change counts. */
|
|
106
99
|
interface AstEditFileDiff {
|
|
@@ -117,29 +110,15 @@ interface AstEditFileDiff {
|
|
|
117
110
|
*/
|
|
118
111
|
function parseAstEditChanges(lines: readonly string[]): AstEditFileDiff[] {
|
|
119
112
|
const files: AstEditFileDiff[] = [];
|
|
120
|
-
const dirs: string[] = [];
|
|
121
113
|
let file: AstEditFileDiff | undefined;
|
|
122
|
-
const
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
for (const line of lines) {
|
|
127
|
-
const header = AST_EDIT_HEADER_RE.exec(line);
|
|
128
|
-
if (header) {
|
|
129
|
-
flush();
|
|
130
|
-
const depth = header[1]!.length;
|
|
131
|
-
const rest = header[2]!.trimEnd().replace(AST_EDIT_HEADER_SUFFIX_RE, "");
|
|
132
|
-
dirs.length = depth - 1;
|
|
133
|
-
if (rest.endsWith("/")) {
|
|
134
|
-
dirs[depth - 1] = rest.slice(0, -1);
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
const prefix = dirs.filter(Boolean).join("/");
|
|
138
|
-
file = { path: prefix ? `${prefix}/${rest}` : rest, lines: [], added: 0, removed: 0 };
|
|
114
|
+
for (const event of walkGroupedOutput(lines)) {
|
|
115
|
+
if (event.kind !== "line") {
|
|
116
|
+
if (file && file.lines.length > 0) files.push(file);
|
|
117
|
+
file = event.kind === "file" ? { path: event.path, lines: [], added: 0, removed: 0 } : undefined;
|
|
139
118
|
continue;
|
|
140
119
|
}
|
|
141
|
-
if (!file
|
|
142
|
-
const change = AST_EDIT_CHANGE_LINE_RE.exec(
|
|
120
|
+
if (!file) continue;
|
|
121
|
+
const change = AST_EDIT_CHANGE_LINE_RE.exec(event.text);
|
|
143
122
|
if (!change) continue;
|
|
144
123
|
const lineNumber = Number.parseInt(change[2]!, 10);
|
|
145
124
|
if (file.lastOld === undefined || lineNumber > file.lastOld + 1 || lineNumber < file.lastOld) {
|
|
@@ -150,7 +129,7 @@ function parseAstEditChanges(lines: readonly string[]): AstEditFileDiff[] {
|
|
|
150
129
|
else if (change[1] === "-") file.removed++;
|
|
151
130
|
file.lines.push(`${change[1]}${change[3]}`);
|
|
152
131
|
}
|
|
153
|
-
|
|
132
|
+
if (file && file.lines.length > 0) files.push(file);
|
|
154
133
|
return files;
|
|
155
134
|
}
|
|
156
135
|
|
|
@@ -248,53 +227,23 @@ export const astEditToolRenderer = {
|
|
|
248
227
|
|
|
249
228
|
const textContent = result.details?.displayContent ?? result.content?.find(c => c.type === "text")?.text ?? "";
|
|
250
229
|
const allLines = textContent.split("\n");
|
|
251
|
-
//
|
|
252
|
-
// reconstruct across the blank-line groups the tree list collapses by.
|
|
230
|
+
// Classify the whole output so nested directory headers resolve across blank-line groups.
|
|
253
231
|
const contexts = classifyGroupedLines(allLines, details?.cwd ?? details?.searchPath, details?.searchPath);
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
const styleLine = (index: number): string => {
|
|
270
|
-
const cached = styledLines[index];
|
|
271
|
-
if (cached !== undefined) return cached;
|
|
272
|
-
const ctx = contexts[index]!;
|
|
273
|
-
// Swap the inner code-frame gutter `│` for a space so it does not nest a
|
|
274
|
-
// second vertical bar inside the frame border.
|
|
275
|
-
const display = replaceTabs(allLines[index]!.replace("│", " "));
|
|
276
|
-
let styled: string;
|
|
277
|
-
const linkPath =
|
|
278
|
-
ctx.headerPath && rawContexts?.[index]?.kind === ctx.kind && rawContexts[index]?.depth === ctx.depth
|
|
279
|
-
? (rawContexts[index]?.headerPath ?? ctx.headerPath)
|
|
280
|
-
: ctx.headerPath;
|
|
281
|
-
if (ctx.kind === "dir") {
|
|
282
|
-
const accent = uiTheme.fg("accent", display);
|
|
283
|
-
styled = linkPath ? fileHyperlink(linkPath, accent) : accent;
|
|
284
|
-
} else if (ctx.kind === "file") {
|
|
285
|
-
const tinted = uiTheme.fg(ctx.depth === 1 ? "accent" : "dim", display);
|
|
286
|
-
styled = linkPath ? fileHyperlink(linkPath, tinted) : tinted;
|
|
287
|
-
} else if (display.startsWith("+")) {
|
|
288
|
-
styled = uiTheme.fg("toolDiffAdded", display);
|
|
289
|
-
} else if (display.startsWith("-")) {
|
|
290
|
-
styled = uiTheme.fg("toolDiffRemoved", display);
|
|
291
|
-
} else {
|
|
292
|
-
styled = uiTheme.fg("toolOutput", display);
|
|
293
|
-
}
|
|
294
|
-
styledLines[index] = styled;
|
|
295
|
-
return styled;
|
|
296
|
-
};
|
|
297
|
-
const changeGroups = groupLineIndicesByBlank(allLines).filter(indices => {
|
|
232
|
+
const styleLine = createGroupedRowStyler(allLines, contexts, uiTheme, {
|
|
233
|
+
linkTarget: groupedLinkTargets(contexts, textContent, details, options.renderContext?.rawDetails),
|
|
234
|
+
styleBody: index => {
|
|
235
|
+
// Swap the code-frame gutter `│` for a space so it does not nest a
|
|
236
|
+
// second vertical bar inside the frame border.
|
|
237
|
+
const line = allLines[index]!.replace("│", " ");
|
|
238
|
+
const color = line.startsWith("+")
|
|
239
|
+
? "toolDiffAdded"
|
|
240
|
+
: line.startsWith("-")
|
|
241
|
+
? "toolDiffRemoved"
|
|
242
|
+
: "toolOutput";
|
|
243
|
+
return uiTheme.fg(color, line);
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
const changeGroups = groupsByFile(groupLineIndicesByBlank(allLines), contexts).filter(indices => {
|
|
298
247
|
const first = allLines[indices[0]!]!;
|
|
299
248
|
return !first.startsWith("Safety cap reached") && !first.startsWith("Parse issues:");
|
|
300
249
|
});
|
|
@@ -321,7 +270,13 @@ export const astEditToolRenderer = {
|
|
|
321
270
|
({ contentWidth }) => {
|
|
322
271
|
const expanded = Boolean(options.expanded);
|
|
323
272
|
if (bodyMemo === undefined || bodyMemo.expanded !== expanded || bodyMemo.width !== contentWidth) {
|
|
324
|
-
const changeLines = buildChangeBody(
|
|
273
|
+
const changeLines = buildChangeBody(
|
|
274
|
+
changeGroups,
|
|
275
|
+
styleLine,
|
|
276
|
+
index => contexts[index]!.kind === "file",
|
|
277
|
+
expanded ? Number.POSITIVE_INFINITY : COLLAPSED_CHANGE_LIMIT,
|
|
278
|
+
uiTheme,
|
|
279
|
+
);
|
|
325
280
|
const lines = [...changeLines, ...extraLines].map(l => truncateToWidth(l, contentWidth, Ellipsis.Omit));
|
|
326
281
|
while (lines.length > 0 && lines[0].trim() === "") lines.shift();
|
|
327
282
|
bodyMemo = { expanded, width: contentWidth, lines };
|