shariq-pi-extensions 0.1.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/LICENSE +21 -0
- package/README.md +94 -0
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/docs/ARCHITECTURE.md +54 -0
- package/docs/DEVELOPMENT.md +88 -0
- package/docs/EXTENSIONS.md +89 -0
- package/extensions/antigravity-provider/LICENSE +21 -0
- package/extensions/antigravity-provider/README.md +23 -0
- package/extensions/antigravity-provider/antigravity/cloud-code-assist.ts +519 -0
- package/extensions/antigravity-provider/antigravity/index.ts +67 -0
- package/extensions/antigravity-provider/antigravity/models.ts +184 -0
- package/extensions/antigravity-provider/antigravity/oauth.ts +499 -0
- package/extensions/antigravity-provider/index.ts +3 -0
- package/extensions/ask-user/README.md +21 -0
- package/extensions/ask-user/index.ts +417 -0
- package/extensions/background-terminals/README.md +43 -0
- package/extensions/background-terminals/index.ts +400 -0
- package/extensions/background-terminals/src/manager.ts +480 -0
- package/extensions/background-terminals/src/output-buffer.ts +110 -0
- package/extensions/background-terminals/src/presentation.ts +74 -0
- package/extensions/background-terminals/src/types.ts +47 -0
- package/extensions/background-terminals/src/ui.ts +478 -0
- package/extensions/context-usage/LICENSE +21 -0
- package/extensions/context-usage/README.md +20 -0
- package/extensions/context-usage/index.ts +2006 -0
- package/extensions/context-usage/lib/ansi.ts +60 -0
- package/extensions/context-usage/lib/boundary.ts +27 -0
- package/extensions/context-usage/lib/chat.ts +190 -0
- package/extensions/context-usage/lib/config.ts +30 -0
- package/extensions/context-usage/lib/fmt.ts +59 -0
- package/extensions/context-usage/lib/style.ts +260 -0
- package/extensions/copy-all/README.md +15 -0
- package/extensions/copy-all/index.ts +72 -0
- package/extensions/factory-provider/API_KEYS.md +17 -0
- package/extensions/factory-provider/AUTH.md +15 -0
- package/extensions/factory-provider/README.md +90 -0
- package/extensions/factory-provider/REFRESH_MODELS.md +185 -0
- package/extensions/factory-provider/UPDATING.md +97 -0
- package/extensions/factory-provider/factory/api-keys.ts +407 -0
- package/extensions/factory-provider/factory/auth.ts +132 -0
- package/extensions/factory-provider/factory/constants.ts +15 -0
- package/extensions/factory-provider/factory/dashboard.ts +363 -0
- package/extensions/factory-provider/factory/droid.ts +84 -0
- package/extensions/factory-provider/factory/limits.ts +342 -0
- package/extensions/factory-provider/factory/models.ts +283 -0
- package/extensions/factory-provider/factory/responses.ts +331 -0
- package/extensions/factory-provider/factory/warm-theme.ts +53 -0
- package/extensions/factory-provider/factory/websocket.ts +130 -0
- package/extensions/factory-provider/index.ts +204 -0
- package/extensions/factory-provider/scripts/audit-reasoning-efforts.py +84 -0
- package/extensions/firecrawl-web/README.md +31 -0
- package/extensions/firecrawl-web/auth.ts +92 -0
- package/extensions/firecrawl-web/client.ts +238 -0
- package/extensions/firecrawl-web/index.ts +96 -0
- package/extensions/firecrawl-web/output.ts +125 -0
- package/extensions/git-info/README.md +25 -0
- package/extensions/git-info/index.ts +162 -0
- package/extensions/git-info/src/git.ts +206 -0
- package/extensions/git-info/src/ui.ts +291 -0
- package/extensions/goal/README.md +44 -0
- package/extensions/goal/constants.ts +4 -0
- package/extensions/goal/extension.ts +1064 -0
- package/extensions/goal/index.ts +2 -0
- package/extensions/goal/types.ts +56 -0
- package/extensions/goal/ui.ts +334 -0
- package/extensions/orchestration/README.md +58 -0
- package/extensions/orchestration/dashboard.ts +192 -0
- package/extensions/orchestration/engine.ts +706 -0
- package/extensions/orchestration/index.ts +243 -0
- package/extensions/orchestration/prompts.ts +124 -0
- package/extensions/orchestration/protocol.ts +116 -0
- package/extensions/orchestration/settings-ui.ts +41 -0
- package/extensions/orchestration/settings.ts +59 -0
- package/extensions/orchestration/storage.ts +49 -0
- package/extensions/orchestration/types.ts +105 -0
- package/extensions/performance-status/README.md +5 -0
- package/extensions/performance-status/index.ts +171 -0
- package/extensions/pi-memory/README.md +53 -0
- package/extensions/pi-memory/index.ts +326 -0
- package/extensions/pi-memory/src/config.ts +75 -0
- package/extensions/pi-memory/src/database.ts +617 -0
- package/extensions/pi-memory/src/extraction.ts +178 -0
- package/extensions/pi-memory/src/project.ts +80 -0
- package/extensions/pi-memory/src/projection.ts +59 -0
- package/extensions/pi-memory/src/redaction.ts +60 -0
- package/extensions/pi-memory/src/retrieval.ts +30 -0
- package/extensions/pi-memory/src/service.ts +194 -0
- package/extensions/pi-memory/src/session.ts +146 -0
- package/extensions/pi-memory/src/types.ts +82 -0
- package/extensions/shared/README.md +18 -0
- package/extensions/shared/activity-status.ts +31 -0
- package/extensions/shared/child-session.ts +150 -0
- package/extensions/shared/context-utilization.ts +47 -0
- package/extensions/shared/dashboard-state.ts +99 -0
- package/extensions/shared/tool-call-timeout.ts +104 -0
- package/extensions/shared/tui-dashboard.ts +139 -0
- package/extensions/shell-shortcuts/README.md +17 -0
- package/extensions/shell-shortcuts/features/exit-alias.ts +10 -0
- package/extensions/shell-shortcuts/index.ts +6 -0
- package/extensions/subagents/README.md +89 -0
- package/extensions/subagents/index.ts +1386 -0
- package/extensions/subagents/src/backend.ts +69 -0
- package/extensions/subagents/src/backends/pi.ts +781 -0
- package/extensions/subagents/src/catalog.ts +85 -0
- package/extensions/subagents/src/config.ts +241 -0
- package/extensions/subagents/src/context.ts +57 -0
- package/extensions/subagents/src/coordinator.ts +64 -0
- package/extensions/subagents/src/domain.ts +294 -0
- package/extensions/subagents/src/format.ts +74 -0
- package/extensions/subagents/src/manager.ts +813 -0
- package/extensions/subagents/src/prompt.ts +94 -0
- package/extensions/subagents/src/result-delivery.ts +20 -0
- package/extensions/subagents/src/runtime.ts +48 -0
- package/extensions/subagents/src/ui/takeover.ts +695 -0
- package/extensions/subagents/src/ui/transcript.ts +182 -0
- package/extensions/subagents/src/worktree.ts +175 -0
- package/extensions/web-fetch/README.md +13 -0
- package/extensions/web-fetch/index.ts +441 -0
- package/package.json +117 -0
- package/skills/background-terminals/SKILL.md +42 -0
- package/skills/orchestration/SKILL.md +33 -0
- package/skills/subagents/SKILL.md +89 -0
- package/themes/ember-warm-dark.json +85 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type TerminalStatus = "running" | "done" | "failed" | "killed";
|
|
2
|
+
|
|
3
|
+
export interface TerminalOutputView {
|
|
4
|
+
readonly text: string;
|
|
5
|
+
readonly totalBytes: number;
|
|
6
|
+
readonly truncatedBytes: number;
|
|
7
|
+
readonly cursor: number;
|
|
8
|
+
readonly version: number;
|
|
9
|
+
readonly spillPath?: string;
|
|
10
|
+
readonly spillTruncated: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface TerminalSnapshot {
|
|
14
|
+
readonly id: string;
|
|
15
|
+
readonly title: string;
|
|
16
|
+
readonly command: string;
|
|
17
|
+
readonly cwd: string;
|
|
18
|
+
readonly pid: number;
|
|
19
|
+
readonly status: TerminalStatus;
|
|
20
|
+
readonly createdAt: number;
|
|
21
|
+
readonly settledAt?: number;
|
|
22
|
+
readonly exitCode?: number;
|
|
23
|
+
readonly signal?: number;
|
|
24
|
+
readonly errorText?: string;
|
|
25
|
+
readonly output: TerminalOutputView;
|
|
26
|
+
readonly cols: number;
|
|
27
|
+
readonly rows: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface StartTerminalOptions {
|
|
31
|
+
readonly title: string;
|
|
32
|
+
readonly command: string;
|
|
33
|
+
readonly cwd: string;
|
|
34
|
+
readonly cols?: number;
|
|
35
|
+
readonly rows?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface TerminalReadResult {
|
|
39
|
+
readonly snapshot: TerminalSnapshot;
|
|
40
|
+
readonly text: string;
|
|
41
|
+
readonly cursor: number;
|
|
42
|
+
readonly omittedBytes: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function terminalElapsedMs(snapshot: TerminalSnapshot): number {
|
|
46
|
+
return Math.max(0, (snapshot.settledAt ?? Date.now()) - snapshot.createdAt);
|
|
47
|
+
}
|
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionCommandContext,
|
|
3
|
+
KeybindingsManager,
|
|
4
|
+
Theme,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { formatSize } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import type { Component, Focusable, TUI } from "@earendil-works/pi-tui";
|
|
8
|
+
import { Input, Key, matchesKey, truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
9
|
+
import {
|
|
10
|
+
frameBottom,
|
|
11
|
+
framedRow,
|
|
12
|
+
frameTop,
|
|
13
|
+
joinSides,
|
|
14
|
+
oneLine,
|
|
15
|
+
padLine,
|
|
16
|
+
sanitizeTerminalText,
|
|
17
|
+
stateLabel,
|
|
18
|
+
viewportSlice,
|
|
19
|
+
} from "../../shared/tui-dashboard.ts";
|
|
20
|
+
import type { TerminalManagerView } from "./manager.ts";
|
|
21
|
+
import type { TerminalSnapshot } from "./types.ts";
|
|
22
|
+
import { terminalElapsedMs } from "./types.ts";
|
|
23
|
+
import { formatDurationMs } from "../../shared/tui-dashboard.ts";
|
|
24
|
+
|
|
25
|
+
function configuredKeys(
|
|
26
|
+
keybindings: KeybindingsManager,
|
|
27
|
+
binding: Parameters<KeybindingsManager["getKeys"]>[0],
|
|
28
|
+
): string {
|
|
29
|
+
return keybindings.getKeys(binding).join("/") || "unbound";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function statusState(snapshot: TerminalSnapshot): "active" | "success" | "warning" | "error" | "muted" {
|
|
33
|
+
switch (snapshot.status) {
|
|
34
|
+
case "running":
|
|
35
|
+
return "active";
|
|
36
|
+
case "done":
|
|
37
|
+
return "success";
|
|
38
|
+
case "failed":
|
|
39
|
+
return "error";
|
|
40
|
+
case "killed":
|
|
41
|
+
return "muted";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function outputLines(text: string, width: number): string[] {
|
|
46
|
+
const out: string[] = [];
|
|
47
|
+
for (const rawLine of sanitizeTerminalText(text).split("\n")) {
|
|
48
|
+
const carriageSegments = rawLine.split("\r");
|
|
49
|
+
const line = carriageSegments.at(-1) || [...carriageSegments].reverse().find(Boolean) || "";
|
|
50
|
+
if (!line) {
|
|
51
|
+
out.push("");
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
out.push(...wrapTextWithAnsi(line, Math.max(10, width)));
|
|
55
|
+
}
|
|
56
|
+
if (out.at(-1) === "") out.pop();
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
class OutputLineCache {
|
|
61
|
+
private key = "";
|
|
62
|
+
private lines: string[] = [];
|
|
63
|
+
|
|
64
|
+
get(snapshot: TerminalSnapshot, width: number): string[] {
|
|
65
|
+
const key = `${snapshot.id}:${snapshot.output.version}:${width}`;
|
|
66
|
+
if (key !== this.key) {
|
|
67
|
+
this.key = key;
|
|
68
|
+
this.lines = outputLines(snapshot.output.text, width);
|
|
69
|
+
}
|
|
70
|
+
return this.lines;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface Selection {
|
|
75
|
+
id?: string;
|
|
76
|
+
index: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function reconcileTerminalSelection(
|
|
80
|
+
selection: Selection,
|
|
81
|
+
terminals: ReadonlyArray<Pick<TerminalSnapshot, "id">>,
|
|
82
|
+
): void {
|
|
83
|
+
const stable = selection.id
|
|
84
|
+
? terminals.findIndex((terminal) => terminal.id === selection.id)
|
|
85
|
+
: -1;
|
|
86
|
+
selection.index = stable >= 0
|
|
87
|
+
? stable
|
|
88
|
+
: Math.min(Math.max(0, selection.index), Math.max(0, terminals.length - 1));
|
|
89
|
+
selection.id = terminals[selection.index]?.id;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function openTerminalDashboard(
|
|
93
|
+
ctx: ExtensionCommandContext,
|
|
94
|
+
view: TerminalManagerView,
|
|
95
|
+
): Promise<void> {
|
|
96
|
+
const selection: Selection = { index: 0 };
|
|
97
|
+
while (view.list().length > 0) {
|
|
98
|
+
const selected = await ctx.ui.custom<string | null>(
|
|
99
|
+
(tui, theme, keybindings, done) =>
|
|
100
|
+
new TerminalDashboard(tui, theme, keybindings, view, selection, done),
|
|
101
|
+
{
|
|
102
|
+
overlay: true,
|
|
103
|
+
overlayOptions: { anchor: "center", width: "100%", maxHeight: "100%" },
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
if (!selected) return;
|
|
107
|
+
if (!view.get(selected)) continue;
|
|
108
|
+
await ctx.ui.custom<null>(
|
|
109
|
+
(tui, theme, keybindings, done) =>
|
|
110
|
+
new TerminalDetail(tui, theme, keybindings, view, selected, done),
|
|
111
|
+
{
|
|
112
|
+
overlay: true,
|
|
113
|
+
overlayOptions: { anchor: "center", width: "100%", maxHeight: "100%" },
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export class TerminalDashboard implements Component {
|
|
120
|
+
private closed = false;
|
|
121
|
+
private killArmed?: string;
|
|
122
|
+
private readonly ticker: ReturnType<typeof setInterval>;
|
|
123
|
+
private readonly unsubscribe: () => void;
|
|
124
|
+
private readonly tui: TUI;
|
|
125
|
+
private readonly theme: Theme;
|
|
126
|
+
private readonly keybindings: KeybindingsManager;
|
|
127
|
+
private readonly view: TerminalManagerView;
|
|
128
|
+
private readonly selection: Selection;
|
|
129
|
+
private readonly done: (id: string | null) => void;
|
|
130
|
+
private readonly outputCache = new OutputLineCache();
|
|
131
|
+
|
|
132
|
+
constructor(
|
|
133
|
+
tui: TUI,
|
|
134
|
+
theme: Theme,
|
|
135
|
+
keybindings: KeybindingsManager,
|
|
136
|
+
view: TerminalManagerView,
|
|
137
|
+
selection: Selection,
|
|
138
|
+
done: (id: string | null) => void,
|
|
139
|
+
) {
|
|
140
|
+
this.tui = tui;
|
|
141
|
+
this.theme = theme;
|
|
142
|
+
this.keybindings = keybindings;
|
|
143
|
+
this.view = view;
|
|
144
|
+
this.selection = selection;
|
|
145
|
+
this.done = done;
|
|
146
|
+
this.ticker = setInterval(() => this.tui.requestRender(), 1000);
|
|
147
|
+
this.ticker.unref?.();
|
|
148
|
+
this.unsubscribe = this.view.subscribe(() => this.tui.requestRender());
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private close(id: string | null): void {
|
|
152
|
+
if (this.closed) return;
|
|
153
|
+
this.closed = true;
|
|
154
|
+
clearInterval(this.ticker);
|
|
155
|
+
this.unsubscribe();
|
|
156
|
+
this.done(id);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
dispose(): void {
|
|
160
|
+
if (!this.closed) {
|
|
161
|
+
clearInterval(this.ticker);
|
|
162
|
+
this.unsubscribe();
|
|
163
|
+
}
|
|
164
|
+
this.closed = true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
handleInput(data: string): void {
|
|
168
|
+
const terminals = this.view.list();
|
|
169
|
+
reconcileTerminalSelection(this.selection, terminals);
|
|
170
|
+
if (this.keybindings.matches(data, "tui.select.cancel") || data === "q") {
|
|
171
|
+
this.close(null);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (this.keybindings.matches(data, "tui.select.confirm")) {
|
|
175
|
+
const terminal = terminals[this.selection.index];
|
|
176
|
+
if (terminal) this.close(terminal.id);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (this.keybindings.matches(data, "tui.select.up") || data === "k") {
|
|
180
|
+
if (terminals.length > 0) {
|
|
181
|
+
this.selection.index = (this.selection.index - 1 + terminals.length) % terminals.length;
|
|
182
|
+
this.selection.id = terminals[this.selection.index]?.id;
|
|
183
|
+
this.killArmed = undefined;
|
|
184
|
+
this.tui.requestRender();
|
|
185
|
+
}
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (this.keybindings.matches(data, "tui.select.down") || data === "j") {
|
|
189
|
+
if (terminals.length > 0) {
|
|
190
|
+
this.selection.index = (this.selection.index + 1) % terminals.length;
|
|
191
|
+
this.selection.id = terminals[this.selection.index]?.id;
|
|
192
|
+
this.killArmed = undefined;
|
|
193
|
+
this.tui.requestRender();
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (data === "x") {
|
|
198
|
+
const terminal = terminals[this.selection.index];
|
|
199
|
+
if (!terminal || terminal.status !== "running") return;
|
|
200
|
+
if (this.killArmed === terminal.id) {
|
|
201
|
+
this.killArmed = undefined;
|
|
202
|
+
this.view.requestKill(terminal.id);
|
|
203
|
+
} else {
|
|
204
|
+
this.killArmed = terminal.id;
|
|
205
|
+
}
|
|
206
|
+
this.tui.requestRender();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
render(width: number): string[] {
|
|
211
|
+
const theme = this.theme;
|
|
212
|
+
const terminals = this.view.list();
|
|
213
|
+
reconcileTerminalSelection(this.selection, terminals);
|
|
214
|
+
const selected = terminals[this.selection.index];
|
|
215
|
+
const running = terminals.filter((terminal) => terminal.status === "running").length;
|
|
216
|
+
const failed = terminals.filter((terminal) => terminal.status === "failed").length;
|
|
217
|
+
const completed = terminals.length - running - failed;
|
|
218
|
+
const rows = this.tui.terminal.rows || 30;
|
|
219
|
+
const bodyHeight = Math.max(7, rows - 6);
|
|
220
|
+
const innerWidth = Math.max(1, width - 2);
|
|
221
|
+
const counts = [
|
|
222
|
+
running ? stateLabel(theme, "active", `${running} running`) : "",
|
|
223
|
+
completed ? stateLabel(theme, "success", `${completed} settled`) : "",
|
|
224
|
+
failed ? stateLabel(theme, "error", `${failed} failed`) : "",
|
|
225
|
+
].filter(Boolean).join(theme.fg("dim", " · "));
|
|
226
|
+
const lines = [
|
|
227
|
+
joinSides(
|
|
228
|
+
` ${theme.fg("accent", theme.bold("Background terminals"))}`,
|
|
229
|
+
`${counts || theme.fg("muted", "idle")} `,
|
|
230
|
+
width,
|
|
231
|
+
),
|
|
232
|
+
frameTop(theme, width, `${terminals.length} terminal${terminals.length === 1 ? "" : "s"} · PTY sessions`),
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
if (width >= 100 && selected) {
|
|
236
|
+
const leftWidth = Math.max(38, Math.floor((innerWidth - 1) * 0.48));
|
|
237
|
+
const rightWidth = Math.max(20, innerWidth - leftWidth - 1);
|
|
238
|
+
const list = this.renderList(terminals, leftWidth, bodyHeight);
|
|
239
|
+
const detail = this.renderPreview(selected, rightWidth, bodyHeight);
|
|
240
|
+
for (let row = 0; row < bodyHeight; row++) {
|
|
241
|
+
lines.push(
|
|
242
|
+
theme.fg("border", "│") +
|
|
243
|
+
padLine(list[row] ?? "", leftWidth) +
|
|
244
|
+
theme.fg("borderMuted", "│") +
|
|
245
|
+
padLine(detail[row] ?? "", rightWidth) +
|
|
246
|
+
theme.fg("border", "│"),
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
const list = this.renderList(terminals, innerWidth, bodyHeight);
|
|
251
|
+
for (let row = 0; row < bodyHeight; row++) {
|
|
252
|
+
lines.push(framedRow(theme, list[row] ?? "", width));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
lines.push(frameBottom(theme, width));
|
|
256
|
+
const killHint = this.killArmed ? theme.fg("warning", "x again to stop") : "x stop (confirm)";
|
|
257
|
+
lines.push(
|
|
258
|
+
truncateToWidth(
|
|
259
|
+
theme.fg("dim", ` ${configuredKeys(this.keybindings, "tui.select.up")}/${configuredKeys(this.keybindings, "tui.select.down")}/jk select · ${configuredKeys(this.keybindings, "tui.select.confirm")} inspect · ${killHint} · ${configuredKeys(this.keybindings, "tui.select.cancel")} close`),
|
|
260
|
+
width,
|
|
261
|
+
),
|
|
262
|
+
);
|
|
263
|
+
return lines;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private renderList(
|
|
267
|
+
terminals: ReadonlyArray<TerminalSnapshot>,
|
|
268
|
+
width: number,
|
|
269
|
+
height: number,
|
|
270
|
+
): string[] {
|
|
271
|
+
const viewport = viewportSlice(terminals, this.selection.index, height);
|
|
272
|
+
return viewport.items.map((terminal, row) => {
|
|
273
|
+
const selected = viewport.start + row === this.selection.index;
|
|
274
|
+
const left = ` ${selected ? this.theme.fg("accent", "❯") : " "} ${stateLabel(this.theme, statusState(terminal), terminal.id)} ${selected ? this.theme.fg("accent", oneLine(terminal.title)) : this.theme.fg("text", oneLine(terminal.title))}`;
|
|
275
|
+
const right = `${this.theme.fg("muted", formatDurationMs(terminalElapsedMs(terminal)))} `;
|
|
276
|
+
const line = joinSides(left, right, width);
|
|
277
|
+
return selected ? this.theme.bg("selectedBg", padLine(line, width)) : line;
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private renderPreview(terminal: TerminalSnapshot, width: number, height: number): string[] {
|
|
282
|
+
const theme = this.theme;
|
|
283
|
+
const lines = [
|
|
284
|
+
` ${stateLabel(theme, statusState(terminal), terminal.status)} ${theme.fg("accent", theme.bold(oneLine(terminal.title)))}`,
|
|
285
|
+
` ${theme.fg("dim", `${terminal.id} · pid ${terminal.pid} · ${formatDurationMs(terminalElapsedMs(terminal))}`)}`,
|
|
286
|
+
"",
|
|
287
|
+
` ${theme.fg("muted", "COMMAND")}`,
|
|
288
|
+
...wrapTextWithAnsi(oneLine(terminal.command), Math.max(10, width - 2)).slice(0, 2).map((line) => ` ${theme.fg("text", line)}`),
|
|
289
|
+
"",
|
|
290
|
+
` ${theme.fg("muted", "OUTPUT")} ${theme.fg("text", formatSize(terminal.output.totalBytes))}${terminal.output.truncatedBytes ? theme.fg("warning", " · retained tail") : ""}`,
|
|
291
|
+
` ${theme.fg("muted", "SIZE")} ${theme.fg("text", `${terminal.cols}×${terminal.rows}`)}`,
|
|
292
|
+
"",
|
|
293
|
+
` ${theme.fg("muted", "LATEST")}`,
|
|
294
|
+
];
|
|
295
|
+
const output = this.outputCache.get(terminal, Math.max(10, width - 2));
|
|
296
|
+
const room = Math.max(1, height - lines.length - 2);
|
|
297
|
+
lines.push(...output.slice(-room).map((line) => ` ${theme.fg("toolOutput", line)}`));
|
|
298
|
+
lines.push("", ` ${theme.fg("dim", terminal.cwd)}`);
|
|
299
|
+
return lines.slice(0, height);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
invalidate(): void {}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export class TerminalDetail implements Component, Focusable {
|
|
306
|
+
private readonly input = new Input();
|
|
307
|
+
private scrollOffset = 0;
|
|
308
|
+
private closed = false;
|
|
309
|
+
private killArmed = false;
|
|
310
|
+
private renderTimer?: ReturnType<typeof setTimeout>;
|
|
311
|
+
private readonly ticker: ReturnType<typeof setInterval>;
|
|
312
|
+
private readonly unsubscribe: () => void;
|
|
313
|
+
private readonly tui: TUI;
|
|
314
|
+
private readonly theme: Theme;
|
|
315
|
+
private readonly keybindings: KeybindingsManager;
|
|
316
|
+
private readonly view: TerminalManagerView;
|
|
317
|
+
private readonly id: string;
|
|
318
|
+
private readonly done: (value: null) => void;
|
|
319
|
+
private readonly outputCache = new OutputLineCache();
|
|
320
|
+
private _focused = false;
|
|
321
|
+
|
|
322
|
+
get focused(): boolean {
|
|
323
|
+
return this._focused;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
set focused(value: boolean) {
|
|
327
|
+
this._focused = value;
|
|
328
|
+
this.input.focused = value;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
constructor(
|
|
332
|
+
tui: TUI,
|
|
333
|
+
theme: Theme,
|
|
334
|
+
keybindings: KeybindingsManager,
|
|
335
|
+
view: TerminalManagerView,
|
|
336
|
+
id: string,
|
|
337
|
+
done: (value: null) => void,
|
|
338
|
+
) {
|
|
339
|
+
this.tui = tui;
|
|
340
|
+
this.theme = theme;
|
|
341
|
+
this.keybindings = keybindings;
|
|
342
|
+
this.view = view;
|
|
343
|
+
this.id = id;
|
|
344
|
+
this.done = done;
|
|
345
|
+
this.unsubscribe = this.view.subscribeTo(id, () => this.scheduleRender());
|
|
346
|
+
this.ticker = setInterval(() => this.tui.requestRender(), 1000);
|
|
347
|
+
this.ticker.unref?.();
|
|
348
|
+
this.input.onSubmit = (value) => {
|
|
349
|
+
const terminal = this.view.get(this.id);
|
|
350
|
+
if (!terminal || terminal.status !== "running") return;
|
|
351
|
+
this.view.requestWrite(this.id, `${value}\r`);
|
|
352
|
+
this.input.setValue("");
|
|
353
|
+
this.scrollOffset = 0;
|
|
354
|
+
this.tui.requestRender();
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
private scheduleRender(): void {
|
|
359
|
+
if (this.renderTimer) return;
|
|
360
|
+
this.renderTimer = setTimeout(() => {
|
|
361
|
+
this.renderTimer = undefined;
|
|
362
|
+
if (!this.closed) this.tui.requestRender();
|
|
363
|
+
}, 40);
|
|
364
|
+
this.renderTimer.unref?.();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private close(): void {
|
|
368
|
+
if (this.closed) return;
|
|
369
|
+
this.closed = true;
|
|
370
|
+
this.cleanup();
|
|
371
|
+
this.done(null);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private cleanup(): void {
|
|
375
|
+
this.unsubscribe();
|
|
376
|
+
clearInterval(this.ticker);
|
|
377
|
+
if (this.renderTimer) clearTimeout(this.renderTimer);
|
|
378
|
+
this.renderTimer = undefined;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
dispose(): void {
|
|
382
|
+
if (!this.closed) this.cleanup();
|
|
383
|
+
this.closed = true;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
handleInput(data: string): void {
|
|
387
|
+
const terminal = this.view.get(this.id);
|
|
388
|
+
if (matchesKey(data, Key.ctrl("c"))) {
|
|
389
|
+
if (terminal?.status === "running") this.view.requestWrite(this.id, "\x03");
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (this.keybindings.matches(data, "app.interrupt") || this.keybindings.matches(data, "tui.select.cancel")) {
|
|
393
|
+
this.close();
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (data === "x") {
|
|
397
|
+
if (terminal?.status !== "running") return;
|
|
398
|
+
if (this.killArmed) {
|
|
399
|
+
this.killArmed = false;
|
|
400
|
+
this.view.requestKill(this.id);
|
|
401
|
+
} else {
|
|
402
|
+
this.killArmed = true;
|
|
403
|
+
}
|
|
404
|
+
this.tui.requestRender();
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (this.keybindings.matches(data, "tui.editor.pageUp") || data === "k") {
|
|
408
|
+
this.scrollOffset += Math.max(4, this.viewportHeight() - 2);
|
|
409
|
+
this.tui.requestRender();
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (this.keybindings.matches(data, "tui.editor.pageDown") || data === "j") {
|
|
413
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - Math.max(4, this.viewportHeight() - 2));
|
|
414
|
+
this.tui.requestRender();
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (data === "g") {
|
|
418
|
+
this.scrollOffset = Number.MAX_SAFE_INTEGER;
|
|
419
|
+
this.tui.requestRender();
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (data === "G") {
|
|
423
|
+
this.scrollOffset = 0;
|
|
424
|
+
this.tui.requestRender();
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
this.killArmed = false;
|
|
428
|
+
this.input.handleInput(data);
|
|
429
|
+
this.tui.requestRender();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
private viewportHeight(): number {
|
|
433
|
+
return Math.max(5, (this.tui.terminal.rows || 30) - 10);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
render(width: number): string[] {
|
|
437
|
+
const theme = this.theme;
|
|
438
|
+
const terminal = this.view.get(this.id);
|
|
439
|
+
if (!terminal) return [frameTop(theme, width, "Terminal"), framedRow(theme, "No longer tracked", width), frameBottom(theme, width)];
|
|
440
|
+
const viewport = this.viewportHeight();
|
|
441
|
+
if (terminal.status === "running") {
|
|
442
|
+
this.view.requestResize(this.id, Math.max(20, width - 2), viewport);
|
|
443
|
+
}
|
|
444
|
+
const lines = [
|
|
445
|
+
joinSides(
|
|
446
|
+
` ${stateLabel(theme, statusState(terminal), terminal.status)} ${theme.fg("accent", theme.bold(`${terminal.id} · ${oneLine(terminal.title)}`))}`,
|
|
447
|
+
`${theme.fg("muted", `pid ${terminal.pid} · ${formatDurationMs(terminalElapsedMs(terminal))}`)} `,
|
|
448
|
+
width,
|
|
449
|
+
),
|
|
450
|
+
truncateToWidth(` ${theme.fg("dim", "$ ")}${theme.fg("text", oneLine(terminal.command))}`, width),
|
|
451
|
+
frameTop(theme, width, `live output · ${formatSize(terminal.output.totalBytes)} · ${terminal.cols}×${terminal.rows}`),
|
|
452
|
+
];
|
|
453
|
+
const output = this.outputCache.get(terminal, Math.max(10, width - 4));
|
|
454
|
+
const maxOffset = Math.max(0, output.length - viewport);
|
|
455
|
+
this.scrollOffset = Math.min(this.scrollOffset, maxOffset);
|
|
456
|
+
const end = output.length - this.scrollOffset;
|
|
457
|
+
const visible = output.slice(Math.max(0, end - viewport), end);
|
|
458
|
+
const body = visible.length > 0 ? visible : [theme.fg("dim", "(waiting for output)")];
|
|
459
|
+
for (let row = 0; row < viewport; row++) {
|
|
460
|
+
lines.push(framedRow(theme, ` ${body[row] ?? ""}`, width));
|
|
461
|
+
}
|
|
462
|
+
lines.push(frameBottom(theme, width));
|
|
463
|
+
if (terminal.status === "running") lines.push(...this.input.render(width));
|
|
464
|
+
else lines.push(truncateToWidth(` ${theme.fg("muted", "Process settled; input is closed.")}`, width));
|
|
465
|
+
const stop = this.killArmed ? theme.fg("warning", "x again to stop") : "x stop (confirm)";
|
|
466
|
+
lines.push(
|
|
467
|
+
truncateToWidth(
|
|
468
|
+
theme.fg("dim", ` enter send · ctrl+c interrupt PTY · ${stop} · pgup/pgdn or j/k scroll · g/G top/bottom · ${configuredKeys(this.keybindings, "tui.select.cancel")} back`),
|
|
469
|
+
width,
|
|
470
|
+
),
|
|
471
|
+
);
|
|
472
|
+
return lines;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
invalidate(): void {
|
|
476
|
+
this.input.invalidate();
|
|
477
|
+
}
|
|
478
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thomas Mustier
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Pi Context Usage
|
|
2
|
+
|
|
3
|
+
Displays a compact startup context-budget card showing total request/harness usage and the system, AGENTS.md, skill-index, and active-tool contributions. Detailed audit views expose session material and per-resource estimates.
|
|
4
|
+
|
|
5
|
+
## Use
|
|
6
|
+
|
|
7
|
+
- `Ctrl+O` cycles summary, compact, and expanded views.
|
|
8
|
+
- `/context-usage` cycles views.
|
|
9
|
+
- `/context-usage summary|compact|expanded` selects a view directly.
|
|
10
|
+
- `/reload` activates source changes in an existing Pi session.
|
|
11
|
+
|
|
12
|
+
Numbers prefixed with `~` are model-aware estimates. `Total request` uses Pi's own context count.
|
|
13
|
+
|
|
14
|
+
## Configuration
|
|
15
|
+
|
|
16
|
+
Optional JSON configuration is loaded from `pi-context-usage.json` in Pi's global or trusted project configuration directories. `PI_CONTEXT_USAGE_CONFIG` can provide additional paths separated by the operating system's path delimiter.
|
|
17
|
+
|
|
18
|
+
## License
|
|
19
|
+
|
|
20
|
+
See `LICENSE`.
|