@oh-my-pi/pi-coding-agent 15.8.3 → 15.9.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.
Files changed (172) hide show
  1. package/CHANGELOG.md +113 -31
  2. package/dist/types/capability/skill.d.ts +7 -0
  3. package/dist/types/cli/update-cli.d.ts +15 -1
  4. package/dist/types/config/append-only-context-mode.d.ts +8 -0
  5. package/dist/types/config/model-registry.d.ts +3 -0
  6. package/dist/types/config/models-config-schema.d.ts +15 -0
  7. package/dist/types/config/settings-schema.d.ts +38 -24
  8. package/dist/types/debug/protocol-probe.d.ts +38 -0
  9. package/dist/types/debug/terminal-info.d.ts +34 -0
  10. package/dist/types/exa/mcp-client.d.ts +2 -1
  11. package/dist/types/export/html/template.generated.d.ts +1 -1
  12. package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
  13. package/dist/types/extensibility/extensions/types.d.ts +11 -0
  14. package/dist/types/extensibility/shared-events.d.ts +1 -1
  15. package/dist/types/index.d.ts +1 -0
  16. package/dist/types/mcp/json-rpc.d.ts +6 -1
  17. package/dist/types/mcp/tool-bridge.d.ts +4 -0
  18. package/dist/types/mcp/transports/stdio.d.ts +16 -7
  19. package/dist/types/mnemopi/state.d.ts +2 -2
  20. package/dist/types/modes/components/agent-dashboard.d.ts +1 -0
  21. package/dist/types/modes/components/assistant-message.d.ts +3 -2
  22. package/dist/types/modes/components/extensions/extension-dashboard.d.ts +1 -0
  23. package/dist/types/modes/components/hook-selector.d.ts +11 -0
  24. package/dist/types/modes/components/plugin-settings.d.ts +40 -8
  25. package/dist/types/modes/components/session-selector.d.ts +8 -3
  26. package/dist/types/modes/components/settings-selector.d.ts +1 -1
  27. package/dist/types/modes/components/todo-reminder.d.ts +1 -1
  28. package/dist/types/modes/components/transcript-container.d.ts +36 -0
  29. package/dist/types/modes/interactive-mode.d.ts +2 -1
  30. package/dist/types/modes/rpc/rpc-types.d.ts +1 -1
  31. package/dist/types/modes/theme/theme.d.ts +20 -1
  32. package/dist/types/modes/utils/keybinding-matchers.d.ts +4 -0
  33. package/dist/types/plan-mode/plan-handoff.d.ts +20 -0
  34. package/dist/types/session/agent-session.d.ts +5 -2
  35. package/dist/types/session/history-storage.d.ts +3 -4
  36. package/dist/types/session/indexed-session-storage.d.ts +59 -0
  37. package/dist/types/session/messages.d.ts +1 -0
  38. package/dist/types/session/redis-session-storage.d.ts +12 -85
  39. package/dist/types/session/session-manager.d.ts +21 -0
  40. package/dist/types/session/session-storage.d.ts +5 -7
  41. package/dist/types/session/sql-session-storage.d.ts +16 -85
  42. package/dist/types/slash-commands/types.d.ts +17 -4
  43. package/dist/types/task/executor.d.ts +9 -0
  44. package/dist/types/task/index.d.ts +3 -1
  45. package/dist/types/telemetry-export.d.ts +19 -0
  46. package/dist/types/tiny/compiled-runtime.d.ts +35 -0
  47. package/dist/types/tiny/text.d.ts +17 -0
  48. package/dist/types/tools/ask.d.ts +1 -0
  49. package/dist/types/tools/index.d.ts +4 -2
  50. package/dist/types/tools/path-utils.d.ts +1 -1
  51. package/dist/types/tools/search.d.ts +2 -2
  52. package/dist/types/tools/{todo-write.d.ts → todo.d.ts} +18 -18
  53. package/dist/types/utils/session-color.d.ts +7 -2
  54. package/dist/types/web/search/index.d.ts +1 -1
  55. package/dist/types/web/search/provider.d.ts +2 -2
  56. package/dist/types/web/search/providers/base.d.ts +14 -0
  57. package/dist/types/web/search/providers/exa.d.ts +9 -0
  58. package/dist/types/web/search/providers/perplexity.d.ts +2 -2
  59. package/dist/types/web/search/types.d.ts +66 -1
  60. package/package.json +15 -9
  61. package/scripts/build-binary.ts +12 -0
  62. package/src/capability/skill.ts +7 -0
  63. package/src/cli/args.ts +1 -1
  64. package/src/cli/session-picker.ts +1 -0
  65. package/src/cli/update-cli.ts +54 -2
  66. package/src/commands/completions.ts +1 -1
  67. package/src/config/append-only-context-mode.ts +37 -0
  68. package/src/config/models-config-schema.ts +1 -0
  69. package/src/config/settings-schema.ts +24 -57
  70. package/src/debug/index.ts +67 -1
  71. package/src/debug/protocol-probe.ts +267 -0
  72. package/src/debug/terminal-info.ts +127 -0
  73. package/src/exa/mcp-client.ts +11 -5
  74. package/src/export/html/template.generated.ts +1 -1
  75. package/src/export/html/template.js +3 -3
  76. package/src/extensibility/custom-tools/types.ts +1 -1
  77. package/src/extensibility/extensions/types.ts +11 -0
  78. package/src/extensibility/shared-events.ts +1 -1
  79. package/src/extensibility/skills.ts +3 -3
  80. package/src/index.ts +1 -0
  81. package/src/internal-urls/docs-index.generated.ts +7 -7
  82. package/src/main.ts +16 -2
  83. package/src/mcp/json-rpc.ts +8 -0
  84. package/src/mcp/render.ts +3 -0
  85. package/src/mcp/tool-bridge.ts +10 -2
  86. package/src/mcp/transports/http.ts +33 -16
  87. package/src/mcp/transports/stdio.ts +37 -12
  88. package/src/mnemopi/state.ts +4 -4
  89. package/src/modes/acp/acp-agent.ts +168 -3
  90. package/src/modes/acp/acp-event-mapper.ts +7 -7
  91. package/src/modes/components/agent-dashboard.ts +103 -31
  92. package/src/modes/components/assistant-message.ts +3 -2
  93. package/src/modes/components/extensions/extension-dashboard.ts +56 -10
  94. package/src/modes/components/history-search.ts +128 -14
  95. package/src/modes/components/hook-selector.ts +149 -14
  96. package/src/modes/components/plugin-settings.ts +270 -36
  97. package/src/modes/components/session-selector.ts +81 -19
  98. package/src/modes/components/settings-selector.ts +1 -1
  99. package/src/modes/components/status-line/segments.ts +2 -1
  100. package/src/modes/components/status-line.ts +1 -1
  101. package/src/modes/components/tips.txt +6 -2
  102. package/src/modes/components/todo-reminder.ts +1 -1
  103. package/src/modes/components/tool-execution.ts +9 -4
  104. package/src/modes/components/transcript-container.ts +109 -0
  105. package/src/modes/controllers/command-controller.ts +4 -3
  106. package/src/modes/controllers/event-controller.ts +12 -7
  107. package/src/modes/controllers/extension-ui-controller.ts +3 -0
  108. package/src/modes/controllers/input-controller.ts +10 -5
  109. package/src/modes/controllers/selector-controller.ts +30 -19
  110. package/src/modes/controllers/todo-command-controller.ts +1 -1
  111. package/src/modes/interactive-mode.ts +56 -9
  112. package/src/modes/print-mode.ts +5 -0
  113. package/src/modes/rpc/rpc-types.ts +1 -1
  114. package/src/modes/theme/theme.ts +48 -8
  115. package/src/modes/utils/keybinding-matchers.ts +10 -0
  116. package/src/modes/utils/ui-helpers.ts +1 -0
  117. package/src/plan-mode/plan-handoff.ts +37 -0
  118. package/src/priority.json +4 -0
  119. package/src/prompts/goals/goal-continuation.md +1 -1
  120. package/src/prompts/steering/user-interjection.md +10 -0
  121. package/src/prompts/system/agent-creation-architect.md +1 -26
  122. package/src/prompts/system/eager-todo.md +3 -3
  123. package/src/prompts/system/orchestrate-notice.md +5 -5
  124. package/src/prompts/system/plan-mode-approved.md +14 -17
  125. package/src/prompts/system/plan-mode-reference.md +3 -6
  126. package/src/prompts/system/subagent-system-prompt.md +11 -0
  127. package/src/prompts/system/system-prompt.md +143 -145
  128. package/src/prompts/system/title-system.md +3 -2
  129. package/src/prompts/system/workflow-notice.md +1 -1
  130. package/src/prompts/tools/browser.md +33 -30
  131. package/src/prompts/tools/render-mermaid.md +2 -2
  132. package/src/prompts/tools/search.md +1 -1
  133. package/src/prompts/tools/task.md +3 -1
  134. package/src/prompts/tools/{todo-write.md → todo.md} +5 -5
  135. package/src/sdk.ts +6 -21
  136. package/src/session/agent-session.ts +44 -21
  137. package/src/session/history-storage.ts +11 -18
  138. package/src/session/indexed-session-storage.ts +430 -0
  139. package/src/session/messages.ts +80 -0
  140. package/src/session/redis-session-storage.ts +66 -377
  141. package/src/session/session-manager.ts +109 -23
  142. package/src/session/session-storage.ts +148 -68
  143. package/src/session/sql-session-storage.ts +131 -382
  144. package/src/slash-commands/helpers/todo.ts +2 -2
  145. package/src/slash-commands/types.ts +27 -10
  146. package/src/task/executor.ts +9 -1
  147. package/src/task/index.ts +51 -1
  148. package/src/telemetry-export.ts +126 -0
  149. package/src/tiny/compiled-runtime.ts +179 -0
  150. package/src/tiny/text.ts +112 -1
  151. package/src/tiny/worker.ts +24 -2
  152. package/src/tools/ask.ts +133 -87
  153. package/src/tools/fetch.ts +17 -4
  154. package/src/tools/find.ts +2 -2
  155. package/src/tools/index.ts +6 -4
  156. package/src/tools/memory-recall.ts +1 -1
  157. package/src/tools/memory-reflect.ts +1 -1
  158. package/src/tools/path-utils.ts +16 -7
  159. package/src/tools/renderers.ts +2 -2
  160. package/src/tools/search.ts +6 -3
  161. package/src/tools/ssh.ts +26 -10
  162. package/src/tools/{todo-write.ts → todo.ts} +32 -35
  163. package/src/tools/write.ts +14 -2
  164. package/src/tui/status-line.ts +15 -4
  165. package/src/utils/session-color.ts +39 -14
  166. package/src/utils/title-generator.ts +9 -2
  167. package/src/web/search/index.ts +4 -2
  168. package/src/web/search/provider.ts +19 -35
  169. package/src/web/search/providers/base.ts +17 -0
  170. package/src/web/search/providers/exa.ts +111 -7
  171. package/src/web/search/providers/perplexity.ts +8 -4
  172. package/src/web/search/types.ts +74 -32
@@ -2,8 +2,9 @@
2
2
  * Plugin settings UI components.
3
3
  *
4
4
  * Provides a hierarchical settings interface:
5
- * - Plugin list (shows all installed plugins)
6
- * - Plugin detail (enable/disable, features, config)
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
- onPluginSelect: (plugin: InstalledPlugin) => void;
66
+ onNpmSelect: (plugin: InstalledPlugin) => void;
67
+ onMarketplaceSelect: (plugin: InstalledPluginSummary) => void;
46
68
  onCancel: () => void;
47
69
  }
48
70
 
49
71
  /**
50
- * Shows list of installed plugins with enable/disable status.
51
- * Selecting a plugin opens its detail view.
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 plugins: InstalledPlugin[],
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 (plugins.length === 0) {
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 with: omp plugin install <package>"), 0, 0));
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
- // Create empty list that just handles escape
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[] = plugins.map(p => {
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 = `v${p.version}`;
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: p.name,
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
- this.#selectList.onSelect = item => {
102
- const plugin = this.plugins.find(p => p.name === item.value);
103
- if (plugin) {
104
- callbacks.onPluginSelect(plugin);
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
- this.#selectList.onCancel = callbacks.onCancel;
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
- this.addChild(this.#selectList);
111
- this.addChild(new Spacer(1));
112
- this.addChild(new Text(theme.fg("dim", " Enter to configure · Esc to go back"), 0, 0));
113
- this.addChild(new DynamicBorder());
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
- const plugins = await this.#manager.list();
446
-
447
- this.#viewComponent = new PluginListComponent(plugins, {
448
- onPluginSelect: plugin => this.#showPluginDetail(plugin),
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
  });
@@ -14,11 +14,34 @@ import {
14
14
  import { formatBytes } from "@oh-my-pi/pi-utils";
15
15
  import { theme } from "../../modes/theme/theme";
16
16
  import { matchesAppInterrupt, matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
17
- import type { SessionInfo } from "../../session/session-manager";
17
+ import type { SessionInfo, SessionStatus } from "../../session/session-manager";
18
18
  import { shortenPath } from "../../tools/render-utils";
19
19
  import { DynamicBorder } from "./dynamic-border";
20
20
  import { HookSelectorComponent } from "./hook-selector";
21
21
 
22
+ /**
23
+ * Themed glyph + colored label for a session's lifecycle status, or `undefined`
24
+ * when there is nothing useful to show (`unknown`/unset) so the metadata line
25
+ * stays uncluttered. The glyph resolves through the active symbol preset
26
+ * (nerdfont / unicode / ascii) via `theme.status.*`.
27
+ */
28
+ function formatSessionStatus(status: SessionStatus | undefined): string | undefined {
29
+ switch (status) {
30
+ case "complete":
31
+ return theme.fg("success", `${theme.status.success} done`);
32
+ case "interrupted":
33
+ return theme.fg("warning", `${theme.status.warning} interrupted`);
34
+ case "aborted":
35
+ return theme.fg("muted", `${theme.status.aborted} aborted`);
36
+ case "error":
37
+ return theme.fg("error", `${theme.status.error} error`);
38
+ case "pending":
39
+ return theme.fg("accent", `${theme.status.pending} pending`);
40
+ default:
41
+ return undefined;
42
+ }
43
+ }
44
+
22
45
  /** Returns the IDs of sessions whose recorded prompts match a query, best first. */
23
46
  export type SessionHistoryMatcher = (query: string) => string[];
24
47
 
@@ -28,10 +51,10 @@ export type SessionHistoryMatcher = (query: string) => string[];
28
51
  *
29
52
  * - `fuzzy` is the ordered fuzzy-filter result over session metadata (best first).
30
53
  * - `historyIds` are session IDs whose recorded prompts matched the query,
31
- * ordered by history relevance (best first); duplicates are tolerated.
54
+ * ordered by prompt-history rank (typically newest matching prompt first); duplicates are tolerated.
32
55
  *
33
56
  * Ranking: sessions matched by **both** signals lead (keeping fuzzy order), then
34
- * 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
35
58
  * is never dropped, and history matches not present in `allSessions` (e.g. deleted
36
59
  * or out-of-scope sessions) are ignored since they cannot be resumed from here.
37
60
  */
@@ -72,7 +95,9 @@ class SessionList implements Component {
72
95
  onCancel?: () => void;
73
96
  onExit: () => void = () => {};
74
97
  onToggleScope?: () => void;
75
- #maxVisible: number = 5; // Max sessions visible (each session is 3 lines: msg + metadata + blank)
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;
76
101
 
77
102
  onDeleteRequest?: (session: SessionInfo) => void;
78
103
 
@@ -80,7 +105,13 @@ class SessionList implements Component {
80
105
  #showCwd: boolean;
81
106
  readonly #historyMatcher?: SessionHistoryMatcher;
82
107
 
83
- constructor(sessions: SessionInfo[], showCwd = false, historyMatcher?: SessionHistoryMatcher) {
108
+ constructor(
109
+ sessions: SessionInfo[],
110
+ showCwd = false,
111
+ historyMatcher?: SessionHistoryMatcher,
112
+ getTerminalRows: () => number = () => 24,
113
+ ) {
114
+ this.#getTerminalRows = getTerminalRows;
84
115
  this.#allSessions = sessions;
85
116
  this.#showCwd = showCwd;
86
117
  this.#historyMatcher = historyMatcher;
@@ -96,6 +127,25 @@ class SessionList implements Component {
96
127
  };
97
128
  }
98
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
+
99
149
  /** Replace the visible dataset, e.g. when toggling folder/all-projects scope. */
100
150
  setSessions(sessions: SessionInfo[], showCwd: boolean): void {
101
151
  this.#allSessions = sessions;
@@ -187,17 +237,16 @@ class SessionList implements Component {
187
237
  return date.toLocaleDateString();
188
238
  };
189
239
 
190
- // 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();
191
243
  const startIndex = Math.max(
192
244
  0,
193
- Math.min(
194
- this.#selectedIndex - Math.floor(this.#maxVisible / 2),
195
- this.#filteredSessions.length - this.#maxVisible,
196
- ),
245
+ Math.min(this.#selectedIndex - Math.floor(maxVisible / 2), this.#filteredSessions.length - maxVisible),
197
246
  );
198
- const endIndex = Math.min(startIndex + this.#maxVisible, this.#filteredSessions.length);
247
+ const endIndex = Math.min(startIndex + maxVisible, this.#filteredSessions.length);
199
248
 
200
- // Render visible sessions (2-3 lines per session + blank line)
249
+ // Render visible sessions (3 lines, or 4 when a title adds a preview line).
201
250
  for (let i = startIndex; i < endIndex; i++) {
202
251
  const session = this.#filteredSessions[i];
203
252
  const isSelected = i === this.#selectedIndex;
@@ -227,13 +276,21 @@ class SessionList implements Component {
227
276
  lines.push(messageLine);
228
277
  }
229
278
 
230
- // Metadata line: date + file size (+ project dir in all-projects scope)
279
+ // Metadata line: date + file size + lifecycle status (+ project dir in
280
+ // all-projects scope). The status segment carries its own color, so each
281
+ // segment is dimmed individually rather than wrapping the whole line.
282
+ const dim = (s: string) => theme.fg("dim", s);
283
+ const dot = dim(theme.sep.dot);
231
284
  const modified = formatDate(session.modified);
232
- let metadata = ` ${modified} ${theme.sep.dot} ${formatBytes(session.size)}`;
285
+ let metadata = ` ${dim(modified)} ${dot} ${dim(formatBytes(session.size))}`;
286
+ const status = formatSessionStatus(session.status);
287
+ if (status) {
288
+ metadata += ` ${dot} ${status}`;
289
+ }
233
290
  if (this.#showCwd && session.cwd) {
234
- metadata += ` ${theme.sep.dot} ${shortenPath(session.cwd)}`;
291
+ metadata += ` ${dot} ${dim(shortenPath(session.cwd))}`;
235
292
  }
236
- const metadataLine = theme.fg("dim", truncateToWidth(metadata, width));
293
+ const metadataLine = truncateToWidth(metadata, width);
237
294
 
238
295
  lines.push(metadataLine);
239
296
  lines.push(""); // Blank line between sessions
@@ -280,12 +337,12 @@ class SessionList implements Component {
280
337
  }
281
338
  // Page up - jump up by maxVisible items
282
339
  if (matchesKey(keyData, "pageUp")) {
283
- this.#selectedIndex = Math.max(0, this.#selectedIndex - this.#maxVisible);
340
+ this.#selectedIndex = Math.max(0, this.#selectedIndex - this.#visibleCount());
284
341
  return;
285
342
  }
286
343
  // Page down - jump down by maxVisible items
287
344
  if (matchesKey(keyData, "pageDown")) {
288
- this.#selectedIndex = Math.min(this.#filteredSessions.length - 1, this.#selectedIndex + this.#maxVisible);
345
+ this.#selectedIndex = Math.min(this.#filteredSessions.length - 1, this.#selectedIndex + this.#visibleCount());
289
346
  return;
290
347
  }
291
348
  // Enter
@@ -328,6 +385,11 @@ export interface SessionSelectorOptions {
328
385
  allSessions?: SessionInfo[];
329
386
  /** Open directly in all-projects scope (e.g. the current folder has no sessions). */
330
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;
331
393
  }
332
394
 
333
395
  /**
@@ -374,7 +436,7 @@ export class SessionSelectorComponent extends Container {
374
436
  this.addChild(new Spacer(1));
375
437
  this.addChild(this.#messageContainer);
376
438
  // Create session list
377
- this.#sessionList = new SessionList(initialSessions, startAll, options.historyMatcher);
439
+ this.#sessionList = new SessionList(initialSessions, startAll, options.historyMatcher, options.getTerminalRows);
378
440
  this.#sessionList.onSelect = onSelect;
379
441
  this.#sessionList.onCancel = onCancel;
380
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
  }
@@ -485,7 +485,8 @@ const sessionNameSegment: StatusLineSegment = {
485
485
  const name = sessionManager?.getSessionName();
486
486
  if (!name) return { content: "", visible: false };
487
487
 
488
- const ansi = getSessionAccentAnsi(getSessionAccentHex(name)) ?? theme.getFgAnsi("accent");
488
+ const ansi =
489
+ getSessionAccentAnsi(getSessionAccentHex(name, theme.accentSurfaceLuminance)) ?? theme.getFgAnsi("accent");
489
490
  return { content: `${ansi}${sanitizeStatusText(name)}\x1b[39m`, visible: true };
490
491
  },
491
492
  };
@@ -764,7 +764,7 @@ export class StatusLineComponent implements Component {
764
764
  const gapWidth = Math.max(1, topFillWidth - leftWidth - rightWidth);
765
765
  const sessionName =
766
766
  effectiveSettings.sessionAccent !== false ? this.session.sessionManager?.getSessionName() : undefined;
767
- const accentHex = sessionName ? getSessionAccentHex(sessionName) : undefined;
767
+ const accentHex = sessionName ? getSessionAccentHex(sessionName, theme.accentSurfaceLuminance) : undefined;
768
768
  const gapColor = getSessionAccentAnsi(accentHex) ?? theme.getFgAnsi("border");
769
769
  const gapFill = `${gapColor}${theme.boxRound.horizontal.repeat(gapWidth)}\x1b[39m`;
770
770
  return leftGroup + gapFill + rightGroup;
@@ -5,10 +5,14 @@ Find out which model you emotionally abuse the most with `omp stats`
5
5
  Try task isolation to create CoW worktrees
6
6
  Your LLM can call an LLM using `llm(x...)`. Have a big batch of tasks? Ask clanker to use it!
7
7
  Spaghetti code? Try complaining with /omfg
8
- Did you know? Each kitty/tmux split keeps its own session — `omp -c` resumes the right one
8
+ Did you know? Each kitty/tmux/cmux split keeps its own session — `omp -c` resumes the right one
9
9
  Drop the word `ultrathink` in your message for harder multi-step reasoning — watch it glow rainbow as you type
10
10
  Say `orchestrate` in your message to drive a multi-phase task with parallel subagents — watch it glow as you type
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,6 +1,6 @@
1
1
  import { Box, Container, Spacer, Text } from "@oh-my-pi/pi-tui";
2
2
  import { theme } from "../../modes/theme/theme";
3
- import type { TodoItem } from "../../tools/todo-write";
3
+ import type { TodoItem } from "../../tools/todo";
4
4
 
5
5
  /**
6
6
  * Component that renders a todo completion reminder notification.