@oh-my-pi/pi-tui 18.4.4 → 18.4.6
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 +46 -0
- package/dist/types/apps/git/sidebar.d.ts +2 -2
- package/dist/types/chat/recap-notice.d.ts +13 -0
- package/dist/types/components/editor.d.ts +15 -3
- package/dist/types/components/input.d.ts +11 -1
- package/dist/types/native/blobs.d.ts +1 -1
- package/dist/types/native/node.d.ts +63 -1
- package/dist/types/overlays/agent-hub-projection.d.ts +9 -0
- package/dist/types/overlays/ask-dialog.d.ts +9 -5
- package/dist/types/overlays/copy-selector.d.ts +0 -23
- package/dist/types/overlays/error-banner.d.ts +5 -2
- package/dist/types/overlays/hook-editor.d.ts +12 -0
- package/dist/types/overlays/jobs-panel.d.ts +25 -1
- package/dist/types/overlays/model-hub.d.ts +2 -0
- package/dist/types/overlays/rewind-selector.d.ts +17 -4
- package/dist/types/prompt/custom-editor.d.ts +16 -8
- package/dist/types/prompt/welcome.d.ts +3 -3
- package/dist/types/render/render-utils.d.ts +5 -0
- package/dist/types/status-line/component.d.ts +2 -0
- package/dist/types/status-line/host.d.ts +2 -0
- package/dist/types/status-line/metrics.d.ts +4 -0
- package/dist/types/status-line/types.d.ts +13 -2
- package/dist/types/tools/edit.d.ts +2 -0
- package/dist/types/tools/task.d.ts +7 -0
- package/dist/types/tui.d.ts +8 -1
- package/package.json +9 -9
- package/src/apps/git/sidebar.ts +15 -2
- package/src/chat/assistant-message.ts +2 -2
- package/src/chat/recap-notice.ts +30 -0
- package/src/chat/tool-execution.ts +19 -3
- package/src/chat/user-message.ts +2 -1
- package/src/components/editor.ts +115 -16
- package/src/components/input.ts +57 -1
- package/src/native/backend.ts +28 -10
- package/src/native/blobs.ts +1 -1
- package/src/native/encode.ts +1 -0
- package/src/native/node.ts +108 -1
- package/src/native/reconcile.ts +5 -4
- package/src/overlays/agent-hub-projection.ts +25 -0
- package/src/overlays/agent-hub.ts +4 -15
- package/src/overlays/ask-dialog.ts +20 -32
- package/src/overlays/copy-selector.ts +6 -6
- package/src/overlays/error-banner.ts +14 -3
- package/src/overlays/hook-editor.ts +59 -6
- package/src/overlays/jobs-panel.ts +132 -49
- package/src/overlays/model-browser.ts +56 -12
- package/src/overlays/model-hub.ts +82 -29
- package/src/overlays/rewind-selector.ts +235 -332
- package/src/overlays/session-selector.ts +19 -20
- package/src/prompt/custom-editor.ts +119 -30
- package/src/prompt/welcome.ts +4 -33
- package/src/render/render-utils.ts +51 -3
- package/src/status-line/component.ts +61 -30
- package/src/status-line/host.ts +2 -0
- package/src/status-line/metrics.ts +36 -5
- package/src/status-line/segments.ts +9 -1
- package/src/status-line/types.ts +13 -2
- package/src/terminal.ts +27 -6
- package/src/tools/edit.ts +23 -0
- package/src/tools/eval.ts +43 -11
- package/src/tools/grouped-file-output.ts +6 -0
- package/src/tools/task.ts +29 -4
- package/src/tui.ts +9 -1
|
@@ -1376,7 +1376,8 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
|
|
|
1376
1376
|
/** The native picker shows `#message`'s error until the next key or pointer event. */
|
|
1377
1377
|
#pickerErrorOpen = false;
|
|
1378
1378
|
readonly #standalone: boolean;
|
|
1379
|
-
|
|
1379
|
+
/** A caller's heading for the picker head; the default `/resume` picker has none (the search names it). */
|
|
1380
|
+
readonly #pickerTitle: string | undefined;
|
|
1380
1381
|
/** The open delete confirmation's two answers, for the picker's confirm strip. */
|
|
1381
1382
|
#deleteChoice: DeleteChoice<T> | null = null;
|
|
1382
1383
|
/** The preview pane's content and the session it shows; follows the selection once it settles. */
|
|
@@ -1423,7 +1424,7 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
|
|
|
1423
1424
|
this.#getTerminalRows = options.getTerminalRows ?? (() => 24);
|
|
1424
1425
|
this.#fillHeight = options.fillHeight ?? false;
|
|
1425
1426
|
this.#title = options.title ?? "Resume Session";
|
|
1426
|
-
this.#pickerTitle = options.title
|
|
1427
|
+
this.#pickerTitle = options.title;
|
|
1427
1428
|
this.#standalone = options.standalone ?? false;
|
|
1428
1429
|
this.#scopeLabel = options.scopeLabel;
|
|
1429
1430
|
this.title = this.#headerLabel();
|
|
@@ -1709,10 +1710,11 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
|
|
|
1709
1710
|
}
|
|
1710
1711
|
|
|
1711
1712
|
/**
|
|
1712
|
-
* `lg` cards sheet (`screen` for the standalone app):
|
|
1713
|
-
*
|
|
1714
|
-
*
|
|
1715
|
-
*
|
|
1713
|
+
* `lg` cards sheet (`screen` for the standalone app): sessions grouped by
|
|
1714
|
+
* day (ranked flat while searching), the selected session's preview, the
|
|
1715
|
+
* delete confirm strip, and the actions of the keys Enter, Delete/Backspace,
|
|
1716
|
+
* Tab (the scope toggle, labelled with the scope it switches to) and Esc.
|
|
1717
|
+
* Untitled unless the caller named it; the placeholder says the scope.
|
|
1716
1718
|
*/
|
|
1717
1719
|
#describePicker(): NativeNode {
|
|
1718
1720
|
const list = this.#sessionList;
|
|
@@ -1742,11 +1744,17 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
|
|
|
1742
1744
|
actions.push(
|
|
1743
1745
|
pickerAction("close", "Close", boundKeys("app.interrupt", ["escape"])[0] ?? "escape", { end: true }),
|
|
1744
1746
|
);
|
|
1745
|
-
const
|
|
1747
|
+
const folder = this.#scopeLabel === false ? undefined : (this.#scopeLabel ?? path.basename(getProjectDir()));
|
|
1748
|
+
const title = this.#pickerTitle;
|
|
1749
|
+
const placeholder =
|
|
1750
|
+
loading || this.#scope === "all"
|
|
1751
|
+
? "Search all sessions…"
|
|
1752
|
+
: folder && !title
|
|
1753
|
+
? `Search sessions in ${folder}…`
|
|
1754
|
+
: "Search sessions…";
|
|
1746
1755
|
const result = picker(
|
|
1747
1756
|
{
|
|
1748
|
-
title:
|
|
1749
|
-
...(subtitle ? { subtitle } : {}),
|
|
1757
|
+
...(title ? { title, ...(folder ? { subtitle: folder } : {}) } : {}),
|
|
1750
1758
|
icon: "history",
|
|
1751
1759
|
noun: "sessions",
|
|
1752
1760
|
size: this.#standalone ? "screen" : "lg",
|
|
@@ -1754,16 +1762,7 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
|
|
|
1754
1762
|
preview: "side",
|
|
1755
1763
|
query: view.query,
|
|
1756
1764
|
cursor: view.cursor,
|
|
1757
|
-
placeholder
|
|
1758
|
-
...(this.#hasScopeTabs()
|
|
1759
|
-
? {
|
|
1760
|
-
tabs: [
|
|
1761
|
-
{ id: "folder", label: "This folder" },
|
|
1762
|
-
{ id: "all", label: "All projects" },
|
|
1763
|
-
],
|
|
1764
|
-
tab: loading ? "all" : this.#scope,
|
|
1765
|
-
}
|
|
1766
|
-
: {}),
|
|
1765
|
+
placeholder,
|
|
1767
1766
|
columns: [
|
|
1768
1767
|
{ id: "when", format: "time" },
|
|
1769
1768
|
{ id: "size", format: "dim" },
|
|
@@ -1846,7 +1845,7 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
|
|
|
1846
1845
|
} else if (choice) return;
|
|
1847
1846
|
else if (ev.act === "resume") list.resumeSelected();
|
|
1848
1847
|
else if (ev.act === "delete") list.requestDelete();
|
|
1849
|
-
else if (ev.act === "scope"
|
|
1848
|
+
else if (ev.act === "scope") {
|
|
1850
1849
|
list.onToggleScope?.();
|
|
1851
1850
|
} else if (ev.act === "clear") list.clearSearch();
|
|
1852
1851
|
}
|
|
@@ -14,6 +14,7 @@ import { SpaceHoldGesture } from "../space-hold";
|
|
|
14
14
|
import { type Component, TUI } from "../tui";
|
|
15
15
|
import type { AppKeybinding } from "../app-keybindings";
|
|
16
16
|
import { formatKeyHint } from "../key-hint-format";
|
|
17
|
+
import { MAIN_AGENT_ID } from "../overlays/agent-hub-types";
|
|
17
18
|
import { compact, keyed, node, row, span } from "../native/describe";
|
|
18
19
|
import type { DescribeContext, NativeNode, NativeUiEvent } from "../native/node";
|
|
19
20
|
import type { ComposerFacts, ComposerFactsSource } from "../status-line/types";
|
|
@@ -60,11 +61,19 @@ export interface ComposerNativeState {
|
|
|
60
61
|
readonly thinking?: string;
|
|
61
62
|
/** A turn is running: the send keycap becomes a Stop button. */
|
|
62
63
|
readonly running: boolean;
|
|
64
|
+
/** The viewed subagent's lineage, outermost first and the viewed agent last; undefined on the main session. */
|
|
65
|
+
readonly viewing?: readonly string[];
|
|
63
66
|
}
|
|
64
67
|
|
|
68
|
+
/** Action code prefix of the viewing header's links: `focus:<agent id>`, {@link MAIN_AGENT_ID} for the main session. */
|
|
69
|
+
const FOCUS_ACTION = "focus:";
|
|
70
|
+
|
|
65
71
|
const IDLE_COMPOSER: ComposerNativeState = { running: false };
|
|
66
72
|
|
|
67
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* Filled steps (of four) of the effort chip's fallback meter, for terminals without the `effort`
|
|
75
|
+
* kind; `auto` before it resolves has none known.
|
|
76
|
+
*/
|
|
68
77
|
const EFFORT_STEPS: Partial<Record<string, number>> = {
|
|
69
78
|
off: 0,
|
|
70
79
|
minimal: 1,
|
|
@@ -932,7 +941,7 @@ export class CustomEditor extends Editor {
|
|
|
932
941
|
}
|
|
933
942
|
}
|
|
934
943
|
for (const typo of this.#spelling.typoRanges(value, { editorText: text, lines, line, startCol: 0 })) {
|
|
935
|
-
decor.push({ from: lineStart + typo.start, to: lineStart + typo.start + typo.length, s: "
|
|
944
|
+
decor.push({ from: lineStart + typo.start, to: lineStart + typo.start + typo.length, s: "typo" });
|
|
936
945
|
}
|
|
937
946
|
lineStart += value.length + 1;
|
|
938
947
|
}
|
|
@@ -986,7 +995,11 @@ export class CustomEditor extends Editor {
|
|
|
986
995
|
}, CustomEditor.SHIMMER_FRAME_MS);
|
|
987
996
|
this.#shimmerTimer.unref?.();
|
|
988
997
|
}
|
|
989
|
-
|
|
998
|
+
/** Viewing a subagent, the draft goes to it: the placeholder names it. */
|
|
999
|
+
override describePlaceholder = (): string => {
|
|
1000
|
+
const agent = this.composerState().viewing?.at(-1);
|
|
1001
|
+
return agent === undefined ? NATIVE_COMPOSER_PLACEHOLDER : `Message ${agent}`;
|
|
1002
|
+
};
|
|
990
1003
|
/** A shell-mode draft highlights as its language. */
|
|
991
1004
|
override describeLanguage = (): string | undefined => this.composerState().shell?.kind;
|
|
992
1005
|
/** Host-owned live state for the TSP composer (shell mode, effort chip, send/stop). */
|
|
@@ -1001,6 +1014,7 @@ export class CustomEditor extends Editor {
|
|
|
1001
1014
|
facts: ComposerFacts | undefined;
|
|
1002
1015
|
chips: Component | undefined;
|
|
1003
1016
|
input: NativeNode;
|
|
1017
|
+
focus: NativeNode | undefined;
|
|
1004
1018
|
mode: NativeNode | undefined;
|
|
1005
1019
|
bar: NativeNode;
|
|
1006
1020
|
layout: NativeEditorLayout;
|
|
@@ -1009,13 +1023,15 @@ export class CustomEditor extends Editor {
|
|
|
1009
1023
|
|
|
1010
1024
|
/**
|
|
1011
1025
|
* The TSP composer: role `omp.editor[.bash|.python]` (tone `pending` while
|
|
1012
|
-
* a turn runs) over the context hairline, the
|
|
1013
|
-
*
|
|
1014
|
-
*
|
|
1015
|
-
*
|
|
1016
|
-
*
|
|
1026
|
+
* a turn runs) over the context hairline, the viewing header while a
|
|
1027
|
+
* subagent is focused, the attachment chips, a `line` row of the
|
|
1028
|
+
* shell-mode chip and the input, and the `bar`: model chip, effort chip,
|
|
1029
|
+
* the other status facts, usage, then send (Stop while a turn runs).
|
|
1030
|
+
* Clicks come back as `status.model`, `thinking.cycle`, `submit`,
|
|
1031
|
+
* `interrupt` and `focus:<id>` actions.
|
|
1017
1032
|
*/
|
|
1018
|
-
override describeLayout = (input: NativeNode,
|
|
1033
|
+
override describeLayout = (input: NativeNode, cx: DescribeContext): NativeEditorLayout => {
|
|
1034
|
+
const effortGlyph = cx.supports("effort");
|
|
1019
1035
|
const state = this.composerState();
|
|
1020
1036
|
const shell = state.shell;
|
|
1021
1037
|
const facts = this.composerFacts?.describeComposerFacts();
|
|
@@ -1030,36 +1046,98 @@ export class CustomEditor extends Editor {
|
|
|
1030
1046
|
thinkingKey,
|
|
1031
1047
|
modelKey,
|
|
1032
1048
|
interruptKey,
|
|
1049
|
+
state.viewing?.join("\u0001"),
|
|
1050
|
+
effortGlyph,
|
|
1033
1051
|
].join("\0");
|
|
1034
1052
|
const chips = this.attachmentChips;
|
|
1035
1053
|
const memo = this.#nativeComposer;
|
|
1036
1054
|
if (memo && memo.key === key && memo.facts === facts && memo.input === input && memo.chips === chips) {
|
|
1037
1055
|
return memo.layout;
|
|
1038
1056
|
}
|
|
1039
|
-
const { mode, bar } =
|
|
1057
|
+
const { focus, mode, bar } =
|
|
1040
1058
|
memo?.key === key && memo.facts === facts
|
|
1041
1059
|
? memo
|
|
1042
|
-
: this.#describeComposerControls(state, facts, thinkingKey, modelKey, interruptKey);
|
|
1060
|
+
: this.#describeComposerControls(state, facts, thinkingKey, modelKey, interruptKey, effortGlyph);
|
|
1043
1061
|
const line = keyed(row(compact([mode, input]), { role: "omp.composer.line", align: "start", gap: "sm" }), "line");
|
|
1044
1062
|
const layout: NativeEditorLayout = {
|
|
1045
1063
|
role: shell ? `omp.editor.${shell.kind}` : "omp.editor",
|
|
1046
1064
|
tone: state.running ? "pending" : undefined,
|
|
1047
|
-
children: compact([facts?.context, chips, line, bar]),
|
|
1065
|
+
children: compact([facts?.context, focus, chips, line, bar]),
|
|
1048
1066
|
caret: "line/input",
|
|
1049
1067
|
};
|
|
1050
|
-
this.#nativeComposer = { key, facts, chips, input, mode, bar, layout };
|
|
1068
|
+
this.#nativeComposer = { key, facts, chips, input, focus, mode, bar, layout };
|
|
1051
1069
|
return layout;
|
|
1052
1070
|
};
|
|
1053
1071
|
|
|
1054
|
-
/**
|
|
1072
|
+
/**
|
|
1073
|
+
* The viewing header (`omp.composer.focus`) while a subagent is focused:
|
|
1074
|
+
* an eye, the agent's ancestors as `omp.composer.crumb` links, the agent
|
|
1075
|
+
* itself (`omp.composer.agent`), then the way back to the main session
|
|
1076
|
+
* (`omp.composer.exit`, the interrupt key's keycap: Esc on an empty draft).
|
|
1077
|
+
*/
|
|
1078
|
+
#describeViewing(viewing: readonly string[], interruptKey: KeyId): NativeNode | undefined {
|
|
1079
|
+
const agent = viewing.at(-1);
|
|
1080
|
+
if (agent === undefined) return undefined;
|
|
1081
|
+
const back = interruptKey === "escape" ? "esc" : formatKeyHint(interruptKey);
|
|
1082
|
+
const crumbs = viewing.slice(0, -1).map(id =>
|
|
1083
|
+
node(
|
|
1084
|
+
"text",
|
|
1085
|
+
{
|
|
1086
|
+
role: "omp.composer.crumb",
|
|
1087
|
+
text: id,
|
|
1088
|
+
wrap: "none",
|
|
1089
|
+
title: `View ${id}`,
|
|
1090
|
+
actions: { click: `${FOCUS_ACTION}${id}` },
|
|
1091
|
+
},
|
|
1092
|
+
undefined,
|
|
1093
|
+
`crumb:${id}`,
|
|
1094
|
+
),
|
|
1095
|
+
);
|
|
1096
|
+
return keyed(
|
|
1097
|
+
row(
|
|
1098
|
+
[
|
|
1099
|
+
node("icon", { name: "eye" }, undefined, "icon"),
|
|
1100
|
+
node("text", { text: "Viewing", wrap: "none" }, undefined, "label"),
|
|
1101
|
+
...crumbs,
|
|
1102
|
+
node("text", { role: "omp.composer.agent", text: agent, wrap: "none" }, undefined, "agent"),
|
|
1103
|
+
node(
|
|
1104
|
+
"row",
|
|
1105
|
+
{
|
|
1106
|
+
role: "omp.composer.exit",
|
|
1107
|
+
gap: "xs",
|
|
1108
|
+
align: "center",
|
|
1109
|
+
title: `Back to the main session ${back}`,
|
|
1110
|
+
actions: { click: `${FOCUS_ACTION}${MAIN_AGENT_ID}` },
|
|
1111
|
+
},
|
|
1112
|
+
[
|
|
1113
|
+
node("kbd", { keys: [interruptKey] }, undefined, "key"),
|
|
1114
|
+
node("text", { text: "main", wrap: "none" }, undefined, "label"),
|
|
1115
|
+
],
|
|
1116
|
+
"exit",
|
|
1117
|
+
),
|
|
1118
|
+
],
|
|
1119
|
+
{
|
|
1120
|
+
role: "omp.composer.focus",
|
|
1121
|
+
gap: "xs",
|
|
1122
|
+
align: "center",
|
|
1123
|
+
title: `Viewing subagent ${agent}: what you send goes to it`,
|
|
1124
|
+
},
|
|
1125
|
+
),
|
|
1126
|
+
"focus",
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
/** The viewing header over the text, the shell-mode chip before the input, and the bar under it. */
|
|
1055
1131
|
#describeComposerControls(
|
|
1056
1132
|
state: ComposerNativeState,
|
|
1057
1133
|
facts: ComposerFacts | undefined,
|
|
1058
1134
|
thinkingKey: KeyId | undefined,
|
|
1059
1135
|
modelKey: KeyId | undefined,
|
|
1060
1136
|
interruptKey: KeyId,
|
|
1061
|
-
|
|
1137
|
+
effortGlyph: boolean,
|
|
1138
|
+
): { focus: NativeNode | undefined; mode: NativeNode | undefined; bar: NativeNode } {
|
|
1062
1139
|
const shell = state.shell;
|
|
1140
|
+
const focus = state.viewing && this.#describeViewing(state.viewing, interruptKey);
|
|
1063
1141
|
const model =
|
|
1064
1142
|
facts &&
|
|
1065
1143
|
node(
|
|
@@ -1079,9 +1157,10 @@ export class CustomEditor extends Editor {
|
|
|
1079
1157
|
],
|
|
1080
1158
|
"model",
|
|
1081
1159
|
);
|
|
1082
|
-
const
|
|
1160
|
+
const thinking = state.thinking;
|
|
1161
|
+
const effortSteps = thinking === undefined ? undefined : EFFORT_STEPS[thinking];
|
|
1083
1162
|
const effort =
|
|
1084
|
-
|
|
1163
|
+
thinking !== undefined &&
|
|
1085
1164
|
node(
|
|
1086
1165
|
"row",
|
|
1087
1166
|
{
|
|
@@ -1092,13 +1171,15 @@ export class CustomEditor extends Editor {
|
|
|
1092
1171
|
actions: { click: "thinking.cycle" },
|
|
1093
1172
|
},
|
|
1094
1173
|
[
|
|
1095
|
-
|
|
1096
|
-
"
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1174
|
+
effortGlyph
|
|
1175
|
+
? node("effort", { level: thinking }, undefined, "glyph")
|
|
1176
|
+
: node(
|
|
1177
|
+
"meter",
|
|
1178
|
+
{ value: effortSteps === undefined ? null : effortSteps / 4, style: "blocks", steps: 4 },
|
|
1179
|
+
undefined,
|
|
1180
|
+
"meter",
|
|
1181
|
+
),
|
|
1182
|
+
node("text", { text: thinking, wrap: "none" }, undefined, "level"),
|
|
1102
1183
|
],
|
|
1103
1184
|
"effort",
|
|
1104
1185
|
);
|
|
@@ -1135,7 +1216,7 @@ export class CustomEditor extends Editor {
|
|
|
1135
1216
|
}),
|
|
1136
1217
|
"bar",
|
|
1137
1218
|
);
|
|
1138
|
-
if (!shell) return { mode: undefined, bar };
|
|
1219
|
+
if (!shell) return { focus, mode: undefined, bar };
|
|
1139
1220
|
const runs = shell.kind === "bash" ? "Runs in your shell" : "Runs in Python";
|
|
1140
1221
|
const mode = keyed(
|
|
1141
1222
|
row(
|
|
@@ -1158,15 +1239,20 @@ export class CustomEditor extends Editor {
|
|
|
1158
1239
|
),
|
|
1159
1240
|
"mode",
|
|
1160
1241
|
);
|
|
1161
|
-
return { mode, bar };
|
|
1242
|
+
return { focus, mode, bar };
|
|
1162
1243
|
}
|
|
1163
1244
|
|
|
1164
1245
|
/**
|
|
1165
1246
|
* Clicks on the composer's controls take the same paths as their keys: ⇧⇥,
|
|
1166
|
-
* ⏎ and Esc; the
|
|
1247
|
+
* ⏎ and Esc; the viewing header's links (`focus:<id>`) go to the host,
|
|
1248
|
+
* the status facts' clicks (`status.*`) to their source. Selection edits
|
|
1249
|
+
* go to the buffer.
|
|
1167
1250
|
*/
|
|
1168
|
-
handleNativeEvent(event: NativeUiEvent): void {
|
|
1169
|
-
if (event.type !== "action")
|
|
1251
|
+
override handleNativeEvent(event: NativeUiEvent): void {
|
|
1252
|
+
if (event.type !== "action") {
|
|
1253
|
+
super.handleNativeEvent(event);
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1170
1256
|
switch (event.act) {
|
|
1171
1257
|
case "thinking.cycle":
|
|
1172
1258
|
this.onCycleThinkingLevel?.();
|
|
@@ -1178,7 +1264,8 @@ export class CustomEditor extends Editor {
|
|
|
1178
1264
|
this.onEscape?.();
|
|
1179
1265
|
return;
|
|
1180
1266
|
default:
|
|
1181
|
-
this.
|
|
1267
|
+
if (event.act.startsWith(FOCUS_ACTION)) this.onFocusAgent?.(event.act.slice(FOCUS_ACTION.length));
|
|
1268
|
+
else this.composerFacts?.handleNativeEvent(event);
|
|
1182
1269
|
}
|
|
1183
1270
|
}
|
|
1184
1271
|
|
|
@@ -1208,6 +1295,8 @@ export class CustomEditor extends Editor {
|
|
|
1208
1295
|
onCapsLock?: () => void;
|
|
1209
1296
|
/** Called when left-arrow is pressed while the editor is empty (cursor necessarily at start). */
|
|
1210
1297
|
onLeftAtStart?: () => void;
|
|
1298
|
+
/** Called when the viewing header asks to view agent `id` ({@link MAIN_AGENT_ID}: the main session). */
|
|
1299
|
+
onFocusAgent?: (id: string) => void;
|
|
1211
1300
|
|
|
1212
1301
|
/** Space-bar push-to-talk; set its `handler` to enable it. It is a text-composition gesture, so it
|
|
1213
1302
|
* stays out of Vim's Normal/Visual modes (where the space bar is the `l` motion) and away from an
|
package/src/prompt/welcome.ts
CHANGED
|
@@ -3,7 +3,6 @@ import type { TspSpan } from "@oh-my-pi/pi-wire";
|
|
|
3
3
|
import { formatDoubleTap, formatKeyHint, formatKeyHints, type KeyName } from "../app-keybindings";
|
|
4
4
|
import { editorKey } from "../chrome/keybinding-hints";
|
|
5
5
|
import { getKeybindings, type Keybinding } from "../keybindings";
|
|
6
|
-
import { registerNativeBlob } from "../native/blobs";
|
|
7
6
|
import { card, col, kbd, keyed, node, row, span, text } from "../native/describe";
|
|
8
7
|
import type { DescribeContext, NativeChild, NativeNode, NativeUiEvent } from "../native/node";
|
|
9
8
|
import { runTranscriptAction } from "../chat/transcript-actions";
|
|
@@ -221,9 +220,9 @@ export class WelcomeComponent implements Component {
|
|
|
221
220
|
|
|
222
221
|
/**
|
|
223
222
|
* A `card` (`omp.welcome`) titled with the app version. The brand column
|
|
224
|
-
* (`omp.welcome.brand`: greeting
|
|
225
|
-
* beside the info column (`omp.welcome.info`: prompt-sigil
|
|
226
|
-
* servers, recent sessions); the tip of the session closes the card. Roles
|
|
223
|
+
* (`omp.welcome.brand`: greeting and the terminal's builtin `omp` mark, which
|
|
224
|
+
* it animates) sits beside the info column (`omp.welcome.info`: prompt-sigil
|
|
225
|
+
* keycaps, LSP servers, recent sessions); the tip of the session closes the card. Roles
|
|
227
226
|
* carry the look (gradient logo, type scale, column hairline); a "[NEW]" tip
|
|
228
227
|
* carries a terminal-clocked shimmering tag.
|
|
229
228
|
*/
|
|
@@ -242,7 +241,7 @@ export class WelcomeComponent implements Component {
|
|
|
242
241
|
node(
|
|
243
242
|
"image",
|
|
244
243
|
{
|
|
245
|
-
|
|
244
|
+
builtin: "omp",
|
|
246
245
|
alt: APP_NAME,
|
|
247
246
|
w: 128,
|
|
248
247
|
role: "omp.welcome.logo",
|
|
@@ -250,8 +249,6 @@ export class WelcomeComponent implements Component {
|
|
|
250
249
|
undefined,
|
|
251
250
|
"logo",
|
|
252
251
|
),
|
|
253
|
-
art([span(plainLine(this.modelName), "accent")], "omp.welcome.model"),
|
|
254
|
-
art([span(plainLine(this.providerName), "muted")], "omp.welcome.provider"),
|
|
255
252
|
],
|
|
256
253
|
{ align: "center", role: "omp.welcome.brand" },
|
|
257
254
|
),
|
|
@@ -682,32 +679,6 @@ export class WelcomeComponent implements Component {
|
|
|
682
679
|
}
|
|
683
680
|
}
|
|
684
681
|
|
|
685
|
-
/**
|
|
686
|
-
* {@link PI_LOGO} as SVG for the native welcome, on the terminal's grid: a
|
|
687
|
-
* cell is 3×6 units, so the 12×5-cell art spans 36×30 from (14,16). The left
|
|
688
|
-
* leg's `▒▒` tail is a half-opacity cell; the gradient spans the whole art in
|
|
689
|
-
* user space (per-axis normalized, like {@link gradientLogo}) so the tail
|
|
690
|
-
* keeps its colour. Tern mounts SVG blobs as live DOM, so the classes are
|
|
691
|
-
* animation hooks: `trace` (the outline, `pathLength=1` for a draw-on), `mark`
|
|
692
|
-
* (the fills) and the gradient stops `s0`–`s2`.
|
|
693
|
-
*/
|
|
694
|
-
const WELCOME_LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="10 12 44 38">
|
|
695
|
-
<defs><linearGradient id="g" gradientUnits="userSpaceOnUse" gradientTransform="matrix(36 0 0 30 14 16)" x1="0" y1="0" x2="1" y2="1">
|
|
696
|
-
<stop class="s0" offset="0" stop-color="#ed4abf"/><stop class="s1" offset=".5" stop-color="#9b4dff"/><stop class="s2" offset="1" stop-color="#5ad8e6"/>
|
|
697
|
-
</linearGradient></defs>
|
|
698
|
-
<path class="mark" fill="url(#g)" d="M14 16h36v6h-9v24h-6V22h-6v12h-6V22h-9z"/>
|
|
699
|
-
<rect class="mark" fill="url(#g)" opacity=".5" x="23" y="34" width="6" height="6"/>
|
|
700
|
-
<path class="trace" fill="none" stroke="url(#g)" stroke-width="1" stroke-linejoin="round" pathLength="1" d="M14 16h36v6h-9v24h-6V22h-6v18h-6V22h-9z"/>
|
|
701
|
-
</svg>`;
|
|
702
|
-
|
|
703
|
-
let welcomeLogoId: string | undefined;
|
|
704
|
-
|
|
705
|
-
/** The registered blob id of {@link WELCOME_LOGO_SVG}. */
|
|
706
|
-
function welcomeLogoBlob(): string {
|
|
707
|
-
welcomeLogoId ??= registerNativeBlob(new TextEncoder().encode(WELCOME_LOGO_SVG), "image/svg+xml");
|
|
708
|
-
return welcomeLogoId;
|
|
709
|
-
}
|
|
710
|
-
|
|
711
682
|
/** Block-grid brand mark shared by the welcome and setup surfaces. */
|
|
712
683
|
export const PI_LOGO = ["████████████", " ██ ██ ", " ██ ██ ", " ▒▒ ██ ", " ██ "];
|
|
713
684
|
|
|
@@ -909,10 +909,16 @@ function homePatternFor(homeDir: string, windowsStyle: boolean): HomePattern {
|
|
|
909
909
|
})
|
|
910
910
|
.join("[\\\\/]");
|
|
911
911
|
}
|
|
912
|
+
// `*` and `_` are also identifier and glob characters (`/home/me_old`, `src/**/home/me/**`), so they
|
|
913
|
+
// bound the home directory only as Markdown emphasis: opened at start or after whitespace, and
|
|
914
|
+
// closed before end, whitespace, or punctuation.
|
|
915
|
+
const end = `[\\\\/\\s"'\\x60)\\]},;:<>&|]`;
|
|
912
916
|
pattern = {
|
|
913
917
|
leading: new RegExp(`^${escapedHome}(?=$|[\\\\/])`, windowsStyle ? "i" : ""),
|
|
914
918
|
embedded: new RegExp(
|
|
915
|
-
`[a-zA-Z][a-zA-Z0-9+.-]*://[^\\s"'<>]
|
|
919
|
+
`[a-zA-Z][a-zA-Z0-9+.-]*://[^\\s"'<>]+` +
|
|
920
|
+
`|(^|[\\s"'\\x60([{=,:<>&|])(${escapedHome})(?=$|${end})` +
|
|
921
|
+
`|((?:^|\\s)[*_]{1,3})(${escapedHome})(?=$|${end}|[*_]{1,3}(?=$|[\\s.,;:!?)\\]}"'\\x60]))`,
|
|
916
922
|
windowsStyle ? "gi" : "g",
|
|
917
923
|
),
|
|
918
924
|
};
|
|
@@ -946,8 +952,13 @@ export function shortenEmbeddedPaths(text: string, homeDir?: string, preserveSep
|
|
|
946
952
|
const homePattern = homePatternFor(resolvedHome, windowsStyle);
|
|
947
953
|
const textWithShortenedHome = text.replace(
|
|
948
954
|
homePattern.embedded,
|
|
949
|
-
(
|
|
950
|
-
|
|
955
|
+
(
|
|
956
|
+
match,
|
|
957
|
+
boundary: string | undefined,
|
|
958
|
+
candidate: string | undefined,
|
|
959
|
+
emphasis: string | undefined,
|
|
960
|
+
emphasized: string | undefined,
|
|
961
|
+
) => (candidate !== undefined ? `${boundary}~` : emphasized !== undefined ? `${emphasis}~` : match),
|
|
951
962
|
);
|
|
952
963
|
if (preserveSeparators) return textWithShortenedHome;
|
|
953
964
|
return textWithShortenedHome
|
|
@@ -966,6 +977,43 @@ export function shortenEmbeddedPaths(text: string, homeDir?: string, preserveSep
|
|
|
966
977
|
.join(" ");
|
|
967
978
|
}
|
|
968
979
|
|
|
980
|
+
/**
|
|
981
|
+
* Shorten filesystem and command arguments without rewriting literal search patterns. A producer that
|
|
982
|
+
* names no argument key gets the general embedded-path shortening.
|
|
983
|
+
*/
|
|
984
|
+
export function shortenToolArgumentPaths(text: string, key: string | undefined, homeDir?: string): string {
|
|
985
|
+
if (key === "url") {
|
|
986
|
+
try {
|
|
987
|
+
const url = new URL(text);
|
|
988
|
+
if (url.protocol === "file:") {
|
|
989
|
+
let decodedPath = url.pathname;
|
|
990
|
+
try {
|
|
991
|
+
decodedPath = decodeURIComponent(decodedPath);
|
|
992
|
+
} catch {
|
|
993
|
+
/* Retain malformed percent escapes as literal path bytes. */
|
|
994
|
+
}
|
|
995
|
+
const filePath = url.hostname
|
|
996
|
+
? `//${url.hostname}${decodedPath}`
|
|
997
|
+
: /^[A-Za-z]:$/.test(decodedPath.slice(1, 3))
|
|
998
|
+
? decodedPath.slice(1)
|
|
999
|
+
: decodedPath;
|
|
1000
|
+
return shortenEmbeddedPaths(filePath, homeDir);
|
|
1001
|
+
}
|
|
1002
|
+
} catch {
|
|
1003
|
+
// Preserve malformed and non-file URL arguments verbatim.
|
|
1004
|
+
}
|
|
1005
|
+
return text;
|
|
1006
|
+
}
|
|
1007
|
+
return key === undefined ||
|
|
1008
|
+
key === "path" ||
|
|
1009
|
+
key === "file_path" ||
|
|
1010
|
+
key === "command" ||
|
|
1011
|
+
key === "task" ||
|
|
1012
|
+
key === "prompt"
|
|
1013
|
+
? shortenEmbeddedPaths(text, homeDir)
|
|
1014
|
+
: text;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
969
1017
|
/** Sanitize warning text before showing it in TUI, including embedded home paths. */
|
|
970
1018
|
export function sanitizeDisplayWarning(text: string): string {
|
|
971
1019
|
return shortenEmbeddedPaths(
|
|
@@ -39,10 +39,10 @@ import { canReuseCachedPr, createPrCacheContext, isSamePrCacheContext, type PrCa
|
|
|
39
39
|
import { summarizeUsageResetCredits } from "../overlays/usage-display";
|
|
40
40
|
import { getPreset } from "./presets";
|
|
41
41
|
import { describeSegment, renderSegment, type SegmentContext } from "./segments";
|
|
42
|
-
import type { TspProps } from "@oh-my-pi/pi-wire";
|
|
42
|
+
import type { TspMeterMark, TspProps } from "@oh-my-pi/pi-wire";
|
|
43
43
|
import type { NativeNode, NativeUiEvent } from "../native/node";
|
|
44
44
|
import { col, node, span } from "../native/describe";
|
|
45
|
-
import { getContextMeterThresholds
|
|
45
|
+
import { getContextMeterThresholds } from "../chrome/context-thresholds";
|
|
46
46
|
import { isNativeRendering } from "../native/state";
|
|
47
47
|
import { getSeparator } from "./separators";
|
|
48
48
|
import type {
|
|
@@ -81,10 +81,12 @@ const PINNED_NATIVE_PRIORITY = 1000;
|
|
|
81
81
|
/**
|
|
82
82
|
* Segments a TSP terminal shows outside the composer's facts: the model chip,
|
|
83
83
|
* the context hairline and usage text (context, cost), Tern's pane header
|
|
84
|
-
* (path, git), the tab title (session name, PR), the HUD pills (subagents)
|
|
85
|
-
*
|
|
84
|
+
* (path, git), the tab title (session name, PR), the HUD pills (subagents),
|
|
85
|
+
* the editor (vim) and the brand (`pi`; while focus-proxied, the viewed agent
|
|
86
|
+
* is the composer's viewing header).
|
|
86
87
|
*/
|
|
87
88
|
const COMPOSER_HOMED_SEGMENTS: Partial<Record<StatusLineSegmentId, true>> = {
|
|
89
|
+
pi: true,
|
|
88
90
|
model: true,
|
|
89
91
|
context_pct: true,
|
|
90
92
|
context_total: true,
|
|
@@ -642,6 +644,7 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
642
644
|
#sortedHookStatuses: readonly string[] = [];
|
|
643
645
|
#subagentCount: number = 0;
|
|
644
646
|
#runningSubagentIds = new Set<string>();
|
|
647
|
+
#subagentTreeCost = 0;
|
|
645
648
|
/**
|
|
646
649
|
* Active-processing accounting for the `time_spent` segment, keyed per
|
|
647
650
|
* {@link StatusLineSession} so the focus-controller mid-turn attach path
|
|
@@ -888,6 +891,14 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
888
891
|
this.#invalidateStatusLineRenderCache();
|
|
889
892
|
}
|
|
890
893
|
|
|
894
|
+
/** Host-computed spend of the main session's whole subagent tree (Agent Hub projection). */
|
|
895
|
+
setSubagentTreeCost(cost: number): void {
|
|
896
|
+
const next = Number.isFinite(cost) && cost > 0 ? cost : 0;
|
|
897
|
+
if (next === this.#subagentTreeCost) return;
|
|
898
|
+
this.#subagentTreeCost = next;
|
|
899
|
+
this.#invalidateStatusLineRenderCache();
|
|
900
|
+
}
|
|
901
|
+
|
|
891
902
|
/**
|
|
892
903
|
* Compatibility shim for callers predating the simplified subagent badge.
|
|
893
904
|
* The status line now intentionally shows only the active count.
|
|
@@ -2391,6 +2402,9 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
2391
2402
|
compactionSpeculation,
|
|
2392
2403
|
speculationBlinkOn: this.#speculationBlinkOn,
|
|
2393
2404
|
subagentCount: this.#subagentCount,
|
|
2405
|
+
// The tree total describes the main session; a focused subagent's
|
|
2406
|
+
// view falls back to its own completed task results.
|
|
2407
|
+
subagentTreeCost: this.#focusedAgentId ? 0 : this.#subagentTreeCost,
|
|
2394
2408
|
activeMs: this.getActiveMs(),
|
|
2395
2409
|
turnElapsedMs,
|
|
2396
2410
|
now: new Date(nowMs),
|
|
@@ -3367,7 +3381,7 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
3367
3381
|
const facts: NativeNode[] = [];
|
|
3368
3382
|
const collect = (side: "left" | "right", ids: readonly StatusLineSegmentId[]): void => {
|
|
3369
3383
|
ids.forEach((id, index) => {
|
|
3370
|
-
if (COMPOSER_HOMED_SEGMENTS[id]
|
|
3384
|
+
if (COMPOSER_HOMED_SEGMENTS[id]) return;
|
|
3371
3385
|
const view = describeSegment(id, ctx);
|
|
3372
3386
|
if (!view) return;
|
|
3373
3387
|
const props: TspProps<"seg"> = {
|
|
@@ -3397,48 +3411,65 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
3397
3411
|
window > 0 ? `${formatNumber(ctx.contextTokens)} of ${formatNumber(window)} tokens` : "No context window",
|
|
3398
3412
|
];
|
|
3399
3413
|
if (boundaries) lines.push(`Auto-compact at ${Math.round(boundaries.thresholdPercent)}%`);
|
|
3400
|
-
|
|
3401
|
-
|
|
3414
|
+
const speculation = ctx.compactionSpeculation;
|
|
3415
|
+
if (speculation === "running") lines.push("Compaction summary in progress");
|
|
3416
|
+
else if (speculation === "armed") lines.push("Compaction summary ready");
|
|
3417
|
+
// The line spans the whole window: omp's boundary symbols sit where speculation
|
|
3418
|
+
// starts and compaction fires, and the share past the speculation point is accent.
|
|
3419
|
+
const used = pct === null ? null : Math.min(1, pct / 100);
|
|
3420
|
+
const speculationAt =
|
|
3421
|
+
boundaries?.speculationPercent == null ? null : Math.min(1, boundaries.speculationPercent / 100);
|
|
3422
|
+
const marks: TspMeterMark[] = [];
|
|
3423
|
+
if (boundaries) {
|
|
3424
|
+
if (speculationAt !== null) {
|
|
3425
|
+
const start: TspMeterMark = {
|
|
3426
|
+
at: speculationAt,
|
|
3427
|
+
icon: "context.speculation",
|
|
3428
|
+
title: `Compaction summary starts at ${Math.round(speculationAt * 100)}%`,
|
|
3429
|
+
};
|
|
3430
|
+
// Lit while a background summary runs or waits armed.
|
|
3431
|
+
if (speculation !== "idle") start.tone = "accent";
|
|
3432
|
+
marks.push(start);
|
|
3433
|
+
}
|
|
3434
|
+
marks.push({
|
|
3435
|
+
at: Math.min(1, boundaries.thresholdPercent / 100),
|
|
3436
|
+
icon: "context.compaction",
|
|
3437
|
+
title: `Auto-compact at ${Math.round(boundaries.thresholdPercent)}%`,
|
|
3438
|
+
});
|
|
3439
|
+
}
|
|
3402
3440
|
const context = node(
|
|
3403
3441
|
"meter",
|
|
3404
3442
|
{
|
|
3405
3443
|
role: "omp.composer.context",
|
|
3406
|
-
value:
|
|
3444
|
+
value: used,
|
|
3407
3445
|
style: "bar",
|
|
3408
3446
|
thresholds: getContextMeterThresholds(window),
|
|
3409
|
-
|
|
3410
|
-
? [
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
]
|
|
3416
|
-
: undefined,
|
|
3447
|
+
...(used !== null && speculationAt !== null && used > speculationAt
|
|
3448
|
+
? { parts: [{ value: speculationAt }, { value: used - speculationAt, token: "accent" }] }
|
|
3449
|
+
: {}),
|
|
3450
|
+
...(marks.length > 0 ? { marks } : {}),
|
|
3451
|
+
...(pct === null ? {} : { label: `${Math.round(pct)}%` }),
|
|
3452
|
+
...(window > 0 ? { total: formatNumber(window) } : {}),
|
|
3417
3453
|
title: lines.join("\n"),
|
|
3454
|
+
actions: { click: "status.context" },
|
|
3418
3455
|
},
|
|
3419
3456
|
undefined,
|
|
3420
3457
|
"context",
|
|
3421
3458
|
);
|
|
3422
3459
|
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
: `${formatNumber(ctx.contextTokens)} tokens`;
|
|
3429
|
-
const cost = describeSegment("cost", ctx)
|
|
3430
|
-
?.spans.map(part => part.t)
|
|
3431
|
-
.join("");
|
|
3432
|
-
if (cost) lines.push(`Session cost ${cost}`);
|
|
3460
|
+
// The meter reads the context; the bar's usage is the session's cost.
|
|
3461
|
+
const cost =
|
|
3462
|
+
describeSegment("cost", ctx)
|
|
3463
|
+
?.spans.map(part => part.t)
|
|
3464
|
+
.join("") ?? "";
|
|
3433
3465
|
const usage = node(
|
|
3434
3466
|
"text",
|
|
3435
3467
|
{
|
|
3436
3468
|
role: "omp.composer.usage",
|
|
3437
|
-
|
|
3438
|
-
text: cost ? `${share} · ${cost}` : share,
|
|
3469
|
+
text: cost,
|
|
3439
3470
|
wrap: "none",
|
|
3440
|
-
title:
|
|
3441
|
-
actions: { click: "status.
|
|
3471
|
+
...(cost ? { title: `Session cost ${cost}` } : {}),
|
|
3472
|
+
actions: { click: "status.cost" },
|
|
3442
3473
|
},
|
|
3443
3474
|
undefined,
|
|
3444
3475
|
"usage",
|