@oh-my-pi/pi-coding-agent 4.3.2 → 4.4.5
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 +43 -0
- package/package.json +5 -6
- package/src/core/frontmatter.ts +98 -0
- package/src/core/keybindings.ts +1 -1
- package/src/core/prompt-templates.ts +5 -34
- package/src/core/sdk.ts +3 -0
- package/src/core/skills.ts +3 -3
- package/src/core/slash-commands.ts +14 -5
- package/src/core/tools/calculator.ts +1 -1
- package/src/core/tools/edit.ts +2 -2
- package/src/core/tools/exa/render.ts +23 -11
- package/src/core/tools/index.test.ts +2 -0
- package/src/core/tools/index.ts +3 -0
- package/src/core/tools/jtd-to-json-schema.ts +1 -6
- package/src/core/tools/ls.ts +5 -2
- package/src/core/tools/lsp/config.ts +2 -2
- package/src/core/tools/lsp/render.ts +33 -12
- package/src/core/tools/notebook.ts +1 -1
- package/src/core/tools/output.ts +1 -1
- package/src/core/tools/read.ts +15 -49
- package/src/core/tools/render-utils.ts +61 -24
- package/src/core/tools/renderers.ts +2 -0
- package/src/core/tools/schema-validation.test.ts +501 -0
- package/src/core/tools/task/agents.ts +6 -2
- package/src/core/tools/task/commands.ts +9 -3
- package/src/core/tools/task/discovery.ts +3 -2
- package/src/core/tools/task/render.ts +10 -7
- package/src/core/tools/todo-write.ts +256 -0
- package/src/core/tools/web-fetch.ts +4 -2
- package/src/core/tools/web-scrapers/choosealicense.ts +2 -2
- package/src/core/tools/web-search/render.ts +13 -10
- package/src/core/tools/write.ts +2 -2
- package/src/discovery/builtin.ts +4 -4
- package/src/discovery/cline.ts +4 -3
- package/src/discovery/codex.ts +3 -3
- package/src/discovery/cursor.ts +2 -2
- package/src/discovery/github.ts +3 -2
- package/src/discovery/helpers.test.ts +14 -10
- package/src/discovery/helpers.ts +2 -39
- package/src/discovery/windsurf.ts +3 -3
- package/src/modes/interactive/components/custom-editor.ts +4 -11
- package/src/modes/interactive/components/index.ts +2 -1
- package/src/modes/interactive/components/read-tool-group.ts +118 -0
- package/src/modes/interactive/components/todo-display.ts +112 -0
- package/src/modes/interactive/components/tool-execution.ts +18 -2
- package/src/modes/interactive/controllers/command-controller.ts +2 -2
- package/src/modes/interactive/controllers/event-controller.ts +91 -32
- package/src/modes/interactive/controllers/input-controller.ts +19 -13
- package/src/modes/interactive/interactive-mode.ts +103 -3
- package/src/modes/interactive/theme/theme.ts +4 -0
- package/src/modes/interactive/types.ts +14 -2
- package/src/modes/interactive/utils/ui-helpers.ts +55 -26
- package/src/prompts/system/system-prompt.md +177 -126
- package/src/prompts/tools/todo-write.md +187 -0
package/src/discovery/helpers.ts
CHANGED
|
@@ -5,11 +5,10 @@
|
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { join, resolve } from "node:path";
|
|
7
7
|
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
8
|
-
import { parse as parseYAML } from "yaml";
|
|
9
8
|
import { readDirEntries, readFile } from "../capability/fs";
|
|
10
9
|
import type { Skill, SkillFrontmatter } from "../capability/skill";
|
|
11
10
|
import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
|
|
12
|
-
import {
|
|
11
|
+
import { parseFrontmatter } from "../core/frontmatter";
|
|
13
12
|
|
|
14
13
|
const VALID_THINKING_LEVELS: readonly string[] = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
15
14
|
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
|
@@ -120,42 +119,6 @@ export function createSourceMeta(provider: string, path: string, level: "user" |
|
|
|
120
119
|
};
|
|
121
120
|
}
|
|
122
121
|
|
|
123
|
-
/**
|
|
124
|
-
* Strip YAML frontmatter from content.
|
|
125
|
-
* Returns { frontmatter, body, raw }
|
|
126
|
-
*/
|
|
127
|
-
export function parseFrontmatter(content: string): {
|
|
128
|
-
frontmatter: Record<string, unknown>;
|
|
129
|
-
body: string;
|
|
130
|
-
raw: string;
|
|
131
|
-
} {
|
|
132
|
-
const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
133
|
-
|
|
134
|
-
if (!normalized.startsWith("---")) {
|
|
135
|
-
return { frontmatter: {}, body: normalized, raw: "" };
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const endIndex = normalized.indexOf("\n---", 3);
|
|
139
|
-
if (endIndex === -1) {
|
|
140
|
-
return { frontmatter: {}, body: normalized, raw: "" };
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const raw = normalized.slice(4, endIndex);
|
|
144
|
-
const body = normalized.slice(endIndex + 4).trim();
|
|
145
|
-
|
|
146
|
-
try {
|
|
147
|
-
// Replace tabs with spaces for YAML compatibility, use failsafe mode for robustness
|
|
148
|
-
const frontmatter = parseYAML(raw.replaceAll("\t", " "), { compat: "failsafe" }) as Record<
|
|
149
|
-
string,
|
|
150
|
-
unknown
|
|
151
|
-
> | null;
|
|
152
|
-
return { frontmatter: frontmatter ?? {}, body, raw };
|
|
153
|
-
} catch (error) {
|
|
154
|
-
logger.warn("Failed to parse YAML frontmatter", { error: String(error) });
|
|
155
|
-
return { frontmatter: {}, body, raw };
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
122
|
/**
|
|
160
123
|
* Parse thinking level from frontmatter.
|
|
161
124
|
* Supports keys: thinkingLevel, thinking-level, thinking
|
|
@@ -272,7 +235,7 @@ export async function loadSkillsFromDir(
|
|
|
272
235
|
return { item: null as Skill | null, warning: null as string | null };
|
|
273
236
|
}
|
|
274
237
|
|
|
275
|
-
const { frontmatter, body } = parseFrontmatter(content);
|
|
238
|
+
const { frontmatter, body } = parseFrontmatter(content, { source: skillFile });
|
|
276
239
|
if (requireDescription && !frontmatter.description) {
|
|
277
240
|
return { item: null as Skill | null, warning: null as string | null };
|
|
278
241
|
}
|
|
@@ -16,13 +16,13 @@ import { registerProvider } from "../capability/index";
|
|
|
16
16
|
import { type MCPServer, mcpCapability } from "../capability/mcp";
|
|
17
17
|
import { type Rule, ruleCapability } from "../capability/rule";
|
|
18
18
|
import type { LoadContext, LoadResult } from "../capability/types";
|
|
19
|
+
import { parseFrontmatter } from "../core/frontmatter";
|
|
19
20
|
import {
|
|
20
21
|
createSourceMeta,
|
|
21
22
|
expandEnvVarsDeep,
|
|
22
23
|
getProjectPath,
|
|
23
24
|
getUserPath,
|
|
24
25
|
loadFilesFromDir,
|
|
25
|
-
parseFrontmatter,
|
|
26
26
|
parseJSON,
|
|
27
27
|
} from "./helpers";
|
|
28
28
|
|
|
@@ -105,7 +105,7 @@ async function loadRules(ctx: LoadContext): Promise<LoadResult<Rule>> {
|
|
|
105
105
|
if (userPath) {
|
|
106
106
|
const content = await readFile(userPath);
|
|
107
107
|
if (content) {
|
|
108
|
-
const { frontmatter, body } = parseFrontmatter(content);
|
|
108
|
+
const { frontmatter, body } = parseFrontmatter(content, { source: userPath });
|
|
109
109
|
|
|
110
110
|
// Validate and normalize globs
|
|
111
111
|
let globs: string[] | undefined;
|
|
@@ -134,7 +134,7 @@ async function loadRules(ctx: LoadContext): Promise<LoadResult<Rule>> {
|
|
|
134
134
|
const result = await loadFilesFromDir<Rule>(ctx, projectRulesDir, PROVIDER_ID, "project", {
|
|
135
135
|
extensions: ["md"],
|
|
136
136
|
transform: (name, content, path, source) => {
|
|
137
|
-
const { frontmatter, body } = parseFrontmatter(content);
|
|
137
|
+
const { frontmatter, body } = parseFrontmatter(content, { source: path });
|
|
138
138
|
const ruleName = name.replace(/\.md$/, "");
|
|
139
139
|
|
|
140
140
|
// Validate and normalize globs
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
isCtrlP,
|
|
10
10
|
isCtrlT,
|
|
11
11
|
isCtrlV,
|
|
12
|
-
isCtrlY,
|
|
13
12
|
isCtrlZ,
|
|
14
13
|
isEscape,
|
|
15
14
|
isShiftCtrlP,
|
|
@@ -36,7 +35,7 @@ export class CustomEditor extends Editor {
|
|
|
36
35
|
public onCtrlZ?: () => void;
|
|
37
36
|
public onQuestionMark?: () => void;
|
|
38
37
|
public onCapsLock?: () => void;
|
|
39
|
-
public
|
|
38
|
+
public onAltP?: () => void;
|
|
40
39
|
/** Called when Ctrl+V is pressed. Returns true if handled (image found), false to fall through to text paste. */
|
|
41
40
|
public onCtrlV?: () => Promise<boolean>;
|
|
42
41
|
/** Called when Alt+Up is pressed (dequeue keybinding). */
|
|
@@ -84,9 +83,9 @@ export class CustomEditor extends Editor {
|
|
|
84
83
|
return;
|
|
85
84
|
}
|
|
86
85
|
|
|
87
|
-
// Intercept
|
|
88
|
-
if (
|
|
89
|
-
this.
|
|
86
|
+
// Intercept Alt+P for quick model switching
|
|
87
|
+
if (matchesKey(data, "alt+p") && this.onAltP) {
|
|
88
|
+
this.onAltP();
|
|
90
89
|
return;
|
|
91
90
|
}
|
|
92
91
|
|
|
@@ -102,12 +101,6 @@ export class CustomEditor extends Editor {
|
|
|
102
101
|
return;
|
|
103
102
|
}
|
|
104
103
|
|
|
105
|
-
// Intercept Ctrl+Y for role-based model cycling
|
|
106
|
-
if (isCtrlY(data) && this.onCtrlY) {
|
|
107
|
-
this.onCtrlY();
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
104
|
// Intercept Ctrl+L for model selector
|
|
112
105
|
if (isCtrlL(data) && this.onCtrlL) {
|
|
113
106
|
this.onCtrlL();
|
|
@@ -19,6 +19,7 @@ export { LoginDialogComponent } from "./login-dialog";
|
|
|
19
19
|
export { ModelSelectorComponent } from "./model-selector";
|
|
20
20
|
export { OAuthSelectorComponent } from "./oauth-selector";
|
|
21
21
|
export { QueueModeSelectorComponent } from "./queue-mode-selector";
|
|
22
|
+
export { ReadToolGroupComponent } from "./read-tool-group";
|
|
22
23
|
export { SessionSelectorComponent } from "./session-selector";
|
|
23
24
|
export {
|
|
24
25
|
type SettingChangeHandler,
|
|
@@ -30,7 +31,7 @@ export { ShowImagesSelectorComponent } from "./show-images-selector";
|
|
|
30
31
|
export { StatusLineComponent } from "./status-line";
|
|
31
32
|
export { ThemeSelectorComponent } from "./theme-selector";
|
|
32
33
|
export { ThinkingSelectorComponent } from "./thinking-selector";
|
|
33
|
-
export { ToolExecutionComponent, type ToolExecutionOptions } from "./tool-execution";
|
|
34
|
+
export { ToolExecutionComponent, type ToolExecutionHandle, type ToolExecutionOptions } from "./tool-execution";
|
|
34
35
|
export { TreeSelectorComponent } from "./tree-selector";
|
|
35
36
|
export { TtsrNotificationComponent } from "./ttsr-notification";
|
|
36
37
|
export { UserMessageComponent } from "./user-message";
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { Component } from "@oh-my-pi/pi-tui";
|
|
2
|
+
import { Container, Text } from "@oh-my-pi/pi-tui";
|
|
3
|
+
import { shortenPath } from "../../../core/tools/render-utils";
|
|
4
|
+
import { theme } from "../theme/theme";
|
|
5
|
+
import type { ToolExecutionHandle } from "./tool-execution";
|
|
6
|
+
|
|
7
|
+
type ReadRenderArgs = {
|
|
8
|
+
path?: string;
|
|
9
|
+
file_path?: string;
|
|
10
|
+
offset?: number;
|
|
11
|
+
limit?: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type ReadEntry = {
|
|
15
|
+
toolCallId: string;
|
|
16
|
+
path: string;
|
|
17
|
+
offset?: number;
|
|
18
|
+
limit?: number;
|
|
19
|
+
status: "pending" | "success" | "error";
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export class ReadToolGroupComponent extends Container implements ToolExecutionHandle {
|
|
23
|
+
private entries = new Map<string, ReadEntry>();
|
|
24
|
+
private text: Text;
|
|
25
|
+
|
|
26
|
+
constructor() {
|
|
27
|
+
super();
|
|
28
|
+
this.text = new Text("", 0, 0);
|
|
29
|
+
this.addChild(this.text);
|
|
30
|
+
this.updateDisplay();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
updateArgs(args: ReadRenderArgs, toolCallId?: string): void {
|
|
34
|
+
if (!toolCallId) return;
|
|
35
|
+
const rawPath = args.file_path || args.path || "";
|
|
36
|
+
const entry: ReadEntry = this.entries.get(toolCallId) ?? {
|
|
37
|
+
toolCallId,
|
|
38
|
+
path: rawPath,
|
|
39
|
+
offset: args.offset,
|
|
40
|
+
limit: args.limit,
|
|
41
|
+
status: "pending",
|
|
42
|
+
};
|
|
43
|
+
entry.path = rawPath;
|
|
44
|
+
entry.offset = args.offset;
|
|
45
|
+
entry.limit = args.limit;
|
|
46
|
+
this.entries.set(toolCallId, entry);
|
|
47
|
+
this.updateDisplay();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
updateResult(
|
|
51
|
+
result: { content: Array<{ type: string; text?: string }>; details?: unknown; isError?: boolean },
|
|
52
|
+
isPartial = false,
|
|
53
|
+
toolCallId?: string,
|
|
54
|
+
): void {
|
|
55
|
+
if (!toolCallId) return;
|
|
56
|
+
const entry = this.entries.get(toolCallId);
|
|
57
|
+
if (!entry) return;
|
|
58
|
+
if (isPartial) return;
|
|
59
|
+
entry.status = result.isError ? "error" : "success";
|
|
60
|
+
this.updateDisplay();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
setArgsComplete(_toolCallId?: string): void {
|
|
64
|
+
this.updateDisplay();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
setExpanded(_expanded: boolean): void {
|
|
68
|
+
this.updateDisplay();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
getComponent(): Component {
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private updateDisplay(): void {
|
|
76
|
+
const entries = [...this.entries.values()];
|
|
77
|
+
const header = `${theme.fg("toolTitle", theme.bold("Read"))}${
|
|
78
|
+
entries.length > 1 ? theme.fg("dim", ` (${entries.length})`) : ""
|
|
79
|
+
}`;
|
|
80
|
+
|
|
81
|
+
if (entries.length === 0) {
|
|
82
|
+
this.text.setText(` ${theme.format.bullet} ${header}`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const lines = [` ${theme.format.bullet} ${header}`];
|
|
87
|
+
const total = entries.length;
|
|
88
|
+
for (const [index, entry] of entries.entries()) {
|
|
89
|
+
const connector = index === total - 1 ? theme.tree.last : theme.tree.branch;
|
|
90
|
+
const statusSymbol = this.formatStatus(entry.status);
|
|
91
|
+
const pathDisplay = this.formatPath(entry);
|
|
92
|
+
lines.push(` ${theme.fg("dim", connector)} ${statusSymbol} ${pathDisplay}`.trimEnd());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.text.setText(lines.join("\n"));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private formatPath(entry: ReadEntry): string {
|
|
99
|
+
const filePath = shortenPath(entry.path);
|
|
100
|
+
let pathDisplay = filePath ? theme.fg("accent", filePath) : theme.fg("toolOutput", theme.format.ellipsis);
|
|
101
|
+
if (entry.offset !== undefined || entry.limit !== undefined) {
|
|
102
|
+
const startLine = entry.offset ?? 1;
|
|
103
|
+
const endLine = entry.limit !== undefined ? startLine + entry.limit - 1 : "";
|
|
104
|
+
pathDisplay += theme.fg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
|
|
105
|
+
}
|
|
106
|
+
return pathDisplay;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private formatStatus(status: ReadEntry["status"]): string {
|
|
110
|
+
if (status === "success") {
|
|
111
|
+
return theme.fg("success", theme.status.success);
|
|
112
|
+
}
|
|
113
|
+
if (status === "error") {
|
|
114
|
+
return theme.fg("error", theme.status.error);
|
|
115
|
+
}
|
|
116
|
+
return theme.fg("dim", theme.status.pending);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { Text } from "@oh-my-pi/pi-tui";
|
|
3
|
+
import { logger } from "../../../core/logger";
|
|
4
|
+
import { getArtifactsDir } from "../../../core/tools/task/artifacts";
|
|
5
|
+
import { theme } from "../theme/theme";
|
|
6
|
+
import type { TodoItem } from "../types";
|
|
7
|
+
|
|
8
|
+
const TODO_FILE_NAME = "todos.json";
|
|
9
|
+
|
|
10
|
+
interface TodoFile {
|
|
11
|
+
updatedAt: number;
|
|
12
|
+
todos: TodoItem[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function loadTodoFile(filePath: string): Promise<TodoFile | null> {
|
|
16
|
+
const file = Bun.file(filePath);
|
|
17
|
+
if (!(await file.exists())) return null;
|
|
18
|
+
try {
|
|
19
|
+
const text = await file.text();
|
|
20
|
+
const data = JSON.parse(text) as TodoFile;
|
|
21
|
+
if (!data || !Array.isArray(data.todos)) return null;
|
|
22
|
+
return data;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
logger.warn("Failed to read todo file", { path: filePath, error: String(error) });
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class TodoDisplayComponent {
|
|
30
|
+
public todos: TodoItem[] = [];
|
|
31
|
+
private expanded = false;
|
|
32
|
+
private visible = false;
|
|
33
|
+
|
|
34
|
+
constructor(private readonly sessionFile: string | null) {}
|
|
35
|
+
|
|
36
|
+
async loadTodos(): Promise<void> {
|
|
37
|
+
if (!this.sessionFile) {
|
|
38
|
+
this.todos = [];
|
|
39
|
+
this.visible = false;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const artifactsDir = getArtifactsDir(this.sessionFile);
|
|
44
|
+
if (!artifactsDir) {
|
|
45
|
+
this.todos = [];
|
|
46
|
+
this.visible = false;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const todoPath = path.join(artifactsDir, TODO_FILE_NAME);
|
|
51
|
+
const data = await loadTodoFile(todoPath);
|
|
52
|
+
this.todos = data?.todos ?? [];
|
|
53
|
+
this.visible = this.todos.length > 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
setTodos(todos: TodoItem[]): void {
|
|
57
|
+
this.todos = todos;
|
|
58
|
+
this.visible = this.todos.length > 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
setExpanded(expanded: boolean): void {
|
|
62
|
+
this.expanded = expanded;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
isVisible(): boolean {
|
|
66
|
+
return this.visible;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
render(_width: number): string[] {
|
|
70
|
+
if (!this.visible || this.todos.length === 0) {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const lines: string[] = [];
|
|
75
|
+
const maxItems = this.expanded ? this.todos.length : Math.min(5, this.todos.length);
|
|
76
|
+
const hasMore = !this.expanded && this.todos.length > 5;
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < maxItems; i++) {
|
|
79
|
+
const todo = this.todos[i];
|
|
80
|
+
const prefix = i === 0 ? ` ${theme.tree.hook} ` : " ";
|
|
81
|
+
|
|
82
|
+
let checkbox: string;
|
|
83
|
+
let text: string;
|
|
84
|
+
|
|
85
|
+
if (todo.status === "completed") {
|
|
86
|
+
checkbox = theme.checkbox.checked;
|
|
87
|
+
text = theme.fg("success", `${prefix}${checkbox} ${theme.strikethrough(todo.content)}`);
|
|
88
|
+
} else if (todo.status === "in_progress") {
|
|
89
|
+
checkbox = theme.checkbox.unchecked;
|
|
90
|
+
const displayText = todo.activeForm !== todo.content ? todo.activeForm : todo.content;
|
|
91
|
+
text = theme.fg("accent", `${prefix}${checkbox} ${displayText}`);
|
|
92
|
+
} else {
|
|
93
|
+
checkbox = theme.checkbox.unchecked;
|
|
94
|
+
text = theme.fg("dim", `${prefix}${checkbox} ${todo.content}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
lines.push(text);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (hasMore) {
|
|
101
|
+
lines.push(theme.fg("dim", ` ${theme.tree.hook} +${this.todos.length - 5} more (Ctrl+T to expand)`));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return lines;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
getRenderedComponent(): Text | null {
|
|
108
|
+
if (!this.visible) return null;
|
|
109
|
+
const lines = this.render(80);
|
|
110
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -86,6 +86,21 @@ export interface ToolExecutionOptions {
|
|
|
86
86
|
showImages?: boolean; // default: true (only used if terminal supports images)
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
export interface ToolExecutionHandle {
|
|
90
|
+
updateArgs(args: any, toolCallId?: string): void;
|
|
91
|
+
updateResult(
|
|
92
|
+
result: {
|
|
93
|
+
content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
|
|
94
|
+
details?: any;
|
|
95
|
+
isError?: boolean;
|
|
96
|
+
},
|
|
97
|
+
isPartial?: boolean,
|
|
98
|
+
toolCallId?: string,
|
|
99
|
+
): void;
|
|
100
|
+
setArgsComplete(toolCallId?: string): void;
|
|
101
|
+
setExpanded(expanded: boolean): void;
|
|
102
|
+
}
|
|
103
|
+
|
|
89
104
|
/**
|
|
90
105
|
* Component that renders a tool call with its result (updateable)
|
|
91
106
|
*/
|
|
@@ -152,7 +167,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
152
167
|
this.updateDisplay();
|
|
153
168
|
}
|
|
154
169
|
|
|
155
|
-
updateArgs(args: any): void {
|
|
170
|
+
updateArgs(args: any, _toolCallId?: string): void {
|
|
156
171
|
this.args = args;
|
|
157
172
|
this.updateDisplay();
|
|
158
173
|
}
|
|
@@ -161,7 +176,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
161
176
|
* Signal that args are complete (tool is about to execute).
|
|
162
177
|
* This triggers diff computation for edit tool.
|
|
163
178
|
*/
|
|
164
|
-
setArgsComplete(): void {
|
|
179
|
+
setArgsComplete(_toolCallId?: string): void {
|
|
165
180
|
this.maybeComputeEditDiff();
|
|
166
181
|
}
|
|
167
182
|
|
|
@@ -206,6 +221,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
206
221
|
isError?: boolean;
|
|
207
222
|
},
|
|
208
223
|
isPartial = false,
|
|
224
|
+
_toolCallId?: string,
|
|
209
225
|
): void {
|
|
210
226
|
this.result = result;
|
|
211
227
|
this.isPartial = isPartial;
|
|
@@ -334,11 +334,11 @@ export class CommandController {
|
|
|
334
334
|
| \`Shift+Tab\` | Cycle thinking level |
|
|
335
335
|
| \`Ctrl+P\` | Cycle role models (slow/default/smol) |
|
|
336
336
|
| \`Shift+Ctrl+P\` | Cycle role models (temporary) |
|
|
337
|
-
| \`
|
|
337
|
+
| \`Alt+P\` | Select model (temporary) |
|
|
338
338
|
| \`Ctrl+L\` | Select model (set roles) |
|
|
339
339
|
| \`Ctrl+R\` | Search prompt history |
|
|
340
340
|
| \`Ctrl+O\` | Toggle tool output expansion |
|
|
341
|
-
| \`Ctrl+T\` | Toggle
|
|
341
|
+
| \`Ctrl+T\` | Toggle todo list expansion |
|
|
342
342
|
| \`Ctrl+G\` | Edit message in external editor |
|
|
343
343
|
| \`/\` | Slash commands |
|
|
344
344
|
| \`!\` | Run bash command |
|
|
@@ -2,14 +2,33 @@ import { Loader, Text } from "@oh-my-pi/pi-tui";
|
|
|
2
2
|
import type { AgentSessionEvent } from "../../../core/agent-session";
|
|
3
3
|
import { detectNotificationProtocol, isNotificationSuppressed, sendNotification } from "../../../core/terminal-notify";
|
|
4
4
|
import { AssistantMessageComponent } from "../components/assistant-message";
|
|
5
|
+
import { ReadToolGroupComponent } from "../components/read-tool-group";
|
|
5
6
|
import { ToolExecutionComponent } from "../components/tool-execution";
|
|
6
7
|
import { TtsrNotificationComponent } from "../components/ttsr-notification";
|
|
7
8
|
import { getSymbolTheme, theme } from "../theme/theme";
|
|
8
|
-
import type { InteractiveModeContext } from "../types";
|
|
9
|
+
import type { InteractiveModeContext, TodoItem } from "../types";
|
|
9
10
|
|
|
10
11
|
export class EventController {
|
|
12
|
+
private lastReadGroup: ReadToolGroupComponent | undefined = undefined;
|
|
13
|
+
private lastThinkingCount = 0;
|
|
14
|
+
|
|
11
15
|
constructor(private ctx: InteractiveModeContext) {}
|
|
12
16
|
|
|
17
|
+
private resetReadGroup(): void {
|
|
18
|
+
this.lastReadGroup = undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private getReadGroup(): ReadToolGroupComponent {
|
|
22
|
+
if (!this.lastReadGroup) {
|
|
23
|
+
this.ctx.chatContainer.addChild(new Text("", 0, 0));
|
|
24
|
+
const group = new ReadToolGroupComponent();
|
|
25
|
+
group.setExpanded(this.ctx.toolOutputExpanded);
|
|
26
|
+
this.ctx.chatContainer.addChild(group);
|
|
27
|
+
this.lastReadGroup = group;
|
|
28
|
+
}
|
|
29
|
+
return this.lastReadGroup;
|
|
30
|
+
}
|
|
31
|
+
|
|
13
32
|
subscribeToAgent(): void {
|
|
14
33
|
this.ctx.unsubscribe = this.ctx.session.subscribe(async (event: AgentSessionEvent) => {
|
|
15
34
|
await this.handleEvent(event);
|
|
@@ -53,17 +72,21 @@ export class EventController {
|
|
|
53
72
|
|
|
54
73
|
case "message_start":
|
|
55
74
|
if (event.message.role === "hookMessage" || event.message.role === "custom") {
|
|
75
|
+
this.resetReadGroup();
|
|
56
76
|
this.ctx.addMessageToChat(event.message);
|
|
57
77
|
this.ctx.ui.requestRender();
|
|
58
78
|
} else if (event.message.role === "user") {
|
|
79
|
+
this.resetReadGroup();
|
|
59
80
|
this.ctx.addMessageToChat(event.message);
|
|
60
81
|
this.ctx.editor.setText("");
|
|
61
82
|
this.ctx.updatePendingMessagesDisplay();
|
|
62
83
|
this.ctx.ui.requestRender();
|
|
63
84
|
} else if (event.message.role === "fileMention") {
|
|
85
|
+
this.resetReadGroup();
|
|
64
86
|
this.ctx.addMessageToChat(event.message);
|
|
65
87
|
this.ctx.ui.requestRender();
|
|
66
88
|
} else if (event.message.role === "assistant") {
|
|
89
|
+
this.lastThinkingCount = 0;
|
|
67
90
|
this.ctx.streamingComponent = new AssistantMessageComponent(undefined, this.ctx.hideThinkingBlock);
|
|
68
91
|
this.ctx.streamingMessage = event.message;
|
|
69
92
|
this.ctx.chatContainer.addChild(this.ctx.streamingComponent);
|
|
@@ -77,29 +100,45 @@ export class EventController {
|
|
|
77
100
|
this.ctx.streamingMessage = event.message;
|
|
78
101
|
this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage);
|
|
79
102
|
|
|
103
|
+
const thinkingCount = this.ctx.streamingMessage.content.filter(
|
|
104
|
+
(content) => content.type === "thinking" && content.thinking.trim(),
|
|
105
|
+
).length;
|
|
106
|
+
if (thinkingCount > this.lastThinkingCount) {
|
|
107
|
+
this.resetReadGroup();
|
|
108
|
+
this.lastThinkingCount = thinkingCount;
|
|
109
|
+
}
|
|
110
|
+
|
|
80
111
|
for (const content of this.ctx.streamingMessage.content) {
|
|
81
|
-
if (content.type
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
112
|
+
if (content.type !== "toolCall") continue;
|
|
113
|
+
|
|
114
|
+
if (!this.ctx.pendingTools.has(content.id)) {
|
|
115
|
+
if (content.name === "read") {
|
|
116
|
+
const group = this.getReadGroup();
|
|
117
|
+
group.updateArgs(content.arguments, content.id);
|
|
118
|
+
this.ctx.pendingTools.set(content.id, group);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this.resetReadGroup();
|
|
123
|
+
this.ctx.chatContainer.addChild(new Text("", 0, 0));
|
|
124
|
+
const tool = this.ctx.session.getToolByName(content.name);
|
|
125
|
+
const component = new ToolExecutionComponent(
|
|
126
|
+
content.name,
|
|
127
|
+
content.arguments,
|
|
128
|
+
{
|
|
129
|
+
showImages: this.ctx.settingsManager.getShowImages(),
|
|
130
|
+
},
|
|
131
|
+
tool,
|
|
132
|
+
this.ctx.ui,
|
|
133
|
+
this.ctx.sessionManager.getCwd(),
|
|
134
|
+
);
|
|
135
|
+
component.setExpanded(this.ctx.toolOutputExpanded);
|
|
136
|
+
this.ctx.chatContainer.addChild(component);
|
|
137
|
+
this.ctx.pendingTools.set(content.id, component);
|
|
138
|
+
} else {
|
|
139
|
+
const component = this.ctx.pendingTools.get(content.id);
|
|
140
|
+
if (component) {
|
|
141
|
+
component.updateArgs(content.arguments, content.id);
|
|
103
142
|
}
|
|
104
143
|
}
|
|
105
144
|
}
|
|
@@ -133,17 +172,21 @@ export class EventController {
|
|
|
133
172
|
} else {
|
|
134
173
|
errorMessage = this.ctx.streamingMessage.errorMessage || "Error";
|
|
135
174
|
}
|
|
136
|
-
for (const [, component] of this.ctx.pendingTools.entries()) {
|
|
137
|
-
component.updateResult(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
175
|
+
for (const [toolCallId, component] of this.ctx.pendingTools.entries()) {
|
|
176
|
+
component.updateResult(
|
|
177
|
+
{
|
|
178
|
+
content: [{ type: "text", text: errorMessage }],
|
|
179
|
+
isError: true,
|
|
180
|
+
},
|
|
181
|
+
false,
|
|
182
|
+
toolCallId,
|
|
183
|
+
);
|
|
141
184
|
}
|
|
142
185
|
}
|
|
143
186
|
this.ctx.pendingTools.clear();
|
|
144
187
|
} else {
|
|
145
|
-
for (const [, component] of this.ctx.pendingTools.entries()) {
|
|
146
|
-
component.setArgsComplete();
|
|
188
|
+
for (const [toolCallId, component] of this.ctx.pendingTools.entries()) {
|
|
189
|
+
component.setArgsComplete(toolCallId);
|
|
147
190
|
}
|
|
148
191
|
}
|
|
149
192
|
this.ctx.streamingComponent = undefined;
|
|
@@ -156,6 +199,15 @@ export class EventController {
|
|
|
156
199
|
|
|
157
200
|
case "tool_execution_start": {
|
|
158
201
|
if (!this.ctx.pendingTools.has(event.toolCallId)) {
|
|
202
|
+
if (event.toolName === "read") {
|
|
203
|
+
const group = this.getReadGroup();
|
|
204
|
+
group.updateArgs(event.args, event.toolCallId);
|
|
205
|
+
this.ctx.pendingTools.set(event.toolCallId, group);
|
|
206
|
+
this.ctx.ui.requestRender();
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
this.resetReadGroup();
|
|
159
211
|
const tool = this.ctx.session.getToolByName(event.toolName);
|
|
160
212
|
const component = new ToolExecutionComponent(
|
|
161
213
|
event.toolName,
|
|
@@ -178,7 +230,7 @@ export class EventController {
|
|
|
178
230
|
case "tool_execution_update": {
|
|
179
231
|
const component = this.ctx.pendingTools.get(event.toolCallId);
|
|
180
232
|
if (component) {
|
|
181
|
-
component.updateResult({ ...event.partialResult, isError: false }, true);
|
|
233
|
+
component.updateResult({ ...event.partialResult, isError: false }, true, event.toolCallId);
|
|
182
234
|
this.ctx.ui.requestRender();
|
|
183
235
|
}
|
|
184
236
|
break;
|
|
@@ -187,10 +239,17 @@ export class EventController {
|
|
|
187
239
|
case "tool_execution_end": {
|
|
188
240
|
const component = this.ctx.pendingTools.get(event.toolCallId);
|
|
189
241
|
if (component) {
|
|
190
|
-
component.updateResult({ ...event.result, isError: event.isError });
|
|
242
|
+
component.updateResult({ ...event.result, isError: event.isError }, false, event.toolCallId);
|
|
191
243
|
this.ctx.pendingTools.delete(event.toolCallId);
|
|
192
244
|
this.ctx.ui.requestRender();
|
|
193
245
|
}
|
|
246
|
+
// Update todo display when todo_write tool completes
|
|
247
|
+
if (event.toolName === "todo_write" && !event.isError) {
|
|
248
|
+
const details = event.result.details as { todos?: TodoItem[] } | undefined;
|
|
249
|
+
if (details?.todos) {
|
|
250
|
+
this.ctx.setTodos(details.todos);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
194
253
|
break;
|
|
195
254
|
}
|
|
196
255
|
|