@oh-my-pi/pi-coding-agent 1.337.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 +1228 -0
- package/README.md +1041 -0
- package/docs/compaction.md +403 -0
- package/docs/custom-tools.md +541 -0
- package/docs/extension-loading.md +1004 -0
- package/docs/hooks.md +867 -0
- package/docs/rpc.md +1040 -0
- package/docs/sdk.md +994 -0
- package/docs/session-tree-plan.md +441 -0
- package/docs/session.md +240 -0
- package/docs/skills.md +290 -0
- package/docs/theme.md +637 -0
- package/docs/tree.md +197 -0
- package/docs/tui.md +341 -0
- package/examples/README.md +21 -0
- package/examples/custom-tools/README.md +124 -0
- package/examples/custom-tools/hello/index.ts +20 -0
- package/examples/custom-tools/question/index.ts +84 -0
- package/examples/custom-tools/subagent/README.md +172 -0
- package/examples/custom-tools/subagent/agents/planner.md +37 -0
- package/examples/custom-tools/subagent/agents/reviewer.md +35 -0
- package/examples/custom-tools/subagent/agents/scout.md +50 -0
- package/examples/custom-tools/subagent/agents/worker.md +24 -0
- package/examples/custom-tools/subagent/agents.ts +156 -0
- package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
- package/examples/custom-tools/subagent/commands/implement.md +10 -0
- package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
- package/examples/custom-tools/subagent/index.ts +1002 -0
- package/examples/custom-tools/todo/index.ts +212 -0
- package/examples/hooks/README.md +56 -0
- package/examples/hooks/auto-commit-on-exit.ts +49 -0
- package/examples/hooks/confirm-destructive.ts +59 -0
- package/examples/hooks/custom-compaction.ts +116 -0
- package/examples/hooks/dirty-repo-guard.ts +52 -0
- package/examples/hooks/file-trigger.ts +41 -0
- package/examples/hooks/git-checkpoint.ts +53 -0
- package/examples/hooks/handoff.ts +150 -0
- package/examples/hooks/permission-gate.ts +34 -0
- package/examples/hooks/protected-paths.ts +30 -0
- package/examples/hooks/qna.ts +119 -0
- package/examples/hooks/snake.ts +343 -0
- package/examples/hooks/status-line.ts +40 -0
- package/examples/sdk/01-minimal.ts +22 -0
- package/examples/sdk/02-custom-model.ts +49 -0
- package/examples/sdk/03-custom-prompt.ts +44 -0
- package/examples/sdk/04-skills.ts +44 -0
- package/examples/sdk/05-tools.ts +90 -0
- package/examples/sdk/06-hooks.ts +61 -0
- package/examples/sdk/07-context-files.ts +36 -0
- package/examples/sdk/08-slash-commands.ts +42 -0
- package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
- package/examples/sdk/10-settings.ts +38 -0
- package/examples/sdk/11-sessions.ts +48 -0
- package/examples/sdk/12-full-control.ts +95 -0
- package/examples/sdk/README.md +154 -0
- package/package.json +81 -0
- package/src/cli/args.ts +246 -0
- package/src/cli/file-processor.ts +72 -0
- package/src/cli/list-models.ts +104 -0
- package/src/cli/plugin-cli.ts +650 -0
- package/src/cli/session-picker.ts +41 -0
- package/src/cli.ts +10 -0
- package/src/commands/init.md +20 -0
- package/src/config.ts +159 -0
- package/src/core/agent-session.ts +1900 -0
- package/src/core/auth-storage.ts +236 -0
- package/src/core/bash-executor.ts +196 -0
- package/src/core/compaction/branch-summarization.ts +343 -0
- package/src/core/compaction/compaction.ts +742 -0
- package/src/core/compaction/index.ts +7 -0
- package/src/core/compaction/utils.ts +154 -0
- package/src/core/custom-tools/index.ts +21 -0
- package/src/core/custom-tools/loader.ts +248 -0
- package/src/core/custom-tools/types.ts +169 -0
- package/src/core/custom-tools/wrapper.ts +28 -0
- package/src/core/exec.ts +129 -0
- package/src/core/export-html/index.ts +211 -0
- package/src/core/export-html/template.css +781 -0
- package/src/core/export-html/template.html +54 -0
- package/src/core/export-html/template.js +1185 -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/hooks/index.ts +16 -0
- package/src/core/hooks/loader.ts +312 -0
- package/src/core/hooks/runner.ts +434 -0
- package/src/core/hooks/tool-wrapper.ts +99 -0
- package/src/core/hooks/types.ts +773 -0
- package/src/core/index.ts +52 -0
- package/src/core/mcp/client.ts +158 -0
- package/src/core/mcp/config.ts +154 -0
- package/src/core/mcp/index.ts +45 -0
- package/src/core/mcp/loader.ts +68 -0
- package/src/core/mcp/manager.ts +181 -0
- package/src/core/mcp/tool-bridge.ts +148 -0
- package/src/core/mcp/transports/http.ts +316 -0
- package/src/core/mcp/transports/index.ts +6 -0
- package/src/core/mcp/transports/stdio.ts +252 -0
- package/src/core/mcp/types.ts +220 -0
- package/src/core/messages.ts +189 -0
- package/src/core/model-registry.ts +317 -0
- package/src/core/model-resolver.ts +393 -0
- package/src/core/plugins/doctor.ts +59 -0
- package/src/core/plugins/index.ts +38 -0
- package/src/core/plugins/installer.ts +189 -0
- package/src/core/plugins/loader.ts +338 -0
- package/src/core/plugins/manager.ts +672 -0
- package/src/core/plugins/parser.ts +105 -0
- package/src/core/plugins/paths.ts +32 -0
- package/src/core/plugins/types.ts +190 -0
- package/src/core/sdk.ts +760 -0
- package/src/core/session-manager.ts +1128 -0
- package/src/core/settings-manager.ts +443 -0
- package/src/core/skills.ts +437 -0
- package/src/core/slash-commands.ts +248 -0
- package/src/core/system-prompt.ts +439 -0
- package/src/core/timings.ts +25 -0
- package/src/core/tools/ask.ts +211 -0
- package/src/core/tools/bash-interceptor.ts +120 -0
- package/src/core/tools/bash.ts +250 -0
- package/src/core/tools/context.ts +32 -0
- package/src/core/tools/edit-diff.ts +475 -0
- package/src/core/tools/edit.ts +208 -0
- package/src/core/tools/exa/company.ts +59 -0
- package/src/core/tools/exa/index.ts +64 -0
- package/src/core/tools/exa/linkedin.ts +59 -0
- package/src/core/tools/exa/logger.ts +56 -0
- package/src/core/tools/exa/mcp-client.ts +368 -0
- package/src/core/tools/exa/render.ts +196 -0
- package/src/core/tools/exa/researcher.ts +90 -0
- package/src/core/tools/exa/search.ts +337 -0
- package/src/core/tools/exa/types.ts +168 -0
- package/src/core/tools/exa/websets.ts +248 -0
- package/src/core/tools/find.ts +261 -0
- package/src/core/tools/grep.ts +555 -0
- package/src/core/tools/index.ts +202 -0
- package/src/core/tools/ls.ts +140 -0
- package/src/core/tools/lsp/client.ts +605 -0
- package/src/core/tools/lsp/config.ts +147 -0
- package/src/core/tools/lsp/edits.ts +101 -0
- package/src/core/tools/lsp/index.ts +804 -0
- package/src/core/tools/lsp/render.ts +447 -0
- package/src/core/tools/lsp/rust-analyzer.ts +145 -0
- package/src/core/tools/lsp/types.ts +463 -0
- package/src/core/tools/lsp/utils.ts +486 -0
- package/src/core/tools/notebook.ts +229 -0
- package/src/core/tools/path-utils.ts +61 -0
- package/src/core/tools/read.ts +240 -0
- package/src/core/tools/renderers.ts +540 -0
- package/src/core/tools/task/agents.ts +153 -0
- package/src/core/tools/task/artifacts.ts +114 -0
- package/src/core/tools/task/bundled-agents/browser.md +71 -0
- package/src/core/tools/task/bundled-agents/explore.md +82 -0
- package/src/core/tools/task/bundled-agents/plan.md +54 -0
- package/src/core/tools/task/bundled-agents/reviewer.md +59 -0
- package/src/core/tools/task/bundled-agents/task.md +53 -0
- package/src/core/tools/task/bundled-commands/architect-plan.md +10 -0
- package/src/core/tools/task/bundled-commands/implement-with-critic.md +11 -0
- package/src/core/tools/task/bundled-commands/implement.md +11 -0
- package/src/core/tools/task/commands.ts +213 -0
- package/src/core/tools/task/discovery.ts +208 -0
- package/src/core/tools/task/executor.ts +367 -0
- package/src/core/tools/task/index.ts +388 -0
- package/src/core/tools/task/model-resolver.ts +115 -0
- package/src/core/tools/task/parallel.ts +38 -0
- package/src/core/tools/task/render.ts +232 -0
- package/src/core/tools/task/types.ts +99 -0
- package/src/core/tools/truncate.ts +265 -0
- package/src/core/tools/web-fetch.ts +2370 -0
- package/src/core/tools/web-search/auth.ts +193 -0
- package/src/core/tools/web-search/index.ts +537 -0
- package/src/core/tools/web-search/providers/anthropic.ts +198 -0
- package/src/core/tools/web-search/providers/exa.ts +302 -0
- package/src/core/tools/web-search/providers/perplexity.ts +195 -0
- package/src/core/tools/web-search/render.ts +182 -0
- package/src/core/tools/web-search/types.ts +180 -0
- package/src/core/tools/write.ts +99 -0
- package/src/index.ts +176 -0
- package/src/main.ts +464 -0
- package/src/migrations.ts +135 -0
- package/src/modes/index.ts +43 -0
- package/src/modes/interactive/components/armin.ts +382 -0
- package/src/modes/interactive/components/assistant-message.ts +86 -0
- package/src/modes/interactive/components/bash-execution.ts +196 -0
- package/src/modes/interactive/components/bordered-loader.ts +41 -0
- package/src/modes/interactive/components/branch-summary-message.ts +42 -0
- package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
- package/src/modes/interactive/components/custom-editor.ts +122 -0
- package/src/modes/interactive/components/diff.ts +147 -0
- package/src/modes/interactive/components/dynamic-border.ts +25 -0
- package/src/modes/interactive/components/footer.ts +381 -0
- package/src/modes/interactive/components/hook-editor.ts +117 -0
- package/src/modes/interactive/components/hook-input.ts +64 -0
- package/src/modes/interactive/components/hook-message.ts +96 -0
- package/src/modes/interactive/components/hook-selector.ts +91 -0
- package/src/modes/interactive/components/model-selector.ts +247 -0
- package/src/modes/interactive/components/oauth-selector.ts +120 -0
- package/src/modes/interactive/components/plugin-settings.ts +479 -0
- package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
- package/src/modes/interactive/components/session-selector.ts +204 -0
- package/src/modes/interactive/components/settings-selector.ts +453 -0
- package/src/modes/interactive/components/show-images-selector.ts +45 -0
- package/src/modes/interactive/components/theme-selector.ts +62 -0
- package/src/modes/interactive/components/thinking-selector.ts +64 -0
- package/src/modes/interactive/components/tool-execution.ts +675 -0
- package/src/modes/interactive/components/tree-selector.ts +866 -0
- package/src/modes/interactive/components/user-message-selector.ts +159 -0
- package/src/modes/interactive/components/user-message.ts +18 -0
- package/src/modes/interactive/components/visual-truncate.ts +50 -0
- package/src/modes/interactive/components/welcome.ts +183 -0
- package/src/modes/interactive/interactive-mode.ts +2516 -0
- package/src/modes/interactive/theme/dark.json +101 -0
- package/src/modes/interactive/theme/light.json +98 -0
- package/src/modes/interactive/theme/theme-schema.json +308 -0
- package/src/modes/interactive/theme/theme.ts +998 -0
- package/src/modes/print-mode.ts +128 -0
- package/src/modes/rpc/rpc-client.ts +527 -0
- package/src/modes/rpc/rpc-mode.ts +483 -0
- package/src/modes/rpc/rpc-types.ts +203 -0
- package/src/utils/changelog.ts +99 -0
- package/src/utils/clipboard.ts +265 -0
- package/src/utils/fuzzy.ts +108 -0
- package/src/utils/mime.ts +30 -0
- package/src/utils/shell.ts +276 -0
- package/src/utils/tools-manager.ts +274 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Box, Markdown, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
2
|
+
import type { BranchSummaryMessage } from "../../../core/messages.js";
|
|
3
|
+
import { getMarkdownTheme, theme } from "../theme/theme.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Component that renders a branch summary message with collapsed/expanded state.
|
|
7
|
+
* Uses same background color as hook messages for visual consistency.
|
|
8
|
+
*/
|
|
9
|
+
export class BranchSummaryMessageComponent extends Box {
|
|
10
|
+
private expanded = false;
|
|
11
|
+
private message: BranchSummaryMessage;
|
|
12
|
+
|
|
13
|
+
constructor(message: BranchSummaryMessage) {
|
|
14
|
+
super(1, 1, (t) => theme.bg("customMessageBg", t));
|
|
15
|
+
this.message = message;
|
|
16
|
+
this.updateDisplay();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
setExpanded(expanded: boolean): void {
|
|
20
|
+
this.expanded = expanded;
|
|
21
|
+
this.updateDisplay();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private updateDisplay(): void {
|
|
25
|
+
this.clear();
|
|
26
|
+
|
|
27
|
+
const label = theme.fg("customMessageLabel", `\x1b[1m[branch]\x1b[22m`);
|
|
28
|
+
this.addChild(new Text(label, 0, 0));
|
|
29
|
+
this.addChild(new Spacer(1));
|
|
30
|
+
|
|
31
|
+
if (this.expanded) {
|
|
32
|
+
const header = "**Branch Summary**\n\n";
|
|
33
|
+
this.addChild(
|
|
34
|
+
new Markdown(header + this.message.summary, 0, 0, getMarkdownTheme(), {
|
|
35
|
+
color: (text: string) => theme.fg("customMessageText", text),
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
} else {
|
|
39
|
+
this.addChild(new Text(theme.fg("customMessageText", "Branch summary (ctrl+o to expand)"), 0, 0));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Box, Markdown, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
2
|
+
import type { CompactionSummaryMessage } from "../../../core/messages.js";
|
|
3
|
+
import { getMarkdownTheme, theme } from "../theme/theme.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Component that renders a compaction message with collapsed/expanded state.
|
|
7
|
+
* Uses same background color as hook messages for visual consistency.
|
|
8
|
+
*/
|
|
9
|
+
export class CompactionSummaryMessageComponent extends Box {
|
|
10
|
+
private expanded = false;
|
|
11
|
+
private message: CompactionSummaryMessage;
|
|
12
|
+
|
|
13
|
+
constructor(message: CompactionSummaryMessage) {
|
|
14
|
+
super(1, 1, (t) => theme.bg("customMessageBg", t));
|
|
15
|
+
this.message = message;
|
|
16
|
+
this.updateDisplay();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
setExpanded(expanded: boolean): void {
|
|
20
|
+
this.expanded = expanded;
|
|
21
|
+
this.updateDisplay();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private updateDisplay(): void {
|
|
25
|
+
this.clear();
|
|
26
|
+
|
|
27
|
+
const tokenStr = this.message.tokensBefore.toLocaleString();
|
|
28
|
+
const label = theme.fg("customMessageLabel", `\x1b[1m[compaction]\x1b[22m`);
|
|
29
|
+
this.addChild(new Text(label, 0, 0));
|
|
30
|
+
this.addChild(new Spacer(1));
|
|
31
|
+
|
|
32
|
+
if (this.expanded) {
|
|
33
|
+
const header = `**Compacted from ${tokenStr} tokens**\n\n`;
|
|
34
|
+
this.addChild(
|
|
35
|
+
new Markdown(header + this.message.summary, 0, 0, getMarkdownTheme(), {
|
|
36
|
+
color: (text: string) => theme.fg("customMessageText", text),
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
} else {
|
|
40
|
+
this.addChild(
|
|
41
|
+
new Text(theme.fg("customMessageText", `Compacted from ${tokenStr} tokens (ctrl+o to expand)`), 0, 0),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Editor,
|
|
3
|
+
isCtrlC,
|
|
4
|
+
isCtrlD,
|
|
5
|
+
isCtrlG,
|
|
6
|
+
isCtrlL,
|
|
7
|
+
isCtrlO,
|
|
8
|
+
isCtrlP,
|
|
9
|
+
isCtrlT,
|
|
10
|
+
isCtrlV,
|
|
11
|
+
isCtrlZ,
|
|
12
|
+
isEscape,
|
|
13
|
+
isShiftCtrlP,
|
|
14
|
+
isShiftTab,
|
|
15
|
+
} from "@oh-my-pi/pi-tui";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Custom editor that handles Escape and Ctrl+C keys for coding-agent
|
|
19
|
+
*/
|
|
20
|
+
export class CustomEditor extends Editor {
|
|
21
|
+
public onEscape?: () => void;
|
|
22
|
+
public onCtrlC?: () => void;
|
|
23
|
+
public onCtrlD?: () => void;
|
|
24
|
+
public onShiftTab?: () => void;
|
|
25
|
+
public onCtrlP?: () => void;
|
|
26
|
+
public onShiftCtrlP?: () => void;
|
|
27
|
+
public onCtrlL?: () => void;
|
|
28
|
+
public onCtrlO?: () => void;
|
|
29
|
+
public onCtrlT?: () => void;
|
|
30
|
+
public onCtrlG?: () => void;
|
|
31
|
+
public onCtrlZ?: () => void;
|
|
32
|
+
public onQuestionMark?: () => void;
|
|
33
|
+
/** Called when Ctrl+V is pressed. Returns true if handled (image found), false to fall through to text paste. */
|
|
34
|
+
public onCtrlV?: () => Promise<boolean>;
|
|
35
|
+
|
|
36
|
+
handleInput(data: string): void {
|
|
37
|
+
// Intercept Ctrl+V for image paste (async - fires and handles result)
|
|
38
|
+
if (isCtrlV(data) && this.onCtrlV) {
|
|
39
|
+
void this.onCtrlV();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Intercept Ctrl+G for external editor
|
|
44
|
+
if (isCtrlG(data) && this.onCtrlG) {
|
|
45
|
+
this.onCtrlG();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Intercept Ctrl+Z for suspend
|
|
50
|
+
if (isCtrlZ(data) && this.onCtrlZ) {
|
|
51
|
+
this.onCtrlZ();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Intercept Ctrl+T for thinking block visibility toggle
|
|
56
|
+
if (isCtrlT(data) && this.onCtrlT) {
|
|
57
|
+
this.onCtrlT();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Intercept Ctrl+L for model selector
|
|
62
|
+
if (isCtrlL(data) && this.onCtrlL) {
|
|
63
|
+
this.onCtrlL();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Intercept Ctrl+O for tool output expansion
|
|
68
|
+
if (isCtrlO(data) && this.onCtrlO) {
|
|
69
|
+
this.onCtrlO();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Intercept Shift+Ctrl+P for backward model cycling (check before Ctrl+P)
|
|
74
|
+
if (isShiftCtrlP(data) && this.onShiftCtrlP) {
|
|
75
|
+
this.onShiftCtrlP();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Intercept Ctrl+P for model cycling
|
|
80
|
+
if (isCtrlP(data) && this.onCtrlP) {
|
|
81
|
+
this.onCtrlP();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Intercept Shift+Tab for thinking level cycling
|
|
86
|
+
if (isShiftTab(data) && this.onShiftTab) {
|
|
87
|
+
this.onShiftTab();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Intercept Escape key - but only if autocomplete is NOT active
|
|
92
|
+
// (let parent handle escape for autocomplete cancellation)
|
|
93
|
+
if (isEscape(data) && this.onEscape && !this.isShowingAutocomplete()) {
|
|
94
|
+
this.onEscape();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Intercept Ctrl+C
|
|
99
|
+
if (isCtrlC(data) && this.onCtrlC) {
|
|
100
|
+
this.onCtrlC();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Intercept Ctrl+D (only when editor is empty)
|
|
105
|
+
if (isCtrlD(data)) {
|
|
106
|
+
if (this.getText().length === 0 && this.onCtrlD) {
|
|
107
|
+
this.onCtrlD();
|
|
108
|
+
}
|
|
109
|
+
// Always consume Ctrl+D (don't pass to parent)
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Intercept ? when editor is empty to show hotkeys
|
|
114
|
+
if (data === "?" && this.getText().length === 0 && this.onQuestionMark) {
|
|
115
|
+
this.onQuestionMark();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Pass to parent for normal handling
|
|
120
|
+
super.handleInput(data);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import * as Diff from "diff";
|
|
2
|
+
import { theme } from "../theme/theme.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parse diff line to extract prefix, line number, and content.
|
|
6
|
+
* Format: "+123 content" or "-123 content" or " 123 content" or " ..."
|
|
7
|
+
*/
|
|
8
|
+
function parseDiffLine(line: string): { prefix: string; lineNum: string; content: string } | null {
|
|
9
|
+
const match = line.match(/^([+-\s])(\s*\d*)\s(.*)$/);
|
|
10
|
+
if (!match) return null;
|
|
11
|
+
return { prefix: match[1], lineNum: match[2], content: match[3] };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Replace tabs with spaces for consistent rendering.
|
|
16
|
+
*/
|
|
17
|
+
function replaceTabs(text: string): string {
|
|
18
|
+
return text.replace(/\t/g, " ");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Compute word-level diff and render with inverse on changed parts.
|
|
23
|
+
* Uses diffWords which groups whitespace with adjacent words for cleaner highlighting.
|
|
24
|
+
* Strips leading whitespace from inverse to avoid highlighting indentation.
|
|
25
|
+
*/
|
|
26
|
+
function renderIntraLineDiff(oldContent: string, newContent: string): { removedLine: string; addedLine: string } {
|
|
27
|
+
const wordDiff = Diff.diffWords(oldContent, newContent);
|
|
28
|
+
|
|
29
|
+
let removedLine = "";
|
|
30
|
+
let addedLine = "";
|
|
31
|
+
let isFirstRemoved = true;
|
|
32
|
+
let isFirstAdded = true;
|
|
33
|
+
|
|
34
|
+
for (const part of wordDiff) {
|
|
35
|
+
if (part.removed) {
|
|
36
|
+
let value = part.value;
|
|
37
|
+
// Strip leading whitespace from the first removed part
|
|
38
|
+
if (isFirstRemoved) {
|
|
39
|
+
const leadingWs = value.match(/^(\s*)/)?.[1] || "";
|
|
40
|
+
value = value.slice(leadingWs.length);
|
|
41
|
+
removedLine += leadingWs;
|
|
42
|
+
isFirstRemoved = false;
|
|
43
|
+
}
|
|
44
|
+
if (value) {
|
|
45
|
+
removedLine += theme.inverse(value);
|
|
46
|
+
}
|
|
47
|
+
} else if (part.added) {
|
|
48
|
+
let value = part.value;
|
|
49
|
+
// Strip leading whitespace from the first added part
|
|
50
|
+
if (isFirstAdded) {
|
|
51
|
+
const leadingWs = value.match(/^(\s*)/)?.[1] || "";
|
|
52
|
+
value = value.slice(leadingWs.length);
|
|
53
|
+
addedLine += leadingWs;
|
|
54
|
+
isFirstAdded = false;
|
|
55
|
+
}
|
|
56
|
+
if (value) {
|
|
57
|
+
addedLine += theme.inverse(value);
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
removedLine += part.value;
|
|
61
|
+
addedLine += part.value;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { removedLine, addedLine };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface RenderDiffOptions {
|
|
69
|
+
/** File path (unused, kept for API compatibility) */
|
|
70
|
+
filePath?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Render a diff string with colored lines and intra-line change highlighting.
|
|
75
|
+
* - Context lines: dim/gray
|
|
76
|
+
* - Removed lines: red, with inverse on changed tokens
|
|
77
|
+
* - Added lines: green, with inverse on changed tokens
|
|
78
|
+
*/
|
|
79
|
+
export function renderDiff(diffText: string, _options: RenderDiffOptions = {}): string {
|
|
80
|
+
const lines = diffText.split("\n");
|
|
81
|
+
const result: string[] = [];
|
|
82
|
+
|
|
83
|
+
let i = 0;
|
|
84
|
+
while (i < lines.length) {
|
|
85
|
+
const line = lines[i];
|
|
86
|
+
const parsed = parseDiffLine(line);
|
|
87
|
+
|
|
88
|
+
if (!parsed) {
|
|
89
|
+
result.push(theme.fg("toolDiffContext", line));
|
|
90
|
+
i++;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (parsed.prefix === "-") {
|
|
95
|
+
// Collect consecutive removed lines
|
|
96
|
+
const removedLines: { lineNum: string; content: string }[] = [];
|
|
97
|
+
while (i < lines.length) {
|
|
98
|
+
const p = parseDiffLine(lines[i]);
|
|
99
|
+
if (!p || p.prefix !== "-") break;
|
|
100
|
+
removedLines.push({ lineNum: p.lineNum, content: p.content });
|
|
101
|
+
i++;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Collect consecutive added lines
|
|
105
|
+
const addedLines: { lineNum: string; content: string }[] = [];
|
|
106
|
+
while (i < lines.length) {
|
|
107
|
+
const p = parseDiffLine(lines[i]);
|
|
108
|
+
if (!p || p.prefix !== "+") break;
|
|
109
|
+
addedLines.push({ lineNum: p.lineNum, content: p.content });
|
|
110
|
+
i++;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Only do intra-line diffing when there's exactly one removed and one added line
|
|
114
|
+
// (indicating a single line modification). Otherwise, show lines as-is.
|
|
115
|
+
if (removedLines.length === 1 && addedLines.length === 1) {
|
|
116
|
+
const removed = removedLines[0];
|
|
117
|
+
const added = addedLines[0];
|
|
118
|
+
|
|
119
|
+
const { removedLine, addedLine } = renderIntraLineDiff(
|
|
120
|
+
replaceTabs(removed.content),
|
|
121
|
+
replaceTabs(added.content),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
result.push(theme.fg("toolDiffRemoved", `-${removed.lineNum} ${removedLine}`));
|
|
125
|
+
result.push(theme.fg("toolDiffAdded", `+${added.lineNum} ${addedLine}`));
|
|
126
|
+
} else {
|
|
127
|
+
// Show all removed lines first, then all added lines
|
|
128
|
+
for (const removed of removedLines) {
|
|
129
|
+
result.push(theme.fg("toolDiffRemoved", `-${removed.lineNum} ${replaceTabs(removed.content)}`));
|
|
130
|
+
}
|
|
131
|
+
for (const added of addedLines) {
|
|
132
|
+
result.push(theme.fg("toolDiffAdded", `+${added.lineNum} ${replaceTabs(added.content)}`));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} else if (parsed.prefix === "+") {
|
|
136
|
+
// Standalone added line
|
|
137
|
+
result.push(theme.fg("toolDiffAdded", `+${parsed.lineNum} ${replaceTabs(parsed.content)}`));
|
|
138
|
+
i++;
|
|
139
|
+
} else {
|
|
140
|
+
// Context line
|
|
141
|
+
result.push(theme.fg("toolDiffContext", ` ${parsed.lineNum} ${replaceTabs(parsed.content)}`));
|
|
142
|
+
i++;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return result.join("\n");
|
|
147
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Component } from "@oh-my-pi/pi-tui";
|
|
2
|
+
import { theme } from "../theme/theme.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Dynamic border component that adjusts to viewport width.
|
|
6
|
+
*
|
|
7
|
+
* Note: When used from hooks loaded via jiti, the global `theme` may be undefined
|
|
8
|
+
* because jiti creates a separate module cache. Always pass an explicit color
|
|
9
|
+
* function when using DynamicBorder in components exported for hook use.
|
|
10
|
+
*/
|
|
11
|
+
export class DynamicBorder implements Component {
|
|
12
|
+
private color: (str: string) => string;
|
|
13
|
+
|
|
14
|
+
constructor(color: (str: string) => string = (str) => theme.fg("border", str)) {
|
|
15
|
+
this.color = color;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
invalidate(): void {
|
|
19
|
+
// No cached state to invalidate currently
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
render(width: number): string[] {
|
|
23
|
+
return [this.color("─".repeat(Math.max(1, width)))];
|
|
24
|
+
}
|
|
25
|
+
}
|