pi-optchat 0.5.0 → 0.5.1
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 +1 -1
- package/package.json +1 -1
- package/src/agents.ts +7 -3
- package/src/index.ts +5 -2
- package/src/inspector.ts +58 -19
- package/src/navigation.ts +8 -5
package/README.md
CHANGED
|
@@ -171,7 +171,7 @@ To delete a profile, delete its folder. Your original Pi sessions are kept in Pi
|
|
|
171
171
|
|
|
172
172
|
- **Profiles separate memory and instructions only.** Agents keep full filesystem access and share provider credentials.
|
|
173
173
|
- **Use worktrees** when parallel agents edit the same repository; they share the filesystem.
|
|
174
|
-
- **Instructions**:
|
|
174
|
+
- **Instructions**: the main agent and subagents get Pi's usual global and repository `AGENTS.md` files and your skills, followed by the profile's `AGENTS.md`, which comes last and wins. OptChat replaces only Pi's opening prompt. Prompt templates work in the main session only.
|
|
175
175
|
- **Images** are available during the current run but stored in memory as text placeholders.
|
|
176
176
|
- **Pi's auto-compaction is off.** A single very long run can still hit the model's context limit; stop it and continue in a new turn.
|
|
177
177
|
- **Restarts**: unsent inputs are recovered into memory, and pending subagent reports are delivered. Interrupted subagents are not restarted.
|
package/package.json
CHANGED
package/src/agents.ts
CHANGED
|
@@ -115,16 +115,20 @@ export class Children {
|
|
|
115
115
|
if (cancelled()) throw new Error('Parent or profile is stopping.');
|
|
116
116
|
const id = randomUUID().slice(0, 8), directory = task.cwd ?? cwd;
|
|
117
117
|
const delegation = depth < 3 ? 'You may delegate parts of your assigned task with spawn when useful. Child reports arrive automatically after your current run ends; the harness keeps you alive to receive them. Never poll, sleep, or wait in a tool for children. Finish your current work and return; you will be prompted with their results. The profile allows 8 active agents total.' : 'You are at the maximum delegation depth. Complete your task with your own tools.';
|
|
118
|
-
const
|
|
118
|
+
const instructions = [this.instructions(), delegation, connected ? 'You are speaking directly with the user in a connected window. Continue this conversation across requests. Use tell_main for questions or findings the main agent needs now. A handoff will be generated when the user completes or disconnects the window.' : ''].filter(Boolean).join('\n\n');
|
|
119
119
|
// The user's settings list their installed packages; a copy in memory keeps the child from writing them back.
|
|
120
120
|
const settingsManager = SettingsManager.inMemory({ ...SettingsManager.create(directory, getAgentDir()).getSettings(), compaction: { enabled: false }, cacheWarming: 'off' });
|
|
121
121
|
const loader = new DefaultResourceLoader({ cwd: directory, agentDir: getAgentDir(), settingsManager,
|
|
122
|
-
|
|
122
|
+
noPromptTemplates: true,
|
|
123
123
|
extensionsOverride: base => ({ ...base, extensions: base.extensions.filter(e => !isOptchat(e.resolvedPath)) }),
|
|
124
124
|
extensionFactories: [pi => {
|
|
125
125
|
const provider = this.registry.getRegisteredProviderConfig(selected.provider);
|
|
126
126
|
if (provider) pi.registerProvider(selected.provider, provider);
|
|
127
|
-
|
|
127
|
+
// Same prompt as the main agent (AGENTS.md files, skills, cwd); only the OptChat preamble differs.
|
|
128
|
+
pi.on('before_agent_start', event => {
|
|
129
|
+
event.systemPromptOptions.customPrompt = `${SUBAGENT}\n\n${VIEW_DOC}`;
|
|
130
|
+
event.systemPromptOptions.sections.instructions = instructions;
|
|
131
|
+
});
|
|
128
132
|
pi.on('before_provider_request', (event, ctx) => ctx.model?.api === 'anthropic-messages' ? cachePayload(event.payload) : event.payload);
|
|
129
133
|
}],
|
|
130
134
|
});
|
package/src/index.ts
CHANGED
|
@@ -194,12 +194,15 @@ export default function optchat(pi: ExtensionAPI) {
|
|
|
194
194
|
}
|
|
195
195
|
return { action: 'continue' };
|
|
196
196
|
});
|
|
197
|
-
pi.on('before_agent_start', (
|
|
197
|
+
pi.on('before_agent_start', (event, ctx) => {
|
|
198
198
|
flush(); run = []; logged = 0; view = undefined; runStarted = true;
|
|
199
199
|
previous = previousExchange(ctx.sessionManager.getBranch());
|
|
200
200
|
pi.appendEntry(RUN_BOUNDARY, { state: 'start' });
|
|
201
201
|
const a = required();
|
|
202
|
-
|
|
202
|
+
// Pi's own prompt sections (AGENTS.md files, skills, cwd) stay; the profile's instructions go last.
|
|
203
|
+
event.systemPromptOptions.customPrompt = `${MASTER}\n\n${VIEW_DOC}\n\nFor conversational continuity, the memory view may be followed by the immediately preceding completed exchange (its user requests and final answer, in full text), then the new input. Use that exact wording to understand follow-ups; older exchanges and previous tool output remain accessible through memory and zoom.`;
|
|
204
|
+
event.systemPromptOptions.sections.instructions = `${instructions(a.dir)}\n\n${IMPORT_GUIDANCE}`;
|
|
205
|
+
prompt = event.systemPrompt;
|
|
203
206
|
});
|
|
204
207
|
pi.on('message_end', (event, ctx) => {
|
|
205
208
|
if (!active || !runStarted) return;
|
package/src/inspector.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Input, matchesKey, truncateToWidth, wrapTextWithAnsi, type Component, type Focusable } from '@earendil-works/pi-tui';
|
|
1
|
+
import { Input, matchesKey, sliceByColumn, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component, type Focusable } from '@earendil-works/pi-tui';
|
|
2
2
|
import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
3
3
|
import type { AgentMessage } from '@earendil-works/pi-agent-core';
|
|
4
4
|
import type { Children } from './agents.ts';
|
|
@@ -11,11 +11,24 @@ export const clean = (text: string) => text.replace(/[\x00-\x08\x0b-\x1f\x7f-\x9
|
|
|
11
11
|
export const oneLine = (text: string) => clean(text).replace(/\n/g, ' ');
|
|
12
12
|
export const count = (n: number) => n.toLocaleString('en-US');
|
|
13
13
|
export const elapsed = (ms: number) => ms < 60_000 ? `${Math.max(0, Math.floor(ms / 1000))}s` : `${Math.floor(ms / 60_000)}m ${Math.floor(ms / 1000) % 60}s`;
|
|
14
|
+
/** Shortens plain text with an ellipsis, preferring a word boundary. */
|
|
15
|
+
export function fit(text: string, width: number) {
|
|
16
|
+
if (visibleWidth(text) <= width) return text;
|
|
17
|
+
if (width < 2) return sliceByColumn(text, 0, width, true);
|
|
18
|
+
const cut = sliceByColumn(text, 0, width - 1, true), space = cut.lastIndexOf(' '); // Unlike truncateToWidth, adds no style reset.
|
|
19
|
+
return `${(space > cut.length * 0.6 ? cut.slice(0, space) : cut).trimEnd()}…`;
|
|
20
|
+
}
|
|
21
|
+
/** Left text and right text on one line, the right side flush with the edge when both fit. */
|
|
22
|
+
const spread = (left: string, right: string, width: number) => {
|
|
23
|
+
const gap = width - visibleWidth(left) - visibleWidth(right);
|
|
24
|
+
return gap >= 2 ? `${left}${' '.repeat(gap)}${right}` : left;
|
|
25
|
+
};
|
|
14
26
|
export type InspectorPage = 'agents' | 'usage';
|
|
27
|
+
type Tone = 'accent' | 'muted' | 'dim' | 'error' | 'border';
|
|
15
28
|
interface Options {
|
|
16
29
|
profile: string; session: string; children: Children; usage: UsageLedger; page: InspectorPage;
|
|
17
30
|
rows: () => number; redraw: () => void; done: (action?: 'model') => void;
|
|
18
|
-
color: (tone:
|
|
31
|
+
color: (tone: Tone, text: string) => string;
|
|
19
32
|
context: () => number | null | undefined;
|
|
20
33
|
refreshUsage?: () => void;
|
|
21
34
|
signal?: AbortSignal;
|
|
@@ -30,6 +43,7 @@ export class Inspector implements Component, Focusable {
|
|
|
30
43
|
private top = 0;
|
|
31
44
|
private scroll = 0;
|
|
32
45
|
private lineCount = 0;
|
|
46
|
+
private hintLines = 1;
|
|
33
47
|
private follow = true;
|
|
34
48
|
private details = false;
|
|
35
49
|
private range: UsageRange = 'This session';
|
|
@@ -55,7 +69,8 @@ export class Inspector implements Component, Focusable {
|
|
|
55
69
|
this.ended = true; clearInterval(this.timer); this.unsubscribe(); this.options.signal?.removeEventListener('abort', this.abort);
|
|
56
70
|
}
|
|
57
71
|
invalidate() { this.composer?.invalidate(); }
|
|
58
|
-
|
|
72
|
+
/** Body rows left after the rules, title, hint, spacing and composer; the rest of the screen keeps the footer visible. */
|
|
73
|
+
private get height() { return Math.max(1, Math.floor(this.options.rows() * 0.8) - (this.composer ? 7 : 6) - (this.hintLines - 1)); }
|
|
59
74
|
private select(delta: number) {
|
|
60
75
|
const list = this.options.children.history.list();
|
|
61
76
|
const index = Math.max(0, list.findIndex(r => r.id === this.selected));
|
|
@@ -178,17 +193,18 @@ export class Inspector implements Component, Focusable {
|
|
|
178
193
|
}
|
|
179
194
|
render(width: number): string[] {
|
|
180
195
|
const { color, children, profile } = this.options;
|
|
196
|
+
const inner = Math.max(1, width - 2);
|
|
181
197
|
const inspected = this.opened ? children.history.records.get(this.opened) : undefined;
|
|
182
|
-
|
|
183
|
-
let body: string[], hint: string;
|
|
198
|
+
let title = `OptChat · ${profile} · ${inspected ? `${inspected.state}: ${oneLine(inspected.task)}` : this.page === 'usage' ? 'Usage' : 'Agents'}`;
|
|
199
|
+
let info: string, body: string[], hint: string;
|
|
184
200
|
if (this.page === 'usage' || this.opened) {
|
|
185
201
|
const run = this.opened ? children.history.records.get(this.opened) : undefined;
|
|
186
202
|
const text = this.page === 'usage' ? this.usageLines() : run ? `${run.state} · ${elapsed((run.ended ?? Date.now()) - run.started)} · ${run.id}\n\n${this.transcript(run)}` : 'Run unavailable.';
|
|
187
|
-
const lines = wrapTextWithAnsi(clean(text),
|
|
203
|
+
const lines = wrapTextWithAnsi(clean(text), inner); this.lineCount = lines.length;
|
|
188
204
|
if (this.opened && this.follow) this.scroll = Math.max(0, lines.length - this.height);
|
|
189
205
|
this.scroll = Math.max(0, Math.min(this.scroll, lines.length - this.height));
|
|
190
206
|
body = lines.slice(this.scroll, this.scroll + this.height);
|
|
191
|
-
|
|
207
|
+
info = `${this.scroll + 1}–${Math.min(this.scroll + this.height, lines.length)} / ${lines.length}${this.opened ? this.follow ? ' · following' : ' · scroll paused' : ''}`;
|
|
192
208
|
hint = this.page === 'usage' ? '←→ period · ↑↓/PgUp/PgDn scroll · Tab agents · Esc close' : '↑↓/PgUp/PgDn scroll · t tools · f follow · s message · x stop tree · Esc back';
|
|
193
209
|
} else {
|
|
194
210
|
const list = children.history.list();
|
|
@@ -197,24 +213,47 @@ export class Inspector implements Component, Focusable {
|
|
|
197
213
|
this.top = Math.max(0, Math.min(this.top, list.length - this.height));
|
|
198
214
|
if (cursor < this.top) this.top = cursor;
|
|
199
215
|
if (cursor >= this.top + this.height) this.top = cursor - this.height + 1;
|
|
200
|
-
|
|
216
|
+
const rows = list.slice(this.top, this.top + this.height).map(run => {
|
|
201
217
|
const live = children.live(run.id), tools = live ? [...live.tools.values()].map(t => t.name).join(', ') : '';
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
218
|
+
const status = live ? `${run.state === 'stopping' ? 'stopping' : run.state === 'waiting' ? 'waiting for children' : tools || (live.streaming ? 'responding' : 'working')} · ${elapsed(Date.now() - live.updated)} ago` : run.state;
|
|
219
|
+
return { run, task: `${' '.repeat(run.depth - 1)}${run.parentId ? '↳ ' : ''}${oneLine(run.task)}`, status, time: elapsed((run.ended ?? Date.now()) - run.started) };
|
|
220
|
+
});
|
|
221
|
+
// Columns: task (flexible) · status · duration (right-aligned); status yields first on narrow screens.
|
|
222
|
+
const timeWidth = Math.max(0, ...rows.map(r => r.time.length));
|
|
223
|
+
let statusWidth = Math.min(Math.max(0, ...rows.map(r => visibleWidth(r.status))), Math.floor(inner * 0.4));
|
|
224
|
+
if (inner - 2 - statusWidth - timeWidth - 4 < 12) statusWidth = 0;
|
|
225
|
+
const taskWidth = Math.max(1, inner - 2 - timeWidth - 2 - (statusWidth ? statusWidth + 2 : 0));
|
|
226
|
+
body = rows.map(({ run, task, status, time }) => {
|
|
227
|
+
const selected = run.id === this.selected;
|
|
228
|
+
const name = truncateToWidth(fit(task, taskWidth), taskWidth, '', true);
|
|
229
|
+
const meta = `${statusWidth ? `${truncateToWidth(fit(status, statusWidth), statusWidth, '', true)} ` : ''}${time.padStart(timeWidth)}`;
|
|
230
|
+
return selected ? color('accent', `→ ${name} ${meta}`) : ` ${name} ${color('muted', meta)}`;
|
|
205
231
|
});
|
|
206
|
-
if (!body.length) body.push('No agents yet. Ask the main agent to delegate a task.');
|
|
207
|
-
|
|
232
|
+
if (!body.length) body.push(color('muted', 'No agents yet. Ask the main agent to delegate a task.'));
|
|
233
|
+
info = `${list.filter(isActiveRun).length} active · ${list.length} saved${list.length ? ` · ${cursor + 1}/${list.length}` : ''}`;
|
|
208
234
|
hint = '↑↓ select · Enter inspect · m model · Tab usage · Esc close';
|
|
209
235
|
}
|
|
210
|
-
if (this.composer) { this.composer.focused = this.focused; body.push(...this.composer.render(
|
|
211
|
-
|
|
236
|
+
if (this.composer) { this.composer.focused = this.focused; body.push(...this.composer.render(inner)); hint = 'Enter sends guidance · Esc cancels message'; }
|
|
237
|
+
title = fit(title, Math.max(1, inner - visibleWidth(info) - 2));
|
|
238
|
+
const footer = wrapTextWithAnsi(this.error || (this.busy ? 'Updating agent…' : hint), inner).map(line => color(this.error ? 'error' : 'dim', line));
|
|
239
|
+
this.hintLines = footer.length;
|
|
240
|
+
const rule = color('border', '─'.repeat(Math.max(1, width)));
|
|
241
|
+
// Same layout as Pi's own selectors: rules above and below, content indented by one column.
|
|
242
|
+
const content = [spread(color('accent', title), color('dim', info), inner), '', ...body, '', ...footer].map(line => ` ${truncateToWidth(line, inner, '…')}`);
|
|
243
|
+
return [rule, ...content, rule].map(line => truncateToWidth(line, width));
|
|
212
244
|
}
|
|
213
245
|
}
|
|
214
246
|
|
|
247
|
+
let showing = false;
|
|
248
|
+
/** True while the panel is on screen; cleared before Pi restores the editor so the agent bar returns in the same frame. */
|
|
249
|
+
export const inspectorShowing = () => showing;
|
|
250
|
+
/** Takes the editor's place, like Pi's own selectors; Pi restores the editor and its draft on close. */
|
|
215
251
|
export function showInspector(ctx: ExtensionContext, options: Omit<Options, 'rows' | 'redraw' | 'done' | 'color' | 'context'>) {
|
|
216
|
-
return ctx.ui.custom<'model' | undefined>((tui, theme, _keys, done) =>
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
252
|
+
return ctx.ui.custom<'model' | undefined>((tui, theme, _keys, done) => {
|
|
253
|
+
showing = true;
|
|
254
|
+
return new Inspector({ ...options,
|
|
255
|
+
rows: () => tui.terminal.rows, redraw: () => tui.requestRender(), done: action => { showing = false; done(action); },
|
|
256
|
+
color: (tone, text) => theme.fg(tone, text), context: () => ctx.getContextUsage()?.tokens,
|
|
257
|
+
});
|
|
258
|
+
});
|
|
220
259
|
}
|
package/src/navigation.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CustomEditor, type ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
2
|
-
import { matchesKey, truncateToWidth, type KeyId } from '@earendil-works/pi-tui';
|
|
2
|
+
import { matchesKey, truncateToWidth, visibleWidth, type KeyId } from '@earendil-works/pi-tui';
|
|
3
3
|
import type { Children } from './agents.ts';
|
|
4
|
-
import type
|
|
4
|
+
import { inspectorShowing, type InspectorPage } from './inspector.ts';
|
|
5
5
|
import { isActiveRun } from './runs.ts';
|
|
6
6
|
|
|
7
7
|
/** Configured before extension registration; this also works in legacy terminals with F6. */
|
|
@@ -50,10 +50,13 @@ export function mountNavigation(ctx: ExtensionContext, children: Children, short
|
|
|
50
50
|
return {
|
|
51
51
|
invalidate() {},
|
|
52
52
|
render(width: number) {
|
|
53
|
+
if (inspectorShowing()) return []; // The open panel replaces the editor and this bar.
|
|
53
54
|
const list = children.history.list(), running = list.filter(isActiveRun).length;
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
|
|
55
|
+
const label = (page: InspectorPage, text: string) => navigation.selected === page ? theme.fg('accent', `› ${text}`) : theme.fg('muted', text);
|
|
56
|
+
const left = `${label('agents', `Agents: ${running} running · ${list.length - running} saved`)} ${label('usage', 'Usage')}`;
|
|
57
|
+
const hint = theme.fg('dim', navigation.selected ? '←→ select · Enter open · Esc input' : `${previous ? '' : '↓ select · '}${shortcut} inspect`);
|
|
58
|
+
const gap = width - visibleWidth(left) - visibleWidth(hint);
|
|
59
|
+
return [truncateToWidth(gap >= 3 ? `${left}${' '.repeat(gap)}${hint}` : `${left} ${hint}`, width)];
|
|
57
60
|
},
|
|
58
61
|
};
|
|
59
62
|
}, { placement: 'belowEditor' });
|