pi-optchat 0.7.1 → 0.7.2
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 +2 -2
- package/package.json +1 -1
- package/src/index.ts +11 -17
- package/src/settings-page.ts +34 -9
package/README.md
CHANGED
|
@@ -44,9 +44,9 @@ For headless use, pass `--optchat-profile work`.
|
|
|
44
44
|
| `/optchat` | Status and actions menu. |
|
|
45
45
|
| `/optchat profile` | Select or create a profile. Switching starts a fresh Pi session. |
|
|
46
46
|
| `/optchat settings` | This profile's settings: models, subagent levels and limits, previous exchange, summary size tolerance. |
|
|
47
|
-
| `/optchat model` | Compactor model and effort for this profile. |
|
|
47
|
+
| `/optchat model` | Compactor model and effort for this profile. Type to filter the models you are logged in to; the current one is marked. |
|
|
48
48
|
| `/optchat agents` | Live agent tree and saved run history. |
|
|
49
|
-
| `/optchat agents model` | Subagent model and effort for this profile. |
|
|
49
|
+
| `/optchat agents model` | Subagent model and effort for this profile, picked the same way. |
|
|
50
50
|
| `/optchat usage` | Token usage and cost estimates. |
|
|
51
51
|
| `/optchat activity` | Memory gauge: view size, summaries catching up, running agents. |
|
|
52
52
|
| `/optchat instructions` | Edit this profile's `AGENTS.md`. |
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { parseSkillBlock, type ExtensionAPI, type ExtensionContext, type Extensi
|
|
|
9
9
|
import { Type } from 'typebox';
|
|
10
10
|
import { Memory } from './memory.ts';
|
|
11
11
|
import { createCompressor } from './compactor.ts';
|
|
12
|
-
import { atomicWrite, createProfile, instructions, lastProfile, listProfiles, loadConfig, lockProfile, profilePath, rememberProfile, saveConfig, ProfileBusyError,
|
|
12
|
+
import { atomicWrite, createProfile, instructions, lastProfile, listProfiles, loadConfig, lockProfile, profilePath, rememberProfile, saveConfig, ProfileBusyError, type ProfileConfig } from './profiles.ts';
|
|
13
13
|
import { MASTER, VIEW_DOC } from './prompts.ts';
|
|
14
14
|
import { cachePayload, record } from './cache.ts';
|
|
15
15
|
import { asUser, boundedMessage, buildContext, logMessage, previousExchange, REPORT_TYPE, RUN_BOUNDARY, textContent, typedText } from './transcript.ts';
|
|
@@ -30,7 +30,7 @@ import { serveWindows } from './window-bridge.ts';
|
|
|
30
30
|
import { openConnectedWindow, registerConnectedRenderer } from './connected-window.ts';
|
|
31
31
|
import { createHandoffSummarizer } from './handoff.ts';
|
|
32
32
|
import { mainTitle, TabTitle } from './title.ts';
|
|
33
|
-
import { showSettings } from './settings-page.ts';
|
|
33
|
+
import { showModelPicker, showSettings } from './settings-page.ts';
|
|
34
34
|
|
|
35
35
|
const binding = 'optchat.profile';
|
|
36
36
|
const CONTINUITY = '\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; left out when very long), then the new input. Use that exact wording to understand follow-ups; older exchanges and previous tool output remain accessible through memory and zoom.';
|
|
@@ -357,20 +357,15 @@ export default function optchat(pi: ExtensionAPI) {
|
|
|
357
357
|
async execute(_id, args) { return result(await required().children.tell(args.id, args.message)); },
|
|
358
358
|
});
|
|
359
359
|
|
|
360
|
+
/** The settings page's options: every model Pi is logged in to, with the thinking levels it takes, sorted so each provider's models sit together. */
|
|
361
|
+
const settingsOptions = (ctx: ExtensionContext, a: Active) => ({ profile: a.name, config: a.config,
|
|
362
|
+
models: ctx.modelRegistry.getAvailable().map(m => ({ name: `${m.provider}/${m.id}`, thinking: getSupportedThinkingLevels(m) })).sort((x, y) => x.name.localeCompare(y.name)),
|
|
363
|
+
save: (config: ProfileConfig) => { saveConfig(a.dir, config); syncSearch(config); } });
|
|
360
364
|
const pickModel = async (ctx: ExtensionContext, role: 'compactor' | 'subagent') => {
|
|
361
|
-
const a = required()
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
if (!selected) return;
|
|
366
|
-
const model = available.find(m => `${m.provider}/${m.id}` === selected);
|
|
367
|
-
const levels = model ? getSupportedThinkingLevels(model) : THINKING;
|
|
368
|
-
const picked = await ctx.ui.select('Thinking level', [...levels]);
|
|
369
|
-
const thinking = levels.find(level => level === picked);
|
|
370
|
-
if (!thinking) return;
|
|
371
|
-
const separator = selected.indexOf('/');
|
|
372
|
-
a.config[role] = { provider: selected.slice(0, separator), model: selected.slice(separator + 1), thinking };
|
|
373
|
-
saveConfig(a.dir, a.config); ctx.ui.notify(`${role}: ${selected} (${thinking}); applies to new calls.`, 'info');
|
|
365
|
+
const a = required();
|
|
366
|
+
if (ctx.mode !== 'tui') throw new Error('Choosing a model requires interactive Pi. Edit config.json in the profile directory instead.');
|
|
367
|
+
const choice = await showModelPicker(ctx, role, settingsOptions(ctx, a));
|
|
368
|
+
if (choice) ctx.ui.notify(`${role}: ${choice.provider}/${choice.model} (${choice.thinking}); applies to new calls.`, 'info');
|
|
374
369
|
};
|
|
375
370
|
const inspect = async (ctx: ExtensionContext, page: InspectorPage) => {
|
|
376
371
|
if (inspectorController) return;
|
|
@@ -451,8 +446,7 @@ export default function optchat(pi: ExtensionAPI) {
|
|
|
451
446
|
if (action === 'settings') {
|
|
452
447
|
const a = required();
|
|
453
448
|
if (ctx.mode !== 'tui') throw new Error('/optchat settings requires interactive Pi. Edit config.json in the profile directory instead.');
|
|
454
|
-
return showSettings(ctx,
|
|
455
|
-
save: config => { saveConfig(a.dir, config); syncSearch(config); } });
|
|
449
|
+
return showSettings(ctx, settingsOptions(ctx, a));
|
|
456
450
|
}
|
|
457
451
|
if (action === 'model') return pickModel(ctx, 'compactor');
|
|
458
452
|
if (action === 'agents model') return pickModel(ctx, 'subagent');
|
package/src/settings-page.ts
CHANGED
|
@@ -71,7 +71,7 @@ class ModelStep extends Container {
|
|
|
71
71
|
}
|
|
72
72
|
private pickModel() {
|
|
73
73
|
const { theme, current } = this, filter = new Input();
|
|
74
|
-
const items: SelectItem[] = this.models.map(({ name }) => ({ value: name, label: name }));
|
|
74
|
+
const items: SelectItem[] = this.models.map(({ name }) => ({ value: name, label: name, description: name === modelName(current) ? 'current' : undefined }));
|
|
75
75
|
let list = new SelectList(items, 10, listTheme(theme));
|
|
76
76
|
const build = () => {
|
|
77
77
|
list = new SelectList(filter.getValue() ? fuzzyFilter(items, filter.getValue(), i => i.value) : items, 10, listTheme(theme));
|
|
@@ -106,6 +106,21 @@ class ModelStep extends Container {
|
|
|
106
106
|
handleInput(data: string) { this.active.handleInput(data); }
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/** Saves a change to the profile config and applies it in memory; returns the reason if it couldn't be written. */
|
|
110
|
+
function update(o: Options, patch: Partial<ProfileConfig>) {
|
|
111
|
+
try { o.save({ ...o.config, ...patch }); }
|
|
112
|
+
catch (error) { return `Could not save: ${error instanceof Error ? error.message : String(error)}`; }
|
|
113
|
+
Object.assign(o.config, patch);
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
/** Bordered, titled like Pi's own dialogs. */
|
|
117
|
+
function frame(theme: Theme, title: string, profile: string, body: Component[]) {
|
|
118
|
+
const page = new Container();
|
|
119
|
+
for (const child of [new DynamicBorder(s => theme.fg('border', s)), new Text(`${theme.bold(theme.fg('accent', title))}${theme.fg('muted', ` · ${profile}`)}`, 1, 0),
|
|
120
|
+
...body, new DynamicBorder(s => theme.fg('border', s))]) page.addChild(child);
|
|
121
|
+
return page;
|
|
122
|
+
}
|
|
123
|
+
|
|
109
124
|
/** The page itself: Pi's settings list over this profile's config, one row per setting, saved on change. */
|
|
110
125
|
export function settingsPage(theme: Theme, o: Options, close: () => void, redraw = () => {}) {
|
|
111
126
|
const { config } = o;
|
|
@@ -113,11 +128,9 @@ export function settingsPage(theme: Theme, o: Options, close: () => void, redraw
|
|
|
113
128
|
const marker = (same: boolean, fallback: string) => theme.fg('dim', same ? ' default' : ` default ${fallback}`);
|
|
114
129
|
const value = (key: SettingKey, current: number | boolean) => `${show(key, current)}${marker(current === SETTINGS[key].default, show(key, SETTINGS[key].default))}`;
|
|
115
130
|
const apply = (patch: Partial<ProfileConfig>, what: string, applies: string) => {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
notice.setText(theme.fg('success', `Saved ${what}. `) + theme.fg('muted', applies));
|
|
120
|
-
return undefined;
|
|
131
|
+
const problem = update(o, patch);
|
|
132
|
+
if (!problem) notice.setText(theme.fg('success', `Saved ${what}. `) + theme.fg('muted', applies));
|
|
133
|
+
return problem;
|
|
121
134
|
};
|
|
122
135
|
const models = (['compactor', 'subagent'] as const).map((role): SettingItem => {
|
|
123
136
|
const { label, description, applies } = ROLES[role];
|
|
@@ -152,13 +165,25 @@ export function settingsPage(theme: Theme, o: Options, close: () => void, redraw
|
|
|
152
165
|
const problem = apply({ [key]: on }, `${SETTINGS[key].label.toLowerCase()} ${on ? 'on' : 'off'}`, SETTINGS[key].applies);
|
|
153
166
|
if (problem) { list.updateValue(key, value(key, config[key])); notice.setText(theme.fg('error', problem)); }
|
|
154
167
|
}, close);
|
|
155
|
-
const page =
|
|
156
|
-
for (const child of [new DynamicBorder(s => theme.fg('border', s)), new Text(`${theme.bold(theme.fg('accent', 'OptChat settings'))}${theme.fg('muted', ` · ${o.profile}`)}`, 1, 0),
|
|
168
|
+
const page = frame(theme, 'OptChat settings', o.profile, [
|
|
157
169
|
new Text(theme.fg('dim', 'Saved as you change them · Victor\'s recipe by default, except Previous exchange and Summary size tolerance'), 1, 0),
|
|
158
|
-
new Spacer(1), list, notice
|
|
170
|
+
new Spacer(1), list, notice]);
|
|
159
171
|
return Object.assign(page, { handleInput: (data: string) => { notice.setText(''); list.handleInput(data); redraw(); } });
|
|
160
172
|
}
|
|
161
173
|
|
|
174
|
+
/** Just the settings page's model step, for /optchat model and /optchat agents model: closes with the saved choice, or nothing on Esc. */
|
|
175
|
+
export function modelPicker(theme: Theme, role: Role, o: Options, close: (choice?: ModelChoice) => void, redraw = () => {}) {
|
|
176
|
+
const step = new ModelStep(theme, role, o.models, o.config[role], choice => {
|
|
177
|
+
const problem = update(o, { [role]: choice });
|
|
178
|
+
if (!problem) close(choice);
|
|
179
|
+
return problem;
|
|
180
|
+
}, () => close());
|
|
181
|
+
return Object.assign(frame(theme, 'OptChat', o.profile, [new Spacer(1), step]), { handleInput: (data: string) => { step.handleInput(data); redraw(); } });
|
|
182
|
+
}
|
|
183
|
+
|
|
162
184
|
export function showSettings(ctx: ExtensionContext, o: Options) {
|
|
163
185
|
return ctx.ui.custom<void>((tui, theme, _keys, done) => settingsPage(theme, o, () => done(undefined), () => tui.requestRender()));
|
|
164
186
|
}
|
|
187
|
+
export function showModelPicker(ctx: ExtensionContext, role: Role, o: Options) {
|
|
188
|
+
return ctx.ui.custom<ModelChoice | undefined>((tui, theme, _keys, done) => modelPicker(theme, role, o, done, () => tui.requestRender()));
|
|
189
|
+
}
|