@oh-my-pi/pi-coding-agent 15.9.0 → 15.9.3
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 +67 -0
- package/dist/types/cli/dry-balance-cli.d.ts +104 -0
- package/dist/types/cli/update-cli.d.ts +15 -1
- package/dist/types/commands/dry-balance.d.ts +31 -0
- package/dist/types/config/append-only-context-mode.d.ts +8 -0
- package/dist/types/config/model-registry.d.ts +5 -0
- package/dist/types/config/models-config-schema.d.ts +18 -0
- package/dist/types/config/settings-schema.d.ts +3 -3
- package/dist/types/config/settings.d.ts +11 -0
- package/dist/types/discovery/helpers.d.ts +1 -0
- package/dist/types/exa/mcp-client.d.ts +2 -1
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -3
- package/dist/types/hindsight/bank.d.ts +17 -9
- package/dist/types/hindsight/mental-models.d.ts +1 -1
- package/dist/types/hindsight/state.d.ts +9 -3
- package/dist/types/mcp/json-rpc.d.ts +6 -1
- package/dist/types/mcp/manager.d.ts +1 -1
- package/dist/types/mcp/tool-bridge.d.ts +4 -0
- package/dist/types/mnemopi/state.d.ts +2 -2
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -0
- package/dist/types/modes/components/extensions/extension-dashboard.d.ts +1 -0
- package/dist/types/modes/components/plugin-settings.d.ts +40 -8
- package/dist/types/modes/components/session-selector.d.ts +8 -3
- package/dist/types/modes/components/settings-selector.d.ts +1 -1
- package/dist/types/modes/components/transcript-container.d.ts +3 -2
- package/dist/types/modes/utils/keybinding-matchers.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +13 -1
- package/dist/types/session/auth-storage.d.ts +2 -2
- package/dist/types/session/history-storage.d.ts +3 -4
- package/dist/types/session/messages.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +1 -0
- package/dist/types/slash-commands/types.d.ts +17 -4
- package/dist/types/task/types.d.ts +2 -0
- package/dist/types/tiny/text.d.ts +17 -0
- package/dist/types/tools/index.d.ts +16 -0
- package/dist/types/tools/path-utils.d.ts +11 -0
- package/dist/types/web/search/providers/base.d.ts +14 -0
- package/dist/types/web/search/providers/exa.d.ts +9 -0
- package/dist/types/web/search/providers/perplexity.d.ts +2 -2
- package/dist/types/web/search/types.d.ts +2 -1
- package/package.json +9 -9
- package/src/cli/dry-balance-cli.ts +823 -0
- package/src/cli/session-picker.ts +1 -0
- package/src/cli/update-cli.ts +54 -2
- package/src/cli-commands.ts +1 -0
- package/src/commands/completions.ts +1 -1
- package/src/commands/dry-balance.ts +43 -0
- package/src/config/append-only-context-mode.ts +37 -0
- package/src/config/model-registry.ts +6 -0
- package/src/config/models-config-schema.ts +3 -0
- package/src/config/settings-schema.ts +2 -2
- package/src/config/settings.ts +38 -0
- package/src/discovery/builtin-rules/ts-no-tiny-functions.md +1 -0
- package/src/discovery/github.ts +37 -1
- package/src/discovery/helpers.ts +3 -1
- package/src/exa/mcp-client.ts +11 -5
- package/src/extensibility/plugins/legacy-pi-compat.ts +245 -25
- package/src/hindsight/backend.ts +184 -35
- package/src/hindsight/bank.ts +32 -22
- package/src/hindsight/mental-models.ts +1 -1
- package/src/hindsight/state.ts +21 -7
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/internal-urls/omp-protocol.ts +8 -2
- package/src/main.ts +4 -2
- package/src/mcp/json-rpc.ts +8 -0
- package/src/mcp/manager.ts +40 -21
- package/src/mcp/render.ts +3 -0
- package/src/mcp/tool-bridge.ts +10 -2
- package/src/mcp/transports/http.ts +33 -16
- package/src/mnemopi/state.ts +4 -4
- package/src/modes/acp/acp-agent.ts +168 -3
- package/src/modes/components/agent-dashboard.ts +103 -31
- package/src/modes/components/extensions/extension-dashboard.ts +56 -10
- package/src/modes/components/history-search.ts +128 -14
- package/src/modes/components/plugin-settings.ts +270 -36
- package/src/modes/components/session-selector.ts +45 -14
- package/src/modes/components/settings-selector.ts +1 -1
- package/src/modes/components/tips.txt +5 -1
- package/src/modes/components/transcript-container.ts +35 -6
- package/src/modes/components/tree-selector.ts +29 -2
- package/src/modes/controllers/command-controller.ts +4 -3
- package/src/modes/controllers/input-controller.ts +18 -7
- package/src/modes/controllers/selector-controller.ts +30 -19
- package/src/modes/interactive-mode.ts +38 -3
- package/src/modes/setup-wizard/scenes/sign-in.ts +27 -7
- package/src/modes/utils/keybinding-matchers.ts +10 -0
- package/src/prompts/agents/explore.md +1 -0
- package/src/prompts/agents/librarian.md +1 -0
- package/src/prompts/dry-balance-bench.md +8 -0
- package/src/prompts/steering/user-interjection.md +10 -0
- package/src/prompts/system/agent-creation-architect.md +1 -26
- package/src/prompts/system/system-prompt.md +143 -145
- package/src/prompts/system/title-system.md +3 -2
- package/src/prompts/tools/browser.md +29 -29
- package/src/prompts/tools/render-mermaid.md +2 -2
- package/src/sdk.ts +87 -30
- package/src/session/agent-session.ts +96 -14
- package/src/session/auth-storage.ts +4 -0
- package/src/session/history-storage.ts +11 -18
- package/src/session/messages.ts +80 -0
- package/src/session/session-manager.ts +7 -1
- package/src/slash-commands/types.ts +27 -10
- package/src/task/executor.ts +6 -2
- package/src/task/index.ts +8 -7
- package/src/task/types.ts +2 -0
- package/src/tiny/text.ts +112 -1
- package/src/tools/bash.ts +3 -4
- package/src/tools/index.ts +16 -0
- package/src/tools/job.ts +3 -3
- package/src/tools/memory-recall.ts +1 -1
- package/src/tools/memory-reflect.ts +3 -3
- package/src/tools/path-utils.ts +21 -0
- package/src/tools/search.ts +18 -1
- package/src/tools/ssh.ts +26 -10
- package/src/tools/write.ts +14 -2
- package/src/tui/status-line.ts +15 -4
- package/src/utils/file-mentions.ts +7 -107
- package/src/utils/title-generator.ts +66 -38
- package/src/web/search/index.ts +3 -1
- package/src/web/search/provider.ts +1 -1
- package/src/web/search/providers/base.ts +17 -0
- package/src/web/search/providers/exa.ts +111 -7
- package/src/web/search/providers/perplexity.ts +8 -4
- package/src/web/search/types.ts +2 -1
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Plugin settings UI components.
|
|
3
3
|
*
|
|
4
4
|
* Provides a hierarchical settings interface:
|
|
5
|
-
* - Plugin list (
|
|
6
|
-
* -
|
|
5
|
+
* - Plugin list (npm plugins + marketplace plugins)
|
|
6
|
+
* - npm plugin detail (enable/disable, features, config)
|
|
7
|
+
* - Marketplace plugin detail (enable/disable + read-only metadata)
|
|
7
8
|
* - Feature toggles
|
|
8
9
|
* - Config value editor
|
|
9
10
|
*/
|
|
@@ -17,9 +18,20 @@ import {
|
|
|
17
18
|
Spacer,
|
|
18
19
|
Text,
|
|
19
20
|
} from "@oh-my-pi/pi-tui";
|
|
21
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
22
|
+
import { clearPluginRootsAndCaches, resolveOrDefaultProjectRegistryPath } from "../../discovery/helpers";
|
|
20
23
|
import { PluginManager } from "../../extensibility/plugins/manager";
|
|
24
|
+
import type { InstalledPluginSummary } from "../../extensibility/plugins/marketplace";
|
|
25
|
+
import {
|
|
26
|
+
getInstalledPluginsRegistryPath,
|
|
27
|
+
getMarketplacesCacheDir,
|
|
28
|
+
getMarketplacesRegistryPath,
|
|
29
|
+
getPluginsCacheDir,
|
|
30
|
+
MarketplaceManager,
|
|
31
|
+
} from "../../extensibility/plugins/marketplace";
|
|
21
32
|
import type { InstalledPlugin, PluginSettingSchema } from "../../extensibility/plugins/types";
|
|
22
33
|
import { getSelectListTheme, getSettingsListTheme, theme } from "../../modes/theme/theme";
|
|
34
|
+
import { shortenPath } from "../../tools/render-utils";
|
|
23
35
|
import { DynamicBorder } from "./dynamic-border";
|
|
24
36
|
|
|
25
37
|
/**
|
|
@@ -41,20 +53,53 @@ export function handleInputOrEscape(
|
|
|
41
53
|
// Plugin List Component
|
|
42
54
|
// =============================================================================
|
|
43
55
|
|
|
56
|
+
/**
|
|
57
|
+
* One row in the unified plugin list. npm and marketplace plugins live in
|
|
58
|
+
* separate registries with different shapes, so a tagged union keeps both
|
|
59
|
+
* paths type-safe end-to-end (list rendering, value lookup, detail callback).
|
|
60
|
+
*/
|
|
61
|
+
export type PluginListEntry =
|
|
62
|
+
| { kind: "npm"; plugin: InstalledPlugin }
|
|
63
|
+
| { kind: "marketplace"; plugin: InstalledPluginSummary };
|
|
64
|
+
|
|
44
65
|
export interface PluginListCallbacks {
|
|
45
|
-
|
|
66
|
+
onNpmSelect: (plugin: InstalledPlugin) => void;
|
|
67
|
+
onMarketplaceSelect: (plugin: InstalledPluginSummary) => void;
|
|
46
68
|
onCancel: () => void;
|
|
47
69
|
}
|
|
48
70
|
|
|
49
71
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
72
|
+
* True when the marketplace summary's first entry is not explicitly disabled.
|
|
73
|
+
* Mirrors the `/plugins list` convention: a missing `enabled` flag means enabled.
|
|
74
|
+
*/
|
|
75
|
+
function marketplaceEnabled(summary: InstalledPluginSummary): boolean {
|
|
76
|
+
return summary.entries[0]?.enabled !== false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Stable SelectList value for a list entry. Combined with `findEntryByValue`
|
|
81
|
+
* this keeps lookup correct even when the same plugin id exists in both user
|
|
82
|
+
* and project scope (one of which is `shadowedBy: "project"`).
|
|
83
|
+
*/
|
|
84
|
+
function entryValue(entry: PluginListEntry): string {
|
|
85
|
+
if (entry.kind === "npm") return `npm:${entry.plugin.name}`;
|
|
86
|
+
return `mkt:${entry.plugin.scope}:${entry.plugin.id}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function findEntryByValue(entries: ReadonlyArray<PluginListEntry>, value: string): PluginListEntry | undefined {
|
|
90
|
+
return entries.find(e => entryValue(e) === value);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Shows installed plugins from both registries (npm + marketplace) with
|
|
95
|
+
* enable/disable status, scope tag, and shadow indicator. Selecting an entry
|
|
96
|
+
* fans out to the kind-specific detail callback.
|
|
52
97
|
*/
|
|
53
98
|
export class PluginListComponent extends Container {
|
|
54
99
|
readonly #selectList: SelectList;
|
|
55
100
|
|
|
56
101
|
constructor(
|
|
57
|
-
private readonly
|
|
102
|
+
private readonly entries: ReadonlyArray<PluginListEntry>,
|
|
58
103
|
callbacks: PluginListCallbacks,
|
|
59
104
|
) {
|
|
60
105
|
super();
|
|
@@ -64,53 +109,87 @@ export class PluginListComponent extends Container {
|
|
|
64
109
|
this.addChild(new Text(theme.bold(theme.fg("accent", " Plugins")), 0, 0));
|
|
65
110
|
this.addChild(new Spacer(1));
|
|
66
111
|
|
|
67
|
-
if (
|
|
112
|
+
if (entries.length === 0) {
|
|
68
113
|
this.addChild(new Text(theme.fg("muted", " No plugins installed"), 0, 0));
|
|
69
114
|
this.addChild(new Spacer(1));
|
|
70
|
-
this.addChild(new Text(theme.fg("dim", " Install
|
|
115
|
+
this.addChild(new Text(theme.fg("dim", " Install npm plugins: omp plugin install <package>"), 0, 0));
|
|
116
|
+
this.addChild(
|
|
117
|
+
new Text(theme.fg("dim", " Install marketplace plugins: omp plugin install <name>@<marketplace>"), 0, 0),
|
|
118
|
+
);
|
|
71
119
|
this.addChild(new Spacer(1));
|
|
72
120
|
this.addChild(new DynamicBorder());
|
|
73
121
|
|
|
74
|
-
//
|
|
122
|
+
// Empty list still handles Escape so the user can leave the panel.
|
|
75
123
|
this.#selectList = new SelectList([], 1, getSelectListTheme());
|
|
76
124
|
this.#selectList.onCancel = callbacks.onCancel;
|
|
77
125
|
return;
|
|
78
126
|
}
|
|
79
127
|
|
|
80
|
-
const items: SelectItem[] =
|
|
128
|
+
const items: SelectItem[] = entries.map(entry => this.#renderItem(entry));
|
|
129
|
+
|
|
130
|
+
// Marketplace plugin ids (`name@marketplace`) routinely run past the
|
|
131
|
+
// SelectList default primary column (32 chars). Widen the bound so the
|
|
132
|
+
// id remains readable; the description gets whatever width is left.
|
|
133
|
+
this.#selectList = new SelectList(items, Math.min(items.length, 8), getSelectListTheme(), {
|
|
134
|
+
minPrimaryColumnWidth: 24,
|
|
135
|
+
maxPrimaryColumnWidth: 64,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
this.#selectList.onSelect = item => {
|
|
139
|
+
const found = findEntryByValue(this.entries, item.value);
|
|
140
|
+
if (!found) return;
|
|
141
|
+
if (found.kind === "npm") callbacks.onNpmSelect(found.plugin);
|
|
142
|
+
else callbacks.onMarketplaceSelect(found.plugin);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
this.#selectList.onCancel = callbacks.onCancel;
|
|
146
|
+
|
|
147
|
+
this.addChild(this.#selectList);
|
|
148
|
+
this.addChild(new Spacer(1));
|
|
149
|
+
this.addChild(new Text(theme.fg("dim", " Enter to configure · Esc to go back"), 0, 0));
|
|
150
|
+
this.addChild(new DynamicBorder());
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
#renderItem(entry: PluginListEntry): SelectItem {
|
|
154
|
+
const kindBadge = theme.fg("dim", entry.kind === "npm" ? "[npm]" : "[marketplace]");
|
|
155
|
+
|
|
156
|
+
if (entry.kind === "npm") {
|
|
157
|
+
const p = entry.plugin;
|
|
81
158
|
const status = p.enabled
|
|
82
159
|
? theme.fg("success", theme.status.enabled)
|
|
83
160
|
: theme.fg("muted", theme.status.disabled);
|
|
84
161
|
const featureCount = p.manifest.features ? Object.keys(p.manifest.features).length : 0;
|
|
85
162
|
const enabledCount = p.enabledFeatures?.length ?? featureCount;
|
|
86
163
|
|
|
87
|
-
let details =
|
|
164
|
+
let details = `${kindBadge} ${theme.sep.dot} v${p.version}`;
|
|
88
165
|
if (featureCount > 0) {
|
|
89
166
|
details += ` ${theme.sep.dot} ${enabledCount}/${featureCount} features`;
|
|
90
167
|
}
|
|
91
168
|
|
|
92
169
|
return {
|
|
93
|
-
value:
|
|
170
|
+
value: entryValue(entry),
|
|
94
171
|
label: `${status} ${p.name}`,
|
|
95
172
|
description: details,
|
|
96
173
|
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
this.#selectList = new SelectList(items, Math.min(items.length, 8), getSelectListTheme());
|
|
174
|
+
}
|
|
100
175
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
176
|
+
const summary = entry.plugin;
|
|
177
|
+
const enabled = marketplaceEnabled(summary);
|
|
178
|
+
const status = enabled ? theme.fg("success", theme.status.enabled) : theme.fg("muted", theme.status.disabled);
|
|
179
|
+
const scopeTag = theme.fg("dim", `[${summary.scope}]`);
|
|
180
|
+
const shadowMarker = summary.shadowedBy ? ` ${theme.fg("warning", theme.status.shadowed)}` : "";
|
|
181
|
+
const version = summary.entries[0]?.version ?? "?";
|
|
107
182
|
|
|
108
|
-
|
|
183
|
+
let details = `${kindBadge} ${scopeTag} ${theme.sep.dot} v${version}`;
|
|
184
|
+
if (summary.shadowedBy) {
|
|
185
|
+
details += ` ${theme.sep.dot} shadowed by ${summary.shadowedBy}`;
|
|
186
|
+
}
|
|
109
187
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
188
|
+
return {
|
|
189
|
+
value: entryValue(entry),
|
|
190
|
+
label: `${status} ${summary.id}${shadowMarker}`,
|
|
191
|
+
description: details,
|
|
192
|
+
};
|
|
114
193
|
}
|
|
115
194
|
|
|
116
195
|
handleInput(data: string): void {
|
|
@@ -296,6 +375,98 @@ export class PluginDetailComponent extends Container {
|
|
|
296
375
|
}
|
|
297
376
|
}
|
|
298
377
|
|
|
378
|
+
// =============================================================================
|
|
379
|
+
// Marketplace Plugin Detail Component
|
|
380
|
+
// =============================================================================
|
|
381
|
+
|
|
382
|
+
export interface MarketplacePluginDetailCallbacks {
|
|
383
|
+
onEnabledChange: (enabled: boolean) => void;
|
|
384
|
+
onBack: () => void;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Detail view for a marketplace plugin. Marketplace plugins do not declare
|
|
389
|
+
* features or settings, so the panel exposes a single enable/disable toggle
|
|
390
|
+
* plus the read-only metadata from the installed-plugins registry.
|
|
391
|
+
*/
|
|
392
|
+
export class MarketplacePluginDetailComponent extends Container {
|
|
393
|
+
#settingsList: SettingsList;
|
|
394
|
+
|
|
395
|
+
constructor(
|
|
396
|
+
private plugin: InstalledPluginSummary,
|
|
397
|
+
private readonly callbacks: MarketplacePluginDetailCallbacks,
|
|
398
|
+
) {
|
|
399
|
+
super();
|
|
400
|
+
|
|
401
|
+
const entry = plugin.entries[0];
|
|
402
|
+
const enabled = marketplaceEnabled(plugin);
|
|
403
|
+
|
|
404
|
+
// Header
|
|
405
|
+
this.addChild(new DynamicBorder());
|
|
406
|
+
this.addChild(new Text(theme.bold(theme.fg("accent", ` ${plugin.id}`)), 0, 0));
|
|
407
|
+
|
|
408
|
+
const subtitleParts = [`[${plugin.scope}]`];
|
|
409
|
+
if (plugin.shadowedBy) subtitleParts.push(`${theme.status.shadowed} shadowed by ${plugin.shadowedBy}`);
|
|
410
|
+
this.addChild(new Text(theme.fg("muted", ` ${subtitleParts.join(" ")}`), 0, 0));
|
|
411
|
+
this.addChild(new Spacer(1));
|
|
412
|
+
|
|
413
|
+
const items: SettingItem[] = [
|
|
414
|
+
{
|
|
415
|
+
id: "__enabled__",
|
|
416
|
+
label: "Enabled",
|
|
417
|
+
description: "Enable or disable this marketplace plugin",
|
|
418
|
+
currentValue: enabled ? "true" : "false",
|
|
419
|
+
values: ["true", "false"],
|
|
420
|
+
},
|
|
421
|
+
];
|
|
422
|
+
|
|
423
|
+
this.#settingsList = new SettingsList(
|
|
424
|
+
items,
|
|
425
|
+
items.length,
|
|
426
|
+
getSettingsListTheme(),
|
|
427
|
+
(id, newValue) => {
|
|
428
|
+
if (id === "__enabled__") {
|
|
429
|
+
const next = newValue === "true";
|
|
430
|
+
this.callbacks.onEnabledChange(next);
|
|
431
|
+
this.plugin = {
|
|
432
|
+
...this.plugin,
|
|
433
|
+
entries: this.plugin.entries.map(e => ({ ...e, enabled: next })),
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
this.callbacks.onBack,
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
this.addChild(this.#settingsList);
|
|
441
|
+
this.addChild(new Spacer(1));
|
|
442
|
+
|
|
443
|
+
// Read-only metadata. SettingsList rejects items without `values`/`submenu`,
|
|
444
|
+
// so we render the metadata as plain text rows beneath the toggle.
|
|
445
|
+
this.addChild(new Text(theme.fg("dim", ` version ${entry?.version ?? "(unknown)"}`), 0, 0));
|
|
446
|
+
this.addChild(new Text(theme.fg("dim", ` scope ${plugin.scope}`), 0, 0));
|
|
447
|
+
this.addChild(
|
|
448
|
+
new Text(
|
|
449
|
+
theme.fg("dim", ` install path ${entry?.installPath ? shortenPath(entry.installPath) : "(unknown)"}`),
|
|
450
|
+
0,
|
|
451
|
+
0,
|
|
452
|
+
),
|
|
453
|
+
);
|
|
454
|
+
this.addChild(new Text(theme.fg("dim", ` installed at ${entry?.installedAt ?? "(unknown)"}`), 0, 0));
|
|
455
|
+
this.addChild(new Text(theme.fg("dim", ` last updated ${entry?.lastUpdated ?? "(unknown)"}`), 0, 0));
|
|
456
|
+
if (entry?.gitCommitSha) {
|
|
457
|
+
this.addChild(new Text(theme.fg("dim", ` git sha ${entry.gitCommitSha}`), 0, 0));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
this.addChild(new Spacer(1));
|
|
461
|
+
this.addChild(new Text(theme.fg("dim", " Enter to toggle · Esc to go back"), 0, 0));
|
|
462
|
+
this.addChild(new DynamicBorder());
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
handleInput(data: string): void {
|
|
466
|
+
this.#settingsList.handleInput(data);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
299
470
|
// =============================================================================
|
|
300
471
|
// Config Submenus
|
|
301
472
|
// =============================================================================
|
|
@@ -408,7 +579,7 @@ class ConfigInputSubmenu extends Container {
|
|
|
408
579
|
|
|
409
580
|
export interface PluginSettingsCallbacks {
|
|
410
581
|
onClose: () => void;
|
|
411
|
-
onPluginChanged: () => void
|
|
582
|
+
onPluginChanged: () => void | Promise<void>;
|
|
412
583
|
}
|
|
413
584
|
|
|
414
585
|
/** Component with handleInput method */
|
|
@@ -421,31 +592,66 @@ interface InputHandler {
|
|
|
421
592
|
* Manages navigation between plugin list and plugin detail views.
|
|
422
593
|
*/
|
|
423
594
|
export class PluginSettingsComponent extends Container {
|
|
595
|
+
#cwd: string;
|
|
424
596
|
#manager: PluginManager;
|
|
425
597
|
#viewComponent: (Container & InputHandler) | null = null;
|
|
426
598
|
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: state tracking for view management
|
|
427
|
-
#currentView: "list" | "detail" = "list";
|
|
599
|
+
#currentView: "list" | "npm-detail" | "marketplace-detail" = "list";
|
|
428
600
|
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: state tracking for view management
|
|
429
601
|
#currentPlugin: InstalledPlugin | null = null;
|
|
602
|
+
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: state tracking for view management
|
|
603
|
+
#currentMarketplacePlugin: InstalledPluginSummary | null = null;
|
|
430
604
|
|
|
431
605
|
constructor(
|
|
432
606
|
cwd: string,
|
|
433
607
|
private readonly callbacks: PluginSettingsCallbacks,
|
|
434
608
|
) {
|
|
435
609
|
super();
|
|
610
|
+
this.#cwd = cwd;
|
|
436
611
|
this.#manager = new PluginManager(cwd);
|
|
437
612
|
this.#showPluginList();
|
|
438
613
|
}
|
|
439
614
|
|
|
615
|
+
async #buildMarketplaceManager(): Promise<MarketplaceManager> {
|
|
616
|
+
return new MarketplaceManager({
|
|
617
|
+
marketplacesRegistryPath: getMarketplacesRegistryPath(),
|
|
618
|
+
installedRegistryPath: getInstalledPluginsRegistryPath(),
|
|
619
|
+
projectInstalledRegistryPath: await resolveOrDefaultProjectRegistryPath(this.#cwd),
|
|
620
|
+
marketplacesCacheDir: getMarketplacesCacheDir(),
|
|
621
|
+
pluginsCacheDir: getPluginsCacheDir(),
|
|
622
|
+
clearPluginRootsCache: clearPluginRootsAndCaches,
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
|
|
440
626
|
async #showPluginList(): Promise<void> {
|
|
441
627
|
this.#currentView = "list";
|
|
442
628
|
this.#currentPlugin = null;
|
|
629
|
+
this.#currentMarketplacePlugin = null;
|
|
443
630
|
this.clear();
|
|
444
631
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
632
|
+
// Surface marketplace failures without taking the npm path down with it —
|
|
633
|
+
// the registry can fail to load (corrupt JSON, missing project root) and
|
|
634
|
+
// the user still benefits from seeing their npm plugins.
|
|
635
|
+
const [npmPlugins, marketplacePlugins] = await Promise.all([
|
|
636
|
+
this.#manager.list(),
|
|
637
|
+
this.#buildMarketplaceManager()
|
|
638
|
+
.then(mgr => mgr.listInstalledPlugins())
|
|
639
|
+
.catch(err => {
|
|
640
|
+
logger.error("Settings → Plugins: failed to list marketplace plugins", {
|
|
641
|
+
error: err instanceof Error ? err.message : String(err),
|
|
642
|
+
});
|
|
643
|
+
return [] as InstalledPluginSummary[];
|
|
644
|
+
}),
|
|
645
|
+
]);
|
|
646
|
+
|
|
647
|
+
const entries: PluginListEntry[] = [
|
|
648
|
+
...npmPlugins.map(plugin => ({ kind: "npm" as const, plugin })),
|
|
649
|
+
...marketplacePlugins.map(plugin => ({ kind: "marketplace" as const, plugin })),
|
|
650
|
+
];
|
|
651
|
+
|
|
652
|
+
this.#viewComponent = new PluginListComponent(entries, {
|
|
653
|
+
onNpmSelect: plugin => this.#showPluginDetail(plugin),
|
|
654
|
+
onMarketplaceSelect: plugin => this.#showMarketplaceDetail(plugin),
|
|
449
655
|
onCancel: () => this.callbacks.onClose(),
|
|
450
656
|
});
|
|
451
657
|
|
|
@@ -453,14 +659,15 @@ export class PluginSettingsComponent extends Container {
|
|
|
453
659
|
}
|
|
454
660
|
|
|
455
661
|
#showPluginDetail(plugin: InstalledPlugin): void {
|
|
456
|
-
this.#currentView = "detail";
|
|
662
|
+
this.#currentView = "npm-detail";
|
|
457
663
|
this.#currentPlugin = plugin;
|
|
664
|
+
this.#currentMarketplacePlugin = null;
|
|
458
665
|
this.clear();
|
|
459
666
|
|
|
460
667
|
this.#viewComponent = new PluginDetailComponent(plugin, this.#manager, {
|
|
461
668
|
onEnabledChange: async enabled => {
|
|
462
669
|
await this.#manager.setEnabled(plugin.name, enabled);
|
|
463
|
-
this.callbacks.onPluginChanged();
|
|
670
|
+
await this.callbacks.onPluginChanged();
|
|
464
671
|
},
|
|
465
672
|
onFeatureChange: async (feature, enabled) => {
|
|
466
673
|
const current = new Set((await this.#manager.getEnabledFeatures(plugin.name)) ?? []);
|
|
@@ -470,11 +677,38 @@ export class PluginSettingsComponent extends Container {
|
|
|
470
677
|
current.delete(feature);
|
|
471
678
|
}
|
|
472
679
|
await this.#manager.setEnabledFeatures(plugin.name, [...current]);
|
|
473
|
-
this.callbacks.onPluginChanged();
|
|
680
|
+
await this.callbacks.onPluginChanged();
|
|
474
681
|
},
|
|
475
682
|
onConfigChange: async (key, value) => {
|
|
476
683
|
await this.#manager.setPluginSetting(plugin.name, key, value);
|
|
477
|
-
this.callbacks.onPluginChanged();
|
|
684
|
+
await this.callbacks.onPluginChanged();
|
|
685
|
+
},
|
|
686
|
+
onBack: () => this.#showPluginList(),
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
this.addChild(this.#viewComponent);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
#showMarketplaceDetail(plugin: InstalledPluginSummary): void {
|
|
693
|
+
this.#currentView = "marketplace-detail";
|
|
694
|
+
this.#currentPlugin = null;
|
|
695
|
+
this.#currentMarketplacePlugin = plugin;
|
|
696
|
+
this.clear();
|
|
697
|
+
|
|
698
|
+
this.#viewComponent = new MarketplacePluginDetailComponent(plugin, {
|
|
699
|
+
onEnabledChange: async enabled => {
|
|
700
|
+
try {
|
|
701
|
+
const mgr = await this.#buildMarketplaceManager();
|
|
702
|
+
await mgr.setPluginEnabled(plugin.id, enabled, plugin.scope);
|
|
703
|
+
await this.callbacks.onPluginChanged();
|
|
704
|
+
} catch (err) {
|
|
705
|
+
logger.error("Settings → Plugins: failed to toggle marketplace plugin", {
|
|
706
|
+
pluginId: plugin.id,
|
|
707
|
+
scope: plugin.scope,
|
|
708
|
+
enabled,
|
|
709
|
+
error: err instanceof Error ? err.message : String(err),
|
|
710
|
+
});
|
|
711
|
+
}
|
|
478
712
|
},
|
|
479
713
|
onBack: () => this.#showPluginList(),
|
|
480
714
|
});
|
|
@@ -51,10 +51,10 @@ export type SessionHistoryMatcher = (query: string) => string[];
|
|
|
51
51
|
*
|
|
52
52
|
* - `fuzzy` is the ordered fuzzy-filter result over session metadata (best first).
|
|
53
53
|
* - `historyIds` are session IDs whose recorded prompts matched the query,
|
|
54
|
-
* ordered by history
|
|
54
|
+
* ordered by prompt-history rank (typically newest matching prompt first); duplicates are tolerated.
|
|
55
55
|
*
|
|
56
56
|
* Ranking: sessions matched by **both** signals lead (keeping fuzzy order), then
|
|
57
|
-
* fuzzy-only matches, then history-only matches (by history order). A fuzzy match
|
|
57
|
+
* fuzzy-only matches, then history-only matches (by prompt-history order). A fuzzy match
|
|
58
58
|
* is never dropped, and history matches not present in `allSessions` (e.g. deleted
|
|
59
59
|
* or out-of-scope sessions) are ignored since they cannot be resumed from here.
|
|
60
60
|
*/
|
|
@@ -95,7 +95,9 @@ class SessionList implements Component {
|
|
|
95
95
|
onCancel?: () => void;
|
|
96
96
|
onExit: () => void = () => {};
|
|
97
97
|
onToggleScope?: () => void;
|
|
98
|
-
|
|
98
|
+
// Snapshot of the live terminal-row getter; the visible window is derived
|
|
99
|
+
// from it per render so the picker fits the viewport (and adapts to resize).
|
|
100
|
+
readonly #getTerminalRows: () => number;
|
|
99
101
|
|
|
100
102
|
onDeleteRequest?: (session: SessionInfo) => void;
|
|
101
103
|
|
|
@@ -103,7 +105,13 @@ class SessionList implements Component {
|
|
|
103
105
|
#showCwd: boolean;
|
|
104
106
|
readonly #historyMatcher?: SessionHistoryMatcher;
|
|
105
107
|
|
|
106
|
-
constructor(
|
|
108
|
+
constructor(
|
|
109
|
+
sessions: SessionInfo[],
|
|
110
|
+
showCwd = false,
|
|
111
|
+
historyMatcher?: SessionHistoryMatcher,
|
|
112
|
+
getTerminalRows: () => number = () => 24,
|
|
113
|
+
) {
|
|
114
|
+
this.#getTerminalRows = getTerminalRows;
|
|
107
115
|
this.#allSessions = sessions;
|
|
108
116
|
this.#showCwd = showCwd;
|
|
109
117
|
this.#historyMatcher = historyMatcher;
|
|
@@ -119,6 +127,25 @@ class SessionList implements Component {
|
|
|
119
127
|
};
|
|
120
128
|
}
|
|
121
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Number of sessions to show at once, sized so the whole picker fits the
|
|
132
|
+
* current viewport instead of pushing its header/search off the top.
|
|
133
|
+
*
|
|
134
|
+
* Budget = rows − chrome − reserve, divided by the worst-case per-session
|
|
135
|
+
* height. Chrome (12) is the surrounding spacers/borders/header (7) plus the
|
|
136
|
+
* list's search line, blank, scroll indicator, blank, and hint (5). A titled
|
|
137
|
+
* session is the tallest item at 4 lines (title + preview + metadata +
|
|
138
|
+
* blank); budgeting for that guarantees no overflow even when every visible
|
|
139
|
+
* entry has a title. The reserve covers below-editor hook widgets / cursor.
|
|
140
|
+
*/
|
|
141
|
+
#visibleCount(): number {
|
|
142
|
+
const CHROME = 12;
|
|
143
|
+
const PER_SESSION = 4;
|
|
144
|
+
const RESERVE = 1;
|
|
145
|
+
const budget = this.#getTerminalRows() - CHROME - RESERVE;
|
|
146
|
+
return Math.max(2, Math.floor(budget / PER_SESSION));
|
|
147
|
+
}
|
|
148
|
+
|
|
122
149
|
/** Replace the visible dataset, e.g. when toggling folder/all-projects scope. */
|
|
123
150
|
setSessions(sessions: SessionInfo[], showCwd: boolean): void {
|
|
124
151
|
this.#allSessions = sessions;
|
|
@@ -210,17 +237,16 @@ class SessionList implements Component {
|
|
|
210
237
|
return date.toLocaleDateString();
|
|
211
238
|
};
|
|
212
239
|
|
|
213
|
-
// Calculate visible range with scrolling
|
|
240
|
+
// Calculate visible range with scrolling. The window is sized to the
|
|
241
|
+
// current viewport so the picker never overflows past the top.
|
|
242
|
+
const maxVisible = this.#visibleCount();
|
|
214
243
|
const startIndex = Math.max(
|
|
215
244
|
0,
|
|
216
|
-
Math.min(
|
|
217
|
-
this.#selectedIndex - Math.floor(this.#maxVisible / 2),
|
|
218
|
-
this.#filteredSessions.length - this.#maxVisible,
|
|
219
|
-
),
|
|
245
|
+
Math.min(this.#selectedIndex - Math.floor(maxVisible / 2), this.#filteredSessions.length - maxVisible),
|
|
220
246
|
);
|
|
221
|
-
const endIndex = Math.min(startIndex +
|
|
247
|
+
const endIndex = Math.min(startIndex + maxVisible, this.#filteredSessions.length);
|
|
222
248
|
|
|
223
|
-
// Render visible sessions (
|
|
249
|
+
// Render visible sessions (3 lines, or 4 when a title adds a preview line).
|
|
224
250
|
for (let i = startIndex; i < endIndex; i++) {
|
|
225
251
|
const session = this.#filteredSessions[i];
|
|
226
252
|
const isSelected = i === this.#selectedIndex;
|
|
@@ -311,12 +337,12 @@ class SessionList implements Component {
|
|
|
311
337
|
}
|
|
312
338
|
// Page up - jump up by maxVisible items
|
|
313
339
|
if (matchesKey(keyData, "pageUp")) {
|
|
314
|
-
this.#selectedIndex = Math.max(0, this.#selectedIndex - this.#
|
|
340
|
+
this.#selectedIndex = Math.max(0, this.#selectedIndex - this.#visibleCount());
|
|
315
341
|
return;
|
|
316
342
|
}
|
|
317
343
|
// Page down - jump down by maxVisible items
|
|
318
344
|
if (matchesKey(keyData, "pageDown")) {
|
|
319
|
-
this.#selectedIndex = Math.min(this.#filteredSessions.length - 1, this.#selectedIndex + this.#
|
|
345
|
+
this.#selectedIndex = Math.min(this.#filteredSessions.length - 1, this.#selectedIndex + this.#visibleCount());
|
|
320
346
|
return;
|
|
321
347
|
}
|
|
322
348
|
// Enter
|
|
@@ -359,6 +385,11 @@ export interface SessionSelectorOptions {
|
|
|
359
385
|
allSessions?: SessionInfo[];
|
|
360
386
|
/** Open directly in all-projects scope (e.g. the current folder has no sessions). */
|
|
361
387
|
startInAllScope?: boolean;
|
|
388
|
+
/**
|
|
389
|
+
* Reads the live terminal height so the visible window fits the viewport.
|
|
390
|
+
* Omitted only in tests; defaults to a conservative 24 rows.
|
|
391
|
+
*/
|
|
392
|
+
getTerminalRows?: () => number;
|
|
362
393
|
}
|
|
363
394
|
|
|
364
395
|
/**
|
|
@@ -405,7 +436,7 @@ export class SessionSelectorComponent extends Container {
|
|
|
405
436
|
this.addChild(new Spacer(1));
|
|
406
437
|
this.addChild(this.#messageContainer);
|
|
407
438
|
// Create session list
|
|
408
|
-
this.#sessionList = new SessionList(initialSessions, startAll, options.historyMatcher);
|
|
439
|
+
this.#sessionList = new SessionList(initialSessions, startAll, options.historyMatcher, options.getTerminalRows);
|
|
409
440
|
this.#sessionList.onSelect = onSelect;
|
|
410
441
|
this.#sessionList.onCancel = onCancel;
|
|
411
442
|
this.#sessionList.onExit = onExit;
|
|
@@ -208,7 +208,7 @@ export interface SettingsCallbacks {
|
|
|
208
208
|
/** Get current rendered status line for inline preview */
|
|
209
209
|
getStatusLinePreview?: () => string;
|
|
210
210
|
/** Called when plugins change */
|
|
211
|
-
onPluginsChanged?: () => void
|
|
211
|
+
onPluginsChanged?: () => void | Promise<void>;
|
|
212
212
|
/** Called when settings panel is closed */
|
|
213
213
|
onCancel: () => void;
|
|
214
214
|
}
|
|
@@ -11,4 +11,8 @@ Say `orchestrate` in your message to drive a multi-phase task with parallel suba
|
|
|
11
11
|
Say `workflow` in your message to drive the task with parallel subagents in eval — watch it glow as you type
|
|
12
12
|
Log in to several accounts of the same provider — `/login` again — and omp load-balances across them automatically
|
|
13
13
|
Run `omp auth-broker serve` once and every machine pulls live tokens over the wire — refresh keys never leave the host; `omp auth-gateway` fronts it as a drop-in proxy any OpenAI-compatible client can hit
|
|
14
|
-
Press alt+p (or /switch) to switch provider, and ctrl+p to cycle role models smol -> slow -> etc
|
|
14
|
+
Press alt+p (or /switch) to switch provider, and ctrl+p to cycle role models smol -> slow -> etc
|
|
15
|
+
Press ctrl+r to search your prompt history and reuse a past message
|
|
16
|
+
`/force read` pins the next turn to one specific tool when the model keeps reaching for the wrong one
|
|
17
|
+
`/copy code` grabs the last code block to your clipboard — `/copy cmd` grabs the last shell/python command
|
|
18
|
+
`/shake` rips heavy tool results out of context to reclaim tokens without a full /compact — `/shake images` drops just images
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Component, Container, TERMINAL } from "@oh-my-pi/pi-tui";
|
|
1
|
+
import { type Component, Container, type NativeScrollbackLiveRegion, TERMINAL } from "@oh-my-pi/pi-tui";
|
|
2
2
|
|
|
3
3
|
const kSnapshot = Symbol("transcript.frozenRender");
|
|
4
4
|
|
|
@@ -34,10 +34,19 @@ interface SnapshotCarrier {
|
|
|
34
34
|
* and any drift reconciles safely. On terminals that can rebuild history this
|
|
35
35
|
* freezing is unnecessary, so it renders every block live for full fidelity.
|
|
36
36
|
*/
|
|
37
|
-
export class TranscriptContainer extends Container {
|
|
37
|
+
export class TranscriptContainer extends Container implements NativeScrollbackLiveRegion {
|
|
38
38
|
// Bumped to invalidate every block's snapshot at once; a snapshot is only
|
|
39
39
|
// honored when its stored generation still matches.
|
|
40
40
|
#generation = 0;
|
|
41
|
+
// The block that was bottom-most (live) on the previous render. When the live
|
|
42
|
+
// position moves past it, its snapshot was last refreshed mid-stream and may
|
|
43
|
+
// predate content that finalized in the same coalesced frame that appended the
|
|
44
|
+
// block now below it — so it must recompute once on the live→frozen transition.
|
|
45
|
+
#prevLiveChild: Component | undefined;
|
|
46
|
+
// Local line index where the current bottom-most block begins in the most
|
|
47
|
+
// recent render. TUI extends the native-scrollback pinned region from this
|
|
48
|
+
// point through the live block and the root chrome rendered below it.
|
|
49
|
+
#nativeScrollbackLiveRegionStart: number | undefined;
|
|
41
50
|
|
|
42
51
|
override invalidate(): void {
|
|
43
52
|
// A theme/global invalidation forces a full recompute on the rebuild that
|
|
@@ -51,6 +60,10 @@ export class TranscriptContainer extends Container {
|
|
|
51
60
|
super.clear();
|
|
52
61
|
}
|
|
53
62
|
|
|
63
|
+
getNativeScrollbackLiveRegionStart(): number | undefined {
|
|
64
|
+
return this.#nativeScrollbackLiveRegionStart;
|
|
65
|
+
}
|
|
66
|
+
|
|
54
67
|
/**
|
|
55
68
|
* Retire all frozen snapshots so the next render reflects each block's current
|
|
56
69
|
* state. Call at reconciliation checkpoints (prompt submit) where the whole
|
|
@@ -63,26 +76,42 @@ export class TranscriptContainer extends Container {
|
|
|
63
76
|
|
|
64
77
|
override render(width: number): string[] {
|
|
65
78
|
width = Math.max(1, width);
|
|
79
|
+
this.#nativeScrollbackLiveRegionStart = undefined;
|
|
66
80
|
if (!TERMINAL.eagerEraseScrollbackRisk) return super.render(width);
|
|
67
81
|
|
|
68
82
|
const lines: string[] = [];
|
|
69
83
|
const liveIndex = this.children.length - 1;
|
|
84
|
+
const liveChild = this.children[liveIndex];
|
|
85
|
+
const prevLiveChild = this.#prevLiveChild;
|
|
86
|
+
this.#prevLiveChild = liveChild;
|
|
70
87
|
for (let i = 0; i < this.children.length; i++) {
|
|
71
88
|
const child = this.children[i]! as Component & SnapshotCarrier;
|
|
72
|
-
if (
|
|
89
|
+
if (child === liveChild) {
|
|
90
|
+
this.#nativeScrollbackLiveRegionStart = lines.length;
|
|
91
|
+
} else {
|
|
73
92
|
const snapshot = child[kSnapshot];
|
|
74
93
|
// Replay the block's last render from while it was live. A stale
|
|
75
94
|
// generation (post-thaw) or width mismatch (resize in flight, an
|
|
76
95
|
// explicit rebuild that reconciles history anyway) recomputes instead.
|
|
77
|
-
|
|
96
|
+
// The block that was live on the previous render is also recomputed
|
|
97
|
+
// here: TUI render coalescing can advance its content (final streamed
|
|
98
|
+
// tokens) in the very frame that appends the block now below it, so its
|
|
99
|
+
// cached snapshot predates that final content. Recomputing on the
|
|
100
|
+
// transition seals the block at its true final state, not a mid-stream one.
|
|
101
|
+
if (
|
|
102
|
+
child !== prevLiveChild &&
|
|
103
|
+
snapshot &&
|
|
104
|
+
snapshot.generation === this.#generation &&
|
|
105
|
+
snapshot.width === width
|
|
106
|
+
) {
|
|
78
107
|
lines.push(...snapshot.lines);
|
|
79
108
|
continue;
|
|
80
109
|
}
|
|
81
110
|
}
|
|
82
111
|
const rendered = child.render(width);
|
|
83
112
|
// Cache every block's latest render. While a block is live this keeps its
|
|
84
|
-
// snapshot current; the frame it stops being live the
|
|
85
|
-
//
|
|
113
|
+
// snapshot current; on the frame it stops being live the recompute above
|
|
114
|
+
// refreshes it to the final state before it freezes.
|
|
86
115
|
child[kSnapshot] = { width, lines: rendered, generation: this.#generation };
|
|
87
116
|
lines.push(...rendered);
|
|
88
117
|
}
|