pi-harness-delegate 0.6.1 → 0.7.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/README.md +66 -10
- package/extensions/acp-runner.ts +98 -11
- package/extensions/activity.ts +91 -1
- package/extensions/command.ts +35 -2
- package/extensions/concurrency.ts +4 -1
- package/extensions/config.ts +114 -20
- package/extensions/engine.ts +586 -0
- package/extensions/fanout.ts +526 -0
- package/extensions/harnesses/amp.ts +7 -6
- package/extensions/harnesses/claude.ts +2 -0
- package/extensions/harnesses/codex.ts +15 -7
- package/extensions/harnesses/opencode.ts +14 -3
- package/extensions/harnesses/types.ts +8 -0
- package/extensions/history.ts +170 -0
- package/extensions/index.ts +85 -1369
- package/extensions/run-registry.ts +36 -3
- package/extensions/runner.ts +72 -10
- package/extensions/subcommands.ts +227 -0
- package/extensions/templates.ts +12 -0
- package/extensions/validate.ts +214 -0
- package/package.json +5 -4
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/** `/delegate history` — transcript discovery, listing, and the scrollable viewer. Split out of index.ts. */
|
|
2
|
+
|
|
3
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
4
|
+
import { basename, join } from 'node:path';
|
|
5
|
+
import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
6
|
+
import { Key, matchesKey, type SelectItem, SelectList, truncateToWidth } from '@earendil-works/pi-tui';
|
|
7
|
+
import { parseTranscriptMeta } from './activity.ts';
|
|
8
|
+
import { outputsDir as getOutputsDir, legacyOutputsDir } from './config.ts';
|
|
9
|
+
import { formatCost } from './engine.ts';
|
|
10
|
+
import { HARNESS_NAMES } from './harnesses/registry.ts';
|
|
11
|
+
export interface HistoryEntry {
|
|
12
|
+
file: string;
|
|
13
|
+
mode: string;
|
|
14
|
+
harness: string;
|
|
15
|
+
cost: number | null;
|
|
16
|
+
sessionId: string | null;
|
|
17
|
+
mtime: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function readHistory(dir: string, harness: string): HistoryEntry[] {
|
|
21
|
+
try {
|
|
22
|
+
return readdirSync(dir)
|
|
23
|
+
.filter(f => f.endsWith('.md') && !f.includes('-partial'))
|
|
24
|
+
.map(f => {
|
|
25
|
+
const file = join(dir, f);
|
|
26
|
+
let mode = 'delegate';
|
|
27
|
+
let cost: number | null = null;
|
|
28
|
+
let sessionId: string | null = null;
|
|
29
|
+
try {
|
|
30
|
+
const meta = parseTranscriptMeta(readFileSync(file, 'utf8').slice(0, 2000));
|
|
31
|
+
mode = meta.mode;
|
|
32
|
+
cost = meta.cost;
|
|
33
|
+
sessionId = meta.sessionId;
|
|
34
|
+
} catch (_e) {
|
|
35
|
+
void _e;
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
file,
|
|
39
|
+
mode,
|
|
40
|
+
harness,
|
|
41
|
+
cost,
|
|
42
|
+
sessionId,
|
|
43
|
+
mtime: statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? 0,
|
|
44
|
+
};
|
|
45
|
+
})
|
|
46
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
47
|
+
} catch {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function readAllHistory(): HistoryEntry[] {
|
|
53
|
+
const entries: HistoryEntry[] = [];
|
|
54
|
+
// new partitioned dir
|
|
55
|
+
for (const h of HARNESS_NAMES) {
|
|
56
|
+
entries.push(...readHistory(getOutputsDir(h), h));
|
|
57
|
+
}
|
|
58
|
+
// also legacy dir for migration display
|
|
59
|
+
try {
|
|
60
|
+
const legacy = readdirSync(legacyOutputsDir()).filter(f => f.endsWith('.md') && !f.includes('-partial'));
|
|
61
|
+
for (const f of legacy) {
|
|
62
|
+
const file = join(legacyOutputsDir(), f);
|
|
63
|
+
let mode = 'delegate';
|
|
64
|
+
let cost: number | null = null;
|
|
65
|
+
let sessionId: string | null = null;
|
|
66
|
+
try {
|
|
67
|
+
const meta = parseTranscriptMeta(readFileSync(file, 'utf8').slice(0, 2000));
|
|
68
|
+
mode = meta.mode;
|
|
69
|
+
cost = meta.cost;
|
|
70
|
+
sessionId = meta.sessionId;
|
|
71
|
+
} catch (_e) {
|
|
72
|
+
void _e;
|
|
73
|
+
}
|
|
74
|
+
entries.push({
|
|
75
|
+
file,
|
|
76
|
+
mode,
|
|
77
|
+
harness: 'claude',
|
|
78
|
+
cost,
|
|
79
|
+
sessionId,
|
|
80
|
+
mtime: statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? 0,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
} catch (_e) {
|
|
84
|
+
void _e;
|
|
85
|
+
}
|
|
86
|
+
return entries.sort((a, b) => b.mtime - a.mtime);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function viewTranscript(ctx: ExtensionContext, entry: HistoryEntry): Promise<void> {
|
|
90
|
+
if (!ctx.hasUI) {
|
|
91
|
+
process.stdout.write(readFileSync(entry.file, 'utf8'));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
95
|
+
const lines = readFileSync(entry.file, 'utf8').split('\n');
|
|
96
|
+
let offset = 0;
|
|
97
|
+
const height = 12;
|
|
98
|
+
return {
|
|
99
|
+
render(width: number): string[] {
|
|
100
|
+
const resume = entry.sessionId ? ` · r resume` : '';
|
|
101
|
+
const header = theme.fg('accent', `${basename(entry.file)} (↑↓ scroll${resume} · esc close)`);
|
|
102
|
+
const visible = lines.slice(offset, offset + height);
|
|
103
|
+
return [header, ...visible.map(l => theme.fg('muted', truncateToWidth(l, width)))];
|
|
104
|
+
},
|
|
105
|
+
handleInput(data: string): void {
|
|
106
|
+
if (matchesKey(data, Key.down) && offset < lines.length - 1) {
|
|
107
|
+
offset++;
|
|
108
|
+
tui.requestRender();
|
|
109
|
+
} else if (matchesKey(data, Key.up) && offset > 0) {
|
|
110
|
+
offset--;
|
|
111
|
+
tui.requestRender();
|
|
112
|
+
} else if (matchesKey(data, Key.escape)) {
|
|
113
|
+
done(undefined);
|
|
114
|
+
} else if (entry.sessionId && data === 'r') {
|
|
115
|
+
ctx.ui.notify?.(`resume with: /delegate --resume=${entry.sessionId} <prompt>`, 'info');
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
invalidate() {},
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function showHistory(ctx: ExtensionContext, harnessFilter?: string): Promise<void> {
|
|
124
|
+
const entries = harnessFilter ? readAllHistory().filter(e => e.harness === harnessFilter) : readAllHistory();
|
|
125
|
+
if (entries.length === 0) {
|
|
126
|
+
const msg = harnessFilter
|
|
127
|
+
? `No transcripts yet for ${harnessFilter} — run /delegate ${harnessFilter} <mode> <prompt> first`
|
|
128
|
+
: 'No transcripts yet — run /delegate <harness> <mode> <prompt> first';
|
|
129
|
+
if (!ctx.hasUI) process.stdout.write(`${msg}\n`);
|
|
130
|
+
else ctx.ui.notify?.(msg, 'info');
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (!ctx.hasUI) {
|
|
134
|
+
if (harnessFilter) process.stdout.write(`delegate — history (${harnessFilter})\n`);
|
|
135
|
+
for (const e of entries)
|
|
136
|
+
process.stdout.write(`${e.harness} ${e.mode} · ${formatCost(e.cost)} · ${e.sessionId ?? '-'}\n`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const entry = await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
140
|
+
const items: SelectItem[] = entries.map(e => ({
|
|
141
|
+
value: e.file,
|
|
142
|
+
label: `${e.harness} ${e.mode} · ${formatCost(e.cost)} · ${new Date(e.mtime).toISOString().slice(0, 16)}`,
|
|
143
|
+
description: e.sessionId ? `session ${e.sessionId.slice(0, 8)}…` : undefined,
|
|
144
|
+
}));
|
|
145
|
+
const list = new SelectList(items, Math.min(items.length, 10), {
|
|
146
|
+
selectedPrefix: (s: string) => theme.fg('accent', s),
|
|
147
|
+
selectedText: (s: string) => theme.fg('accent', s),
|
|
148
|
+
description: (s: string) => theme.fg('dim', s),
|
|
149
|
+
scrollInfo: (s: string) => theme.fg('dim', s),
|
|
150
|
+
noMatch: (s: string) => theme.fg('warning', s),
|
|
151
|
+
});
|
|
152
|
+
list.onSelect = item => done(item.value);
|
|
153
|
+
list.onCancel = () => done(undefined);
|
|
154
|
+
return {
|
|
155
|
+
render: (w: number) => {
|
|
156
|
+
const rows = list.render(w);
|
|
157
|
+
return harnessFilter ? [theme.fg('accent', `delegate — history (${harnessFilter})`), ...rows] : rows;
|
|
158
|
+
},
|
|
159
|
+
invalidate: () => list.invalidate(),
|
|
160
|
+
handleInput: (data: string) => {
|
|
161
|
+
list.handleInput(data);
|
|
162
|
+
tui.requestRender();
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
if (entry) {
|
|
167
|
+
const chosen = entries.find(e => e.file === entry);
|
|
168
|
+
if (chosen) await viewTranscript(ctx, chosen);
|
|
169
|
+
}
|
|
170
|
+
}
|