@oh-my-pi/pi-coding-agent 3.15.0 → 3.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -1
- package/docs/extensions.md +1055 -0
- package/docs/rpc.md +69 -13
- package/docs/session-tree-plan.md +1 -1
- package/examples/extensions/README.md +141 -0
- package/examples/extensions/api-demo.ts +87 -0
- package/examples/extensions/chalk-logger.ts +26 -0
- package/examples/extensions/hello.ts +33 -0
- package/examples/extensions/pirate.ts +44 -0
- package/examples/extensions/plan-mode.ts +551 -0
- package/examples/extensions/subagent/agents/reviewer.md +35 -0
- package/examples/extensions/todo.ts +299 -0
- package/examples/extensions/tools.ts +145 -0
- package/examples/extensions/with-deps/index.ts +36 -0
- package/examples/extensions/with-deps/package-lock.json +31 -0
- package/examples/extensions/with-deps/package.json +16 -0
- package/examples/sdk/02-custom-model.ts +3 -3
- package/examples/sdk/05-tools.ts +7 -3
- package/examples/sdk/06-extensions.ts +81 -0
- package/examples/sdk/06-hooks.ts +14 -13
- package/examples/sdk/08-prompt-templates.ts +42 -0
- package/examples/sdk/08-slash-commands.ts +17 -12
- package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
- package/examples/sdk/12-full-control.ts +6 -6
- package/package.json +11 -7
- package/src/capability/extension-module.ts +34 -0
- package/src/cli/args.ts +22 -7
- package/src/cli/file-processor.ts +38 -67
- package/src/cli/list-models.ts +1 -1
- package/src/config.ts +25 -14
- package/src/core/agent-session.ts +505 -242
- package/src/core/auth-storage.ts +33 -21
- package/src/core/compaction/branch-summarization.ts +4 -4
- package/src/core/compaction/compaction.ts +3 -3
- package/src/core/custom-commands/bundled/wt/index.ts +430 -0
- package/src/core/custom-commands/loader.ts +9 -0
- package/src/core/custom-tools/wrapper.ts +5 -0
- package/src/core/event-bus.ts +59 -0
- package/src/core/export-html/vendor/highlight.min.js +1213 -0
- package/src/core/export-html/vendor/marked.min.js +6 -0
- package/src/core/extensions/index.ts +100 -0
- package/src/core/extensions/loader.ts +501 -0
- package/src/core/extensions/runner.ts +477 -0
- package/src/core/extensions/types.ts +712 -0
- package/src/core/extensions/wrapper.ts +147 -0
- package/src/core/hooks/types.ts +2 -2
- package/src/core/index.ts +10 -21
- package/src/core/keybindings.ts +199 -0
- package/src/core/messages.ts +26 -7
- package/src/core/model-registry.ts +123 -46
- package/src/core/model-resolver.ts +7 -5
- package/src/core/prompt-templates.ts +242 -0
- package/src/core/sdk.ts +378 -295
- package/src/core/session-manager.ts +72 -58
- package/src/core/settings-manager.ts +118 -22
- package/src/core/system-prompt.ts +24 -1
- package/src/core/terminal-notify.ts +37 -0
- package/src/core/tools/context.ts +4 -4
- package/src/core/tools/exa/mcp-client.ts +5 -4
- package/src/core/tools/exa/render.ts +176 -131
- package/src/core/tools/gemini-image.ts +361 -0
- package/src/core/tools/git.ts +216 -0
- package/src/core/tools/index.ts +28 -15
- package/src/core/tools/lsp/config.ts +5 -4
- package/src/core/tools/lsp/index.ts +17 -12
- package/src/core/tools/lsp/render.ts +39 -47
- package/src/core/tools/read.ts +66 -29
- package/src/core/tools/render-utils.ts +268 -0
- package/src/core/tools/renderers.ts +243 -225
- package/src/core/tools/task/discovery.ts +2 -2
- package/src/core/tools/task/executor.ts +66 -58
- package/src/core/tools/task/index.ts +29 -10
- package/src/core/tools/task/model-resolver.ts +8 -13
- package/src/core/tools/task/omp-command.ts +24 -0
- package/src/core/tools/task/render.ts +35 -60
- package/src/core/tools/task/types.ts +3 -0
- package/src/core/tools/web-fetch.ts +29 -28
- package/src/core/tools/web-search/index.ts +6 -5
- package/src/core/tools/web-search/providers/exa.ts +6 -5
- package/src/core/tools/web-search/render.ts +66 -111
- package/src/core/voice-controller.ts +135 -0
- package/src/core/voice-supervisor.ts +1003 -0
- package/src/core/voice.ts +308 -0
- package/src/discovery/builtin.ts +75 -1
- package/src/discovery/claude.ts +47 -1
- package/src/discovery/codex.ts +54 -2
- package/src/discovery/gemini.ts +55 -2
- package/src/discovery/helpers.ts +100 -1
- package/src/discovery/index.ts +2 -0
- package/src/index.ts +14 -9
- package/src/lib/worktree/collapse.ts +179 -0
- package/src/lib/worktree/constants.ts +14 -0
- package/src/lib/worktree/errors.ts +23 -0
- package/src/lib/worktree/git.ts +110 -0
- package/src/lib/worktree/index.ts +23 -0
- package/src/lib/worktree/operations.ts +216 -0
- package/src/lib/worktree/session.ts +114 -0
- package/src/lib/worktree/stats.ts +67 -0
- package/src/main.ts +61 -37
- package/src/migrations.ts +37 -7
- package/src/modes/interactive/components/bash-execution.ts +6 -4
- package/src/modes/interactive/components/custom-editor.ts +55 -0
- package/src/modes/interactive/components/custom-message.ts +95 -0
- package/src/modes/interactive/components/extensions/extension-list.ts +5 -0
- package/src/modes/interactive/components/extensions/inspector-panel.ts +18 -12
- package/src/modes/interactive/components/extensions/state-manager.ts +12 -0
- package/src/modes/interactive/components/extensions/types.ts +1 -0
- package/src/modes/interactive/components/footer.ts +324 -0
- package/src/modes/interactive/components/hook-editor.ts +1 -0
- package/src/modes/interactive/components/hook-selector.ts +3 -3
- package/src/modes/interactive/components/model-selector.ts +7 -6
- package/src/modes/interactive/components/oauth-selector.ts +3 -3
- package/src/modes/interactive/components/settings-defs.ts +55 -6
- package/src/modes/interactive/components/status-line/separators.ts +4 -4
- package/src/modes/interactive/components/status-line.ts +45 -35
- package/src/modes/interactive/components/tool-execution.ts +95 -23
- package/src/modes/interactive/interactive-mode.ts +644 -113
- package/src/modes/interactive/theme/defaults/alabaster.json +99 -0
- package/src/modes/interactive/theme/defaults/amethyst.json +103 -0
- package/src/modes/interactive/theme/defaults/anthracite.json +100 -0
- package/src/modes/interactive/theme/defaults/basalt.json +90 -0
- package/src/modes/interactive/theme/defaults/birch.json +101 -0
- package/src/modes/interactive/theme/defaults/dark-abyss.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-aurora.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-cavern.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-copper.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-cosmos.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-eclipse.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-ember.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-equinox.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-lavender.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-lunar.json +95 -0
- package/src/modes/interactive/theme/defaults/dark-midnight.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-nebula.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-rainforest.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-reef.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-sakura.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-slate.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-solstice.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-starfall.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-swamp.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-taiga.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-terminal.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-tundra.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-twilight.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-volcanic.json +97 -0
- package/src/modes/interactive/theme/defaults/graphite.json +99 -0
- package/src/modes/interactive/theme/defaults/index.ts +128 -0
- package/src/modes/interactive/theme/defaults/light-aurora-day.json +97 -0
- package/src/modes/interactive/theme/defaults/light-canyon.json +97 -0
- package/src/modes/interactive/theme/defaults/light-cirrus.json +96 -0
- package/src/modes/interactive/theme/defaults/light-coral.json +94 -0
- package/src/modes/interactive/theme/defaults/light-dawn.json +96 -0
- package/src/modes/interactive/theme/defaults/light-dunes.json +97 -0
- package/src/modes/interactive/theme/defaults/light-eucalyptus.json +94 -0
- package/src/modes/interactive/theme/defaults/light-frost.json +94 -0
- package/src/modes/interactive/theme/defaults/light-glacier.json +97 -0
- package/src/modes/interactive/theme/defaults/light-haze.json +96 -0
- package/src/modes/interactive/theme/defaults/light-honeycomb.json +94 -0
- package/src/modes/interactive/theme/defaults/light-lagoon.json +97 -0
- package/src/modes/interactive/theme/defaults/light-lavender.json +94 -0
- package/src/modes/interactive/theme/defaults/light-meadow.json +97 -0
- package/src/modes/interactive/theme/defaults/light-mint.json +94 -0
- package/src/modes/interactive/theme/defaults/light-opal.json +97 -0
- package/src/modes/interactive/theme/defaults/light-orchard.json +97 -0
- package/src/modes/interactive/theme/defaults/light-paper.json +94 -0
- package/src/modes/interactive/theme/defaults/light-prism.json +96 -0
- package/src/modes/interactive/theme/defaults/light-sand.json +94 -0
- package/src/modes/interactive/theme/defaults/light-savanna.json +97 -0
- package/src/modes/interactive/theme/defaults/light-soleil.json +96 -0
- package/src/modes/interactive/theme/defaults/light-wetland.json +97 -0
- package/src/modes/interactive/theme/defaults/light-zenith.json +95 -0
- package/src/modes/interactive/theme/defaults/limestone.json +100 -0
- package/src/modes/interactive/theme/defaults/mahogany.json +104 -0
- package/src/modes/interactive/theme/defaults/marble.json +99 -0
- package/src/modes/interactive/theme/defaults/obsidian.json +90 -0
- package/src/modes/interactive/theme/defaults/onyx.json +90 -0
- package/src/modes/interactive/theme/defaults/pearl.json +99 -0
- package/src/modes/interactive/theme/defaults/porcelain.json +90 -0
- package/src/modes/interactive/theme/defaults/quartz.json +102 -0
- package/src/modes/interactive/theme/defaults/sandstone.json +101 -0
- package/src/modes/interactive/theme/defaults/titanium.json +89 -0
- package/src/modes/print-mode.ts +14 -72
- package/src/modes/rpc/rpc-client.ts +23 -9
- package/src/modes/rpc/rpc-mode.ts +137 -125
- package/src/modes/rpc/rpc-types.ts +46 -24
- package/src/prompts/task.md +1 -0
- package/src/prompts/tools/gemini-image.md +4 -0
- package/src/prompts/tools/git.md +9 -0
- package/src/prompts/voice-summary.md +12 -0
- package/src/utils/image-convert.ts +26 -0
- package/src/utils/image-resize.ts +215 -0
- package/src/utils/shell-snapshot.ts +22 -20
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Displays a list of string options with keyboard navigation.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { Container, isArrowDown, isArrowUp, isEnter, isEscape, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
6
|
+
import { Container, isArrowDown, isArrowUp, isCtrlC, isEnter, isEscape, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
7
7
|
import { theme } from "../theme/theme";
|
|
8
8
|
import { DynamicBorder } from "./dynamic-border";
|
|
9
9
|
|
|
@@ -83,8 +83,8 @@ export class HookSelectorComponent extends Container {
|
|
|
83
83
|
this.onSelectCallback(selected);
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
-
// Escape
|
|
87
|
-
else if (isEscape(keyData)) {
|
|
86
|
+
// Escape or Ctrl+C
|
|
87
|
+
else if (isEscape(keyData) || isCtrlC(keyData)) {
|
|
88
88
|
this.onCancelCallback();
|
|
89
89
|
}
|
|
90
90
|
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
isArrowLeft,
|
|
7
7
|
isArrowRight,
|
|
8
8
|
isArrowUp,
|
|
9
|
+
isCtrlC,
|
|
9
10
|
isEnter,
|
|
10
11
|
isEscape,
|
|
11
12
|
isShiftTab,
|
|
@@ -202,7 +203,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
202
203
|
}));
|
|
203
204
|
} else {
|
|
204
205
|
// Refresh to pick up any changes to models.json
|
|
205
|
-
this.modelRegistry.refresh();
|
|
206
|
+
await this.modelRegistry.refresh();
|
|
206
207
|
|
|
207
208
|
// Check for models.json errors
|
|
208
209
|
const loadError = this.modelRegistry.getError();
|
|
@@ -212,7 +213,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
212
213
|
|
|
213
214
|
// Load available models (built-in models still work even if models.json failed)
|
|
214
215
|
try {
|
|
215
|
-
const availableModels =
|
|
216
|
+
const availableModels = this.modelRegistry.getAvailable();
|
|
216
217
|
models = availableModels.map((model: Model<any>) => ({
|
|
217
218
|
provider: model.provider,
|
|
218
219
|
id: model.id,
|
|
@@ -490,8 +491,8 @@ export class ModelSelectorComponent extends Container {
|
|
|
490
491
|
return;
|
|
491
492
|
}
|
|
492
493
|
|
|
493
|
-
// Escape - close selector
|
|
494
|
-
if (isEscape(keyData)) {
|
|
494
|
+
// Escape or Ctrl+C - close selector
|
|
495
|
+
if (isEscape(keyData) || isCtrlC(keyData)) {
|
|
495
496
|
this.onCancelCallback();
|
|
496
497
|
return;
|
|
497
498
|
}
|
|
@@ -527,8 +528,8 @@ export class ModelSelectorComponent extends Container {
|
|
|
527
528
|
return;
|
|
528
529
|
}
|
|
529
530
|
|
|
530
|
-
// Escape - close menu only
|
|
531
|
-
if (isEscape(keyData)) {
|
|
531
|
+
// Escape or Ctrl+C - close menu only
|
|
532
|
+
if (isEscape(keyData) || isCtrlC(keyData)) {
|
|
532
533
|
this.closeMenu();
|
|
533
534
|
return;
|
|
534
535
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getOAuthProviders, type OAuthProviderInfo } from "@oh-my-pi/pi-ai";
|
|
2
|
-
import { Container, isArrowDown, isArrowUp, isEnter, isEscape, Spacer, TruncatedText } from "@oh-my-pi/pi-tui";
|
|
2
|
+
import { Container, isArrowDown, isArrowUp, isCtrlC, isEnter, isEscape, Spacer, TruncatedText } from "@oh-my-pi/pi-tui";
|
|
3
3
|
import type { AuthStorage } from "../../../core/auth-storage";
|
|
4
4
|
import { theme } from "../theme/theme";
|
|
5
5
|
import { DynamicBorder } from "./dynamic-border";
|
|
@@ -128,8 +128,8 @@ export class OAuthSelectorComponent extends Container {
|
|
|
128
128
|
this.updateList();
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
|
-
// Escape
|
|
132
|
-
else if (isEscape(keyData)) {
|
|
131
|
+
// Escape or Ctrl+C
|
|
132
|
+
else if (isEscape(keyData) || isCtrlC(keyData)) {
|
|
133
133
|
this.onCancelCallback();
|
|
134
134
|
}
|
|
135
135
|
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
12
12
|
import { getCapabilities } from "@oh-my-pi/pi-tui";
|
|
13
13
|
import type {
|
|
14
|
+
NotificationMethod,
|
|
14
15
|
SettingsManager,
|
|
15
16
|
StatusLinePreset,
|
|
16
17
|
StatusLineSeparatorStyle,
|
|
@@ -96,21 +97,59 @@ export const SETTINGS_DEFS: SettingDef[] = [
|
|
|
96
97
|
condition: () => !!getCapabilities().images,
|
|
97
98
|
},
|
|
98
99
|
{
|
|
99
|
-
id: "
|
|
100
|
+
id: "voiceEnabled",
|
|
101
|
+
tab: "config",
|
|
102
|
+
type: "boolean",
|
|
103
|
+
label: "Voice mode",
|
|
104
|
+
description: "Enable realtime voice input/output (Ctrl+Y toggle, auto-send on silence)",
|
|
105
|
+
get: (sm) => sm.getVoiceEnabled(),
|
|
106
|
+
set: (sm, v) => sm.setVoiceEnabled(v),
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: "completionNotification",
|
|
100
110
|
tab: "config",
|
|
101
111
|
type: "enum",
|
|
102
|
-
label: "
|
|
112
|
+
label: "Completion notification",
|
|
113
|
+
description: "Notify when the agent completes",
|
|
114
|
+
values: ["auto", "bell", "osc99", "osc9", "off"],
|
|
115
|
+
get: (sm) => sm.getNotificationOnComplete(),
|
|
116
|
+
set: (sm, v) => sm.setNotificationOnComplete(v as NotificationMethod),
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: "autoResizeImages",
|
|
120
|
+
tab: "config",
|
|
121
|
+
type: "boolean",
|
|
122
|
+
label: "Auto-resize images",
|
|
123
|
+
description: "Resize large images to 2000x2000 max for better model compatibility",
|
|
124
|
+
get: (sm) => sm.getImageAutoResize(),
|
|
125
|
+
set: (sm, v) => sm.setImageAutoResize(v),
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: "steeringMode",
|
|
129
|
+
tab: "config",
|
|
130
|
+
type: "enum",
|
|
131
|
+
label: "Steering mode",
|
|
103
132
|
description: "How to process queued messages while agent is working",
|
|
104
133
|
values: ["one-at-a-time", "all"],
|
|
105
|
-
get: (sm) => sm.
|
|
106
|
-
set: (sm, v) => sm.
|
|
134
|
+
get: (sm) => sm.getSteeringMode(),
|
|
135
|
+
set: (sm, v) => sm.setSteeringMode(v as "all" | "one-at-a-time"), // Also handled in session
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: "followUpMode",
|
|
139
|
+
tab: "config",
|
|
140
|
+
type: "enum",
|
|
141
|
+
label: "Follow-up mode",
|
|
142
|
+
description: "How to drain follow-up messages after a turn completes",
|
|
143
|
+
values: ["one-at-a-time", "all"],
|
|
144
|
+
get: (sm) => sm.getFollowUpMode(),
|
|
145
|
+
set: (sm, v) => sm.setFollowUpMode(v as "one-at-a-time" | "all"), // Also handled in session
|
|
107
146
|
},
|
|
108
147
|
{
|
|
109
148
|
id: "interruptMode",
|
|
110
149
|
tab: "config",
|
|
111
150
|
type: "enum",
|
|
112
151
|
label: "Interrupt mode",
|
|
113
|
-
description: "When
|
|
152
|
+
description: "When steering messages interrupt tool execution",
|
|
114
153
|
values: ["immediate", "wait"],
|
|
115
154
|
get: (sm) => sm.getInterruptMode(),
|
|
116
155
|
set: (sm, v) => sm.setInterruptMode(v as "immediate" | "wait"), // Also handled in session
|
|
@@ -133,6 +172,16 @@ export const SETTINGS_DEFS: SettingDef[] = [
|
|
|
133
172
|
get: (sm) => sm.getCollapseChangelog(),
|
|
134
173
|
set: (sm, v) => sm.setCollapseChangelog(v),
|
|
135
174
|
},
|
|
175
|
+
{
|
|
176
|
+
id: "doubleEscapeAction",
|
|
177
|
+
tab: "config",
|
|
178
|
+
type: "enum",
|
|
179
|
+
label: "Double-escape action",
|
|
180
|
+
description: "Action when pressing Escape twice with empty editor",
|
|
181
|
+
values: ["tree", "branch"],
|
|
182
|
+
get: (sm) => sm.getDoubleEscapeAction(),
|
|
183
|
+
set: (sm, v) => sm.setDoubleEscapeAction(v as "branch" | "tree"),
|
|
184
|
+
},
|
|
136
185
|
{
|
|
137
186
|
id: "bashInterceptor",
|
|
138
187
|
tab: "config",
|
|
@@ -359,7 +408,7 @@ export const SETTINGS_DEFS: SettingDef[] = [
|
|
|
359
408
|
id: "statusLineShowHooks",
|
|
360
409
|
tab: "status",
|
|
361
410
|
type: "boolean",
|
|
362
|
-
label: "Show
|
|
411
|
+
label: "Show extension status",
|
|
363
412
|
description: "Display hook status messages below status line",
|
|
364
413
|
get: (sm) => sm.getStatusLineShowHookStatus(),
|
|
365
414
|
set: (sm, v) => sm.setStatusLineShowHookStatus(v),
|
|
@@ -22,8 +22,8 @@ export function getSeparator(style: StatusLineSeparatorStyle, theme: Theme): Sep
|
|
|
22
22
|
left: theme.sep.powerlineThinLeft,
|
|
23
23
|
right: theme.sep.powerlineThinRight,
|
|
24
24
|
endCaps: {
|
|
25
|
-
left: theme.sep.
|
|
26
|
-
right: theme.sep.
|
|
25
|
+
left: theme.sep.powerlineRight,
|
|
26
|
+
right: theme.sep.powerlineLeft,
|
|
27
27
|
useBgAsFg: true,
|
|
28
28
|
},
|
|
29
29
|
};
|
|
@@ -46,8 +46,8 @@ export function getSeparator(style: StatusLineSeparatorStyle, theme: Theme): Sep
|
|
|
46
46
|
left: theme.sep.powerlineThinLeft,
|
|
47
47
|
right: theme.sep.powerlineThinRight,
|
|
48
48
|
endCaps: {
|
|
49
|
-
left: theme.sep.
|
|
50
|
-
right: theme.sep.
|
|
49
|
+
left: theme.sep.powerlineRight,
|
|
50
|
+
right: theme.sep.powerlineLeft,
|
|
51
51
|
useBgAsFg: true,
|
|
52
52
|
},
|
|
53
53
|
};
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
2
|
-
import { existsSync, type FSWatcher, readFileSync, watch } from "node:fs";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
1
|
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
|
5
2
|
import { type Component, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
3
|
+
import { type FSWatcher, watch } from "fs";
|
|
4
|
+
import { dirname, join } from "path";
|
|
6
5
|
import type { AgentSession } from "../../../core/agent-session";
|
|
7
6
|
import type { StatusLineSegmentOptions, StatusLineSettings } from "../../../core/settings-manager";
|
|
8
7
|
import { theme } from "../theme/theme";
|
|
@@ -23,11 +22,11 @@ function sanitizeStatusText(text: string): string {
|
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
/** Find the git root directory by walking up from cwd */
|
|
26
|
-
function findGitHeadPath(): string | null {
|
|
25
|
+
async function findGitHeadPath(): Promise<string | null> {
|
|
27
26
|
let dir = process.cwd();
|
|
28
27
|
while (true) {
|
|
29
28
|
const gitHeadPath = join(dir, ".git", "HEAD");
|
|
30
|
-
if (
|
|
29
|
+
if (await Bun.file(gitHeadPath).exists()) {
|
|
31
30
|
return gitHeadPath;
|
|
32
31
|
}
|
|
33
32
|
const parent = dirname(dir);
|
|
@@ -98,19 +97,20 @@ export class StatusLineComponent implements Component {
|
|
|
98
97
|
this.gitWatcher = null;
|
|
99
98
|
}
|
|
100
99
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
100
|
+
findGitHeadPath().then((gitHeadPath) => {
|
|
101
|
+
if (!gitHeadPath) return;
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
this.gitWatcher = watch(gitHeadPath, () => {
|
|
105
|
+
this.cachedBranch = undefined;
|
|
106
|
+
if (this.onBranchChange) {
|
|
107
|
+
this.onBranchChange();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
} catch {
|
|
111
|
+
// Silently fail
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
dispose(): void {
|
|
@@ -129,24 +129,27 @@ export class StatusLineComponent implements Component {
|
|
|
129
129
|
return this.cachedBranch;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
// Note: synchronous call to async function - will return undefined on first call
|
|
133
|
+
// This is acceptable since it's a cached value that will update on next render
|
|
134
|
+
findGitHeadPath().then(async (gitHeadPath) => {
|
|
134
135
|
if (!gitHeadPath) {
|
|
135
136
|
this.cachedBranch = null;
|
|
136
|
-
return
|
|
137
|
+
return;
|
|
137
138
|
}
|
|
138
|
-
|
|
139
|
+
try {
|
|
140
|
+
const content = (await Bun.file(gitHeadPath).text()).trim();
|
|
139
141
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
if (content.startsWith("ref: refs/heads/")) {
|
|
143
|
+
this.cachedBranch = content.slice(16);
|
|
144
|
+
} else {
|
|
145
|
+
this.cachedBranch = "detached";
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
this.cachedBranch = null;
|
|
144
149
|
}
|
|
145
|
-
}
|
|
146
|
-
this.cachedBranch = null;
|
|
147
|
-
}
|
|
150
|
+
});
|
|
148
151
|
|
|
149
|
-
return this.cachedBranch;
|
|
152
|
+
return this.cachedBranch ?? null;
|
|
150
153
|
}
|
|
151
154
|
|
|
152
155
|
private getGitStatus(): { staged: number; unstaged: number; untracked: number } | null {
|
|
@@ -156,12 +159,19 @@ export class StatusLineComponent implements Component {
|
|
|
156
159
|
}
|
|
157
160
|
|
|
158
161
|
try {
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
162
|
+
const result = Bun.spawnSync(["git", "status", "--porcelain"], {
|
|
163
|
+
stdout: "pipe",
|
|
164
|
+
stderr: "pipe",
|
|
163
165
|
});
|
|
164
166
|
|
|
167
|
+
if (!result.success) {
|
|
168
|
+
this.cachedGitStatus = null;
|
|
169
|
+
this.gitStatusLastFetch = now;
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const output = result.stdout.toString("utf8");
|
|
174
|
+
|
|
165
175
|
let staged = 0;
|
|
166
176
|
let unstaged = 0;
|
|
167
177
|
let untracked = 0;
|
|
@@ -333,7 +343,7 @@ export class StatusLineComponent implements Component {
|
|
|
333
343
|
? separatorDef.endCaps.right
|
|
334
344
|
: separatorDef.endCaps.left
|
|
335
345
|
: "";
|
|
336
|
-
const capPrefix = separatorDef.endCaps?.useBgAsFg ? bgAnsi.replace("\x1b[48;", "\x1b[38;") : sepAnsi;
|
|
346
|
+
const capPrefix = separatorDef.endCaps?.useBgAsFg ? bgAnsi.replace("\x1b[48;", "\x1b[38;") : bgAnsi + sepAnsi;
|
|
337
347
|
const capText = cap ? `${capPrefix}${cap}\x1b[0m` : "";
|
|
338
348
|
|
|
339
349
|
let content = bgAnsi + fgAnsi;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
2
3
|
import {
|
|
3
4
|
Box,
|
|
4
5
|
Container,
|
|
@@ -11,10 +12,10 @@ import {
|
|
|
11
12
|
type TUI,
|
|
12
13
|
} from "@oh-my-pi/pi-tui";
|
|
13
14
|
import stripAnsi from "strip-ansi";
|
|
14
|
-
import type { CustomTool } from "../../../core/custom-tools/types";
|
|
15
15
|
import { computeEditDiff, type EditDiffError, type EditDiffResult } from "../../../core/tools/edit-diff";
|
|
16
16
|
import { toolRenderers } from "../../../core/tools/renderers";
|
|
17
17
|
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from "../../../core/tools/truncate";
|
|
18
|
+
import { convertToPng } from "../../../utils/image-convert";
|
|
18
19
|
import { sanitizeBinaryOutput } from "../../../utils/shell";
|
|
19
20
|
import { getLanguageFromPath, highlightCode, theme } from "../theme/theme";
|
|
20
21
|
import { renderDiff } from "./diff";
|
|
@@ -202,7 +203,9 @@ function formatDiagnostics(diag: { errored: boolean; summary: string; messages:
|
|
|
202
203
|
const fileBranch = isLastFile ? theme.tree.last : theme.tree.branch;
|
|
203
204
|
|
|
204
205
|
// File header with icon
|
|
205
|
-
|
|
206
|
+
const fileLang = getLanguageFromPath(filePath);
|
|
207
|
+
const fileIcon = theme.fg("muted", theme.getLangIcon(fileLang));
|
|
208
|
+
output += `\n ${theme.fg("dim", fileBranch)} ${fileIcon} ${theme.fg("accent", filePath)}`;
|
|
206
209
|
shown++;
|
|
207
210
|
|
|
208
211
|
// Render diagnostics for this file
|
|
@@ -310,8 +313,8 @@ function formatArgsPreview(
|
|
|
310
313
|
* Convert absolute path to tilde notation if it's in home directory
|
|
311
314
|
*/
|
|
312
315
|
function shortenPath(path: string): string {
|
|
313
|
-
const home =
|
|
314
|
-
if (path.startsWith(home)) {
|
|
316
|
+
const home = homedir();
|
|
317
|
+
if (home && path.startsWith(home)) {
|
|
315
318
|
return `~${path.slice(home.length)}`;
|
|
316
319
|
}
|
|
317
320
|
return path;
|
|
@@ -341,7 +344,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
341
344
|
private expanded = false;
|
|
342
345
|
private showImages: boolean;
|
|
343
346
|
private isPartial = true;
|
|
344
|
-
private
|
|
347
|
+
private tool?: AgentTool;
|
|
345
348
|
private ui: TUI;
|
|
346
349
|
private cwd: string;
|
|
347
350
|
private result?: {
|
|
@@ -352,6 +355,8 @@ export class ToolExecutionComponent extends Container {
|
|
|
352
355
|
// Cached edit diff preview (computed when args arrive, before tool executes)
|
|
353
356
|
private editDiffPreview?: EditDiffResult | EditDiffError;
|
|
354
357
|
private editDiffArgsKey?: string; // Track which args the preview is for
|
|
358
|
+
// Cached converted images for Kitty protocol (which requires PNG), keyed by index
|
|
359
|
+
private convertedImages: Map<number, { data: string; mimeType: string }> = new Map();
|
|
355
360
|
// Spinner animation for partial task results
|
|
356
361
|
private spinnerFrame = 0;
|
|
357
362
|
private spinnerInterval: ReturnType<typeof setInterval> | null = null;
|
|
@@ -360,7 +365,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
360
365
|
toolName: string,
|
|
361
366
|
args: any,
|
|
362
367
|
options: ToolExecutionOptions = {},
|
|
363
|
-
|
|
368
|
+
tool: AgentTool | undefined,
|
|
364
369
|
ui: TUI,
|
|
365
370
|
cwd: string = process.cwd(),
|
|
366
371
|
) {
|
|
@@ -368,7 +373,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
368
373
|
this.toolName = toolName;
|
|
369
374
|
this.args = args;
|
|
370
375
|
this.showImages = options.showImages ?? true;
|
|
371
|
-
this.
|
|
376
|
+
this.tool = tool;
|
|
372
377
|
this.ui = ui;
|
|
373
378
|
this.cwd = cwd;
|
|
374
379
|
|
|
@@ -380,7 +385,8 @@ export class ToolExecutionComponent extends Container {
|
|
|
380
385
|
|
|
381
386
|
// Use Box for custom tools, bash, or built-in tools that have renderers
|
|
382
387
|
const hasRenderer = toolName in toolRenderers;
|
|
383
|
-
|
|
388
|
+
const hasCustomRenderer = !!(tool?.renderCall || tool?.renderResult);
|
|
389
|
+
if (hasCustomRenderer || toolName === "bash" || hasRenderer) {
|
|
384
390
|
this.addChild(this.contentBox);
|
|
385
391
|
} else {
|
|
386
392
|
this.addChild(this.contentText);
|
|
@@ -447,6 +453,39 @@ export class ToolExecutionComponent extends Container {
|
|
|
447
453
|
this.isPartial = isPartial;
|
|
448
454
|
this.updateSpinnerAnimation();
|
|
449
455
|
this.updateDisplay();
|
|
456
|
+
// Convert non-PNG images to PNG for Kitty protocol (async)
|
|
457
|
+
this.maybeConvertImagesForKitty();
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Convert non-PNG images to PNG for Kitty graphics protocol.
|
|
462
|
+
* Kitty requires PNG format (f=100), so JPEG/GIF/WebP won't display.
|
|
463
|
+
*/
|
|
464
|
+
private maybeConvertImagesForKitty(): void {
|
|
465
|
+
const caps = getCapabilities();
|
|
466
|
+
// Only needed for Kitty protocol
|
|
467
|
+
if (caps.images !== "kitty") return;
|
|
468
|
+
if (!this.result) return;
|
|
469
|
+
|
|
470
|
+
const imageBlocks = this.result.content?.filter((c: any) => c.type === "image") || [];
|
|
471
|
+
|
|
472
|
+
for (let i = 0; i < imageBlocks.length; i++) {
|
|
473
|
+
const img = imageBlocks[i];
|
|
474
|
+
if (!img.data || !img.mimeType) continue;
|
|
475
|
+
// Skip if already PNG or already converted
|
|
476
|
+
if (img.mimeType === "image/png") continue;
|
|
477
|
+
if (this.convertedImages.has(i)) continue;
|
|
478
|
+
|
|
479
|
+
// Convert async
|
|
480
|
+
const index = i;
|
|
481
|
+
convertToPng(img.data, img.mimeType).then((converted) => {
|
|
482
|
+
if (converted) {
|
|
483
|
+
this.convertedImages.set(index, converted);
|
|
484
|
+
this.updateDisplay();
|
|
485
|
+
this.ui.requestRender();
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
}
|
|
450
489
|
}
|
|
451
490
|
|
|
452
491
|
/**
|
|
@@ -497,17 +536,23 @@ export class ToolExecutionComponent extends Container {
|
|
|
497
536
|
: (text: string) => theme.bg("toolSuccessBg", text);
|
|
498
537
|
|
|
499
538
|
// Check for custom tool rendering
|
|
500
|
-
if (this.
|
|
539
|
+
if (this.tool && (this.tool.renderCall || this.tool.renderResult)) {
|
|
540
|
+
const tool = this.tool;
|
|
501
541
|
// Custom tools use Box for flexible component rendering
|
|
502
542
|
this.contentBox.setBgFn(bgFn);
|
|
503
543
|
this.contentBox.clear();
|
|
504
544
|
|
|
505
545
|
// Render call component
|
|
506
|
-
if (
|
|
546
|
+
if (tool.renderCall) {
|
|
507
547
|
try {
|
|
508
|
-
const callComponent =
|
|
548
|
+
const callComponent = tool.renderCall(this.args, theme);
|
|
509
549
|
if (callComponent) {
|
|
510
|
-
|
|
550
|
+
// Ensure component has invalidate() method for Component interface
|
|
551
|
+
const component = callComponent as any;
|
|
552
|
+
if (!component.invalidate) {
|
|
553
|
+
component.invalidate = () => {};
|
|
554
|
+
}
|
|
555
|
+
this.contentBox.addChild(component);
|
|
511
556
|
}
|
|
512
557
|
} catch {
|
|
513
558
|
// Fall back to default on error
|
|
@@ -519,15 +564,20 @@ export class ToolExecutionComponent extends Container {
|
|
|
519
564
|
}
|
|
520
565
|
|
|
521
566
|
// Render result component if we have a result
|
|
522
|
-
if (this.result &&
|
|
567
|
+
if (this.result && tool.renderResult) {
|
|
523
568
|
try {
|
|
524
|
-
const resultComponent =
|
|
569
|
+
const resultComponent = tool.renderResult(
|
|
525
570
|
{ content: this.result.content as any, details: this.result.details },
|
|
526
571
|
{ expanded: this.expanded, isPartial: this.isPartial, spinnerFrame: this.spinnerFrame },
|
|
527
572
|
theme,
|
|
528
573
|
);
|
|
529
574
|
if (resultComponent) {
|
|
530
|
-
|
|
575
|
+
// Ensure component has invalidate() method for Component interface
|
|
576
|
+
const component = resultComponent as any;
|
|
577
|
+
if (!component.invalidate) {
|
|
578
|
+
component.invalidate = () => {};
|
|
579
|
+
}
|
|
580
|
+
this.contentBox.addChild(component);
|
|
531
581
|
}
|
|
532
582
|
} catch {
|
|
533
583
|
// Fall back to showing raw output on error
|
|
@@ -558,7 +608,12 @@ export class ToolExecutionComponent extends Container {
|
|
|
558
608
|
try {
|
|
559
609
|
const callComponent = renderer.renderCall(this.args, theme);
|
|
560
610
|
if (callComponent) {
|
|
561
|
-
|
|
611
|
+
// Ensure component has invalidate() method for Component interface
|
|
612
|
+
const component = callComponent as any;
|
|
613
|
+
if (!component.invalidate) {
|
|
614
|
+
component.invalidate = () => {};
|
|
615
|
+
}
|
|
616
|
+
this.contentBox.addChild(component);
|
|
562
617
|
}
|
|
563
618
|
} catch {
|
|
564
619
|
// Fall back to default on error
|
|
@@ -574,7 +629,12 @@ export class ToolExecutionComponent extends Container {
|
|
|
574
629
|
theme,
|
|
575
630
|
);
|
|
576
631
|
if (resultComponent) {
|
|
577
|
-
|
|
632
|
+
// Ensure component has invalidate() method for Component interface
|
|
633
|
+
const component = resultComponent as any;
|
|
634
|
+
if (!component.invalidate) {
|
|
635
|
+
component.invalidate = () => {};
|
|
636
|
+
}
|
|
637
|
+
this.contentBox.addChild(component);
|
|
578
638
|
}
|
|
579
639
|
} catch {
|
|
580
640
|
// Fall back to showing raw output on error
|
|
@@ -604,14 +664,25 @@ export class ToolExecutionComponent extends Container {
|
|
|
604
664
|
const imageBlocks = this.result.content?.filter((c: any) => c.type === "image") || [];
|
|
605
665
|
const caps = getCapabilities();
|
|
606
666
|
|
|
607
|
-
for (
|
|
667
|
+
for (let i = 0; i < imageBlocks.length; i++) {
|
|
668
|
+
const img = imageBlocks[i];
|
|
608
669
|
if (caps.images && this.showImages && img.data && img.mimeType) {
|
|
670
|
+
// Use converted PNG for Kitty protocol if available
|
|
671
|
+
const converted = this.convertedImages.get(i);
|
|
672
|
+
const imageData = converted?.data ?? img.data;
|
|
673
|
+
const imageMimeType = converted?.mimeType ?? img.mimeType;
|
|
674
|
+
|
|
675
|
+
// For Kitty, skip non-PNG images that haven't been converted yet
|
|
676
|
+
if (caps.images === "kitty" && imageMimeType !== "image/png") {
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
|
|
609
680
|
const spacer = new Spacer(1);
|
|
610
681
|
this.addChild(spacer);
|
|
611
682
|
this.imageSpacers.push(spacer);
|
|
612
683
|
const imageComponent = new Image(
|
|
613
|
-
|
|
614
|
-
|
|
684
|
+
imageData,
|
|
685
|
+
imageMimeType,
|
|
615
686
|
{ fallbackColor: (s: string) => theme.fg("toolOutput", s) },
|
|
616
687
|
{ maxWidthCells: 60 },
|
|
617
688
|
);
|
|
@@ -840,6 +911,8 @@ export class ToolExecutionComponent extends Container {
|
|
|
840
911
|
} else if (this.toolName === "edit") {
|
|
841
912
|
const rawPath = this.args?.file_path || this.args?.path || "";
|
|
842
913
|
const path = shortenPath(rawPath);
|
|
914
|
+
const editLanguage = getLanguageFromPath(rawPath) ?? "text";
|
|
915
|
+
const editIcon = theme.fg("muted", theme.getLangIcon(editLanguage));
|
|
843
916
|
|
|
844
917
|
// Build path display, appending :line if we have diff info
|
|
845
918
|
let pathDisplay = path ? theme.fg("accent", path) : theme.fg("toolOutput", theme.format.ellipsis);
|
|
@@ -852,9 +925,8 @@ export class ToolExecutionComponent extends Container {
|
|
|
852
925
|
pathDisplay += theme.fg("warning", `:${firstChangedLine}`);
|
|
853
926
|
}
|
|
854
927
|
|
|
855
|
-
text = `${theme.fg("toolTitle", theme.bold("edit"))} ${pathDisplay}`;
|
|
928
|
+
text = `${theme.fg("toolTitle", theme.bold("edit"))} ${editIcon} ${pathDisplay}`;
|
|
856
929
|
|
|
857
|
-
const editLanguage = getLanguageFromPath(rawPath) ?? "text";
|
|
858
930
|
const editLineCount = countLines(this.args?.newText ?? this.args?.oldText ?? "");
|
|
859
931
|
text += `\n${formatMetadataLine(editLineCount, editLanguage)}`;
|
|
860
932
|
|