@oh-my-pi/pi-coding-agent 11.2.3 → 11.3.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 +100 -0
- package/examples/extensions/plan-mode.ts +1 -1
- package/examples/hooks/qna.ts +1 -1
- package/examples/hooks/status-line.ts +1 -1
- package/examples/sdk/11-sessions.ts +1 -1
- package/package.json +8 -8
- package/src/cli/args.ts +9 -6
- package/src/cli/update-cli.ts +2 -2
- package/src/commands/index/index.ts +2 -5
- package/src/commit/agentic/agent.ts +1 -1
- package/src/commit/changelog/index.ts +2 -2
- package/src/config/keybindings.ts +16 -1
- package/src/config/model-registry.ts +25 -20
- package/src/config/model-resolver.ts +8 -8
- package/src/config/resolve-config-value.ts +92 -0
- package/src/config/settings-schema.ts +9 -0
- package/src/config.ts +14 -1
- package/src/export/html/template.css +7 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +33 -16
- package/src/extensibility/custom-commands/bundled/review/index.ts +1 -1
- package/src/extensibility/extensions/index.ts +18 -0
- package/src/extensibility/extensions/loader.ts +15 -0
- package/src/extensibility/extensions/runner.ts +78 -1
- package/src/extensibility/extensions/types.ts +131 -5
- package/src/extensibility/extensions/wrapper.ts +1 -1
- package/src/extensibility/plugins/git-url.ts +270 -0
- package/src/extensibility/plugins/index.ts +2 -0
- package/src/extensibility/slash-commands.ts +45 -0
- package/src/index.ts +7 -0
- package/src/lsp/render.ts +50 -43
- package/src/lsp/utils.ts +2 -2
- package/src/main.ts +11 -10
- package/src/mcp/transports/stdio.ts +3 -5
- package/src/modes/components/custom-message.ts +0 -8
- package/src/modes/components/diff.ts +1 -7
- package/src/modes/components/footer.ts +4 -4
- package/src/modes/components/model-selector.ts +4 -0
- package/src/modes/components/todo-display.ts +13 -3
- package/src/modes/components/tool-execution.ts +30 -16
- package/src/modes/components/tree-selector.ts +50 -19
- package/src/modes/controllers/event-controller.ts +1 -0
- package/src/modes/controllers/extension-ui-controller.ts +34 -2
- package/src/modes/controllers/input-controller.ts +47 -33
- package/src/modes/controllers/selector-controller.ts +10 -15
- package/src/modes/interactive-mode.ts +50 -38
- package/src/modes/print-mode.ts +6 -0
- package/src/modes/rpc/rpc-client.ts +4 -4
- package/src/modes/rpc/rpc-mode.ts +17 -2
- package/src/modes/rpc/rpc-types.ts +2 -2
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +3 -1
- package/src/patch/applicator.ts +2 -3
- package/src/patch/fuzzy.ts +1 -1
- package/src/patch/shared.ts +74 -61
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/tools/task.md +6 -0
- package/src/sdk.ts +15 -11
- package/src/session/agent-session.ts +72 -23
- package/src/session/auth-storage.ts +2 -1
- package/src/session/blob-store.ts +105 -0
- package/src/session/session-manager.ts +107 -44
- package/src/task/executor.ts +19 -9
- package/src/task/render.ts +80 -58
- package/src/tools/ask.ts +28 -5
- package/src/tools/bash.ts +47 -39
- package/src/tools/browser.ts +248 -26
- package/src/tools/calculator.ts +42 -23
- package/src/tools/fetch.ts +33 -16
- package/src/tools/find.ts +57 -22
- package/src/tools/grep.ts +54 -25
- package/src/tools/index.ts +5 -5
- package/src/tools/notebook.ts +19 -6
- package/src/tools/path-utils.ts +26 -1
- package/src/tools/python.ts +20 -14
- package/src/tools/read.ts +21 -8
- package/src/tools/render-utils.ts +5 -45
- package/src/tools/ssh.ts +59 -53
- package/src/tools/submit-result.ts +2 -2
- package/src/tools/todo-write.ts +32 -14
- package/src/tools/truncate.ts +1 -1
- package/src/tools/write.ts +39 -24
- package/src/tui/output-block.ts +61 -3
- package/src/tui/tree-list.ts +4 -4
- package/src/tui/utils.ts +71 -1
- package/src/utils/frontmatter.ts +1 -1
- package/src/utils/title-generator.ts +1 -1
- package/src/utils/tools-manager.ts +18 -2
- package/src/web/scrapers/osv.ts +4 -1
- package/src/web/scrapers/youtube.ts +1 -1
- package/src/web/search/index.ts +1 -1
- package/src/web/search/render.ts +96 -90
|
@@ -1,3 +1,48 @@
|
|
|
1
|
+
export type SlashCommandSource = "extension" | "prompt" | "skill";
|
|
2
|
+
|
|
3
|
+
export type SlashCommandLocation = "user" | "project" | "path";
|
|
4
|
+
|
|
5
|
+
export interface SlashCommandInfo {
|
|
6
|
+
name: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
source: SlashCommandSource;
|
|
9
|
+
location?: SlashCommandLocation;
|
|
10
|
+
path?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface BuiltinSlashCommand {
|
|
14
|
+
name: string;
|
|
15
|
+
description: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [
|
|
19
|
+
{ name: "settings", description: "Open settings menu" },
|
|
20
|
+
{ name: "plan", description: "Toggle plan mode (agent plans before executing)" },
|
|
21
|
+
{ name: "model", description: "Select model (opens selector UI)" },
|
|
22
|
+
{ name: "export", description: "Export session to HTML file" },
|
|
23
|
+
{ name: "dump", description: "Copy session transcript to clipboard" },
|
|
24
|
+
{ name: "share", description: "Share session as a secret GitHub gist" },
|
|
25
|
+
{ name: "browser", description: "Toggle browser headless vs visible mode" },
|
|
26
|
+
{ name: "copy", description: "Copy last agent message to clipboard" },
|
|
27
|
+
{ name: "session", description: "Show session info and stats" },
|
|
28
|
+
{ name: "usage", description: "Show provider usage and limits" },
|
|
29
|
+
{ name: "changelog", description: "Show changelog entries" },
|
|
30
|
+
{ name: "hotkeys", description: "Show all keyboard shortcuts" },
|
|
31
|
+
{ name: "extensions", description: "Open Extension Control Center dashboard" },
|
|
32
|
+
{ name: "branch", description: "Create a new branch from a previous message" },
|
|
33
|
+
{ name: "fork", description: "Create a new fork from a previous message" },
|
|
34
|
+
{ name: "tree", description: "Navigate session tree (switch branches)" },
|
|
35
|
+
{ name: "login", description: "Login with OAuth provider" },
|
|
36
|
+
{ name: "logout", description: "Logout from OAuth provider" },
|
|
37
|
+
{ name: "new", description: "Start a new session" },
|
|
38
|
+
{ name: "compact", description: "Manually compact the session context" },
|
|
39
|
+
{ name: "handoff", description: "Hand off session context to a new session" },
|
|
40
|
+
{ name: "resume", description: "Resume a different session" },
|
|
41
|
+
{ name: "background", description: "Detach UI and continue running in background" },
|
|
42
|
+
{ name: "debug", description: "Write debug log (TUI state and messages)" },
|
|
43
|
+
{ name: "exit", description: "Exit the application" },
|
|
44
|
+
];
|
|
45
|
+
|
|
1
46
|
import { slashCommandCapability } from "../capability/slash-command";
|
|
2
47
|
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
3
48
|
import type { SlashCommand } from "../discovery";
|
package/src/index.ts
CHANGED
|
@@ -62,6 +62,7 @@ export type {
|
|
|
62
62
|
MessageRenderer,
|
|
63
63
|
MessageRenderOptions,
|
|
64
64
|
RegisteredCommand,
|
|
65
|
+
ToolCallEvent,
|
|
65
66
|
ToolResultEvent,
|
|
66
67
|
TurnEndEvent,
|
|
67
68
|
TurnStartEvent,
|
|
@@ -222,6 +223,7 @@ export {
|
|
|
222
223
|
type CustomMessageEntry,
|
|
223
224
|
type FileEntry,
|
|
224
225
|
getLatestCompactionEntry,
|
|
226
|
+
type ModeChangeEntry,
|
|
225
227
|
type ModelChangeEntry,
|
|
226
228
|
migrateSessionEntries,
|
|
227
229
|
type NewSessionOptions,
|
|
@@ -238,22 +240,27 @@ export {
|
|
|
238
240
|
// Tools (detail types and utilities)
|
|
239
241
|
export {
|
|
240
242
|
type BashToolDetails,
|
|
243
|
+
type BashToolInput,
|
|
241
244
|
type BrowserToolDetails,
|
|
242
245
|
DEFAULT_MAX_BYTES,
|
|
243
246
|
DEFAULT_MAX_LINES,
|
|
244
247
|
type FindOperations,
|
|
245
248
|
type FindToolDetails,
|
|
249
|
+
type FindToolInput,
|
|
246
250
|
type FindToolOptions,
|
|
247
251
|
formatSize,
|
|
248
252
|
type GrepOperations,
|
|
249
253
|
type GrepToolDetails,
|
|
254
|
+
type GrepToolInput,
|
|
250
255
|
type GrepToolOptions,
|
|
251
256
|
type PythonToolDetails,
|
|
252
257
|
type ReadToolDetails,
|
|
258
|
+
type ReadToolInput,
|
|
253
259
|
type TruncationOptions,
|
|
254
260
|
type TruncationResult,
|
|
255
261
|
truncateHead,
|
|
256
262
|
truncateLine,
|
|
257
263
|
truncateTail,
|
|
258
264
|
type WriteToolDetails,
|
|
265
|
+
type WriteToolInput,
|
|
259
266
|
} from "./tools";
|
package/src/lsp/render.ts
CHANGED
|
@@ -19,7 +19,8 @@ import {
|
|
|
19
19
|
TRUNCATE_LENGTHS,
|
|
20
20
|
truncateToWidth,
|
|
21
21
|
} from "../tools/render-utils";
|
|
22
|
-
import {
|
|
22
|
+
import { renderStatusLine } from "../tui";
|
|
23
|
+
import { CachedOutputBlock } from "../tui/output-block";
|
|
23
24
|
import type { LspParams, LspToolDetails } from "./types";
|
|
24
25
|
|
|
25
26
|
// =============================================================================
|
|
@@ -114,43 +115,16 @@ export function renderResult(
|
|
|
114
115
|
|
|
115
116
|
const text = content.text;
|
|
116
117
|
const lines = text.split("\n");
|
|
117
|
-
const expanded = options.expanded;
|
|
118
|
-
|
|
119
|
-
let label = "Result";
|
|
120
|
-
let state: "success" | "warning" | "error" = "success";
|
|
121
|
-
let bodyLines: string[] = [];
|
|
122
118
|
|
|
119
|
+
// Static type detection (result content doesn't change between renders)
|
|
123
120
|
const codeBlockMatch = text.match(/```(\w*)\n([\s\S]*?)```/);
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const warningMatch = text.match(/(\d+)\s+warning\(s\)/);
|
|
130
|
-
if (errorMatch || warningMatch || text.includes(theme.status.error)) {
|
|
131
|
-
label = "Diagnostics";
|
|
132
|
-
const errorCount = errorMatch ? Number.parseInt(errorMatch[1], 10) : 0;
|
|
133
|
-
const warnCount = warningMatch ? Number.parseInt(warningMatch[1], 10) : 0;
|
|
134
|
-
state = errorCount > 0 ? "error" : warnCount > 0 ? "warning" : "success";
|
|
135
|
-
bodyLines = renderDiagnostics(errorMatch, warningMatch, lines, expanded, theme);
|
|
136
|
-
} else {
|
|
137
|
-
const refMatch = text.match(/(\d+)\s+reference\(s\)/);
|
|
138
|
-
if (refMatch) {
|
|
139
|
-
label = "References";
|
|
140
|
-
bodyLines = renderReferences(refMatch, lines, expanded, theme);
|
|
141
|
-
} else {
|
|
142
|
-
const symbolsMatch = text.match(/Symbols in (.+):/);
|
|
143
|
-
if (symbolsMatch) {
|
|
144
|
-
label = "Symbols";
|
|
145
|
-
bodyLines = renderSymbols(symbolsMatch, lines, expanded, theme);
|
|
146
|
-
} else {
|
|
147
|
-
label = "Response";
|
|
148
|
-
bodyLines = renderGeneric(text, lines, expanded, theme);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
121
|
+
const errorMatch = text.match(/(\d+)\s+error\(s\)/);
|
|
122
|
+
const warningMatch = text.match(/(\d+)\s+warning\(s\)/);
|
|
123
|
+
const refMatch = text.match(/(\d+)\s+reference\(s\)/);
|
|
124
|
+
const symbolsMatch = text.match(/Symbols in (.+):/);
|
|
125
|
+
const hasStatusError = text.includes(theme.status.error);
|
|
153
126
|
|
|
127
|
+
// Static request info
|
|
154
128
|
const request = args ?? result.details?.request;
|
|
155
129
|
const requestLines: string[] = [];
|
|
156
130
|
if (request?.file) {
|
|
@@ -175,14 +149,44 @@ export function renderResult(
|
|
|
175
149
|
requestLines.push(theme.fg("dim", `include declaration: ${request.include_declaration ? "true" : "false"}`));
|
|
176
150
|
}
|
|
177
151
|
|
|
178
|
-
const
|
|
179
|
-
const status = options.isPartial ? "running" : result.isError ? "error" : "success";
|
|
180
|
-
const icon = formatStatusIcon(status, theme, options.spinnerFrame);
|
|
181
|
-
const header = `${icon} LSP ${actionLabel}`;
|
|
152
|
+
const outputBlock = new CachedOutputBlock();
|
|
182
153
|
|
|
183
154
|
return {
|
|
184
|
-
render
|
|
185
|
-
|
|
155
|
+
render(width: number): string[] {
|
|
156
|
+
// Read mutable state at render time
|
|
157
|
+
const { expanded, isPartial, spinnerFrame } = options;
|
|
158
|
+
|
|
159
|
+
// Determine label, state, bodyLines based on type + current expanded
|
|
160
|
+
let label = "Result";
|
|
161
|
+
let state: "success" | "warning" | "error" = "success";
|
|
162
|
+
let bodyLines: string[] = [];
|
|
163
|
+
|
|
164
|
+
if (codeBlockMatch) {
|
|
165
|
+
label = "Hover";
|
|
166
|
+
bodyLines = renderHover(codeBlockMatch, text, lines, expanded, theme);
|
|
167
|
+
} else if (errorMatch || warningMatch || hasStatusError) {
|
|
168
|
+
label = "Diagnostics";
|
|
169
|
+
const errorCount = errorMatch ? Number.parseInt(errorMatch[1], 10) : 0;
|
|
170
|
+
const warnCount = warningMatch ? Number.parseInt(warningMatch[1], 10) : 0;
|
|
171
|
+
state = errorCount > 0 ? "error" : warnCount > 0 ? "warning" : "success";
|
|
172
|
+
bodyLines = renderDiagnostics(errorMatch, warningMatch, lines, expanded, theme);
|
|
173
|
+
} else if (refMatch) {
|
|
174
|
+
label = "References";
|
|
175
|
+
bodyLines = renderReferences(refMatch, lines, expanded, theme);
|
|
176
|
+
} else if (symbolsMatch) {
|
|
177
|
+
label = "Symbols";
|
|
178
|
+
bodyLines = renderSymbols(symbolsMatch, lines, expanded, theme);
|
|
179
|
+
} else {
|
|
180
|
+
label = "Response";
|
|
181
|
+
bodyLines = renderGeneric(text, lines, expanded, theme);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const actionLabel = (request?.action ?? result.details?.action ?? label.toLowerCase()).replace(/_/g, " ");
|
|
185
|
+
const status = isPartial ? "running" : result.isError ? "error" : "success";
|
|
186
|
+
const icon = formatStatusIcon(status, theme, spinnerFrame);
|
|
187
|
+
const header = `${icon} LSP ${actionLabel}`;
|
|
188
|
+
|
|
189
|
+
return outputBlock.render(
|
|
186
190
|
{
|
|
187
191
|
header,
|
|
188
192
|
state,
|
|
@@ -194,8 +198,11 @@ export function renderResult(
|
|
|
194
198
|
applyBg: false,
|
|
195
199
|
},
|
|
196
200
|
theme,
|
|
197
|
-
)
|
|
198
|
-
|
|
201
|
+
);
|
|
202
|
+
},
|
|
203
|
+
invalidate() {
|
|
204
|
+
outputBlock.invalidate();
|
|
205
|
+
},
|
|
199
206
|
};
|
|
200
207
|
}
|
|
201
208
|
|
package/src/lsp/utils.ts
CHANGED
|
@@ -371,7 +371,7 @@ export function formatTextEdit(edit: TextEdit, maxLength = 50): string {
|
|
|
371
371
|
const range = `${edit.range.start.line + 1}:${edit.range.start.character + 1}`;
|
|
372
372
|
const preview =
|
|
373
373
|
edit.newText.length > maxLength
|
|
374
|
-
? `${edit.newText.slice(0, maxLength).replace(/\n/g, "\\n")}
|
|
374
|
+
? `${edit.newText.slice(0, maxLength).replace(/\n/g, "\\n")}…`
|
|
375
375
|
: edit.newText.replace(/\n/g, "\\n");
|
|
376
376
|
return `line ${range} ${theme.nav.cursor} "${preview}"`;
|
|
377
377
|
}
|
|
@@ -530,7 +530,7 @@ export function extractHoverText(
|
|
|
530
530
|
*/
|
|
531
531
|
export function truncate(str: string, maxLength: number): string {
|
|
532
532
|
if (str.length <= maxLength) return str;
|
|
533
|
-
return `${str.slice(0, maxLength -
|
|
533
|
+
return `${str.slice(0, maxLength - 1)}…`;
|
|
534
534
|
}
|
|
535
535
|
|
|
536
536
|
/**
|
package/src/main.ts
CHANGED
|
@@ -235,8 +235,8 @@ async function createSessionManager(parsed: Args, cwd: string): Promise<SessionM
|
|
|
235
235
|
if (parsed.noSession) {
|
|
236
236
|
return SessionManager.inMemory();
|
|
237
237
|
}
|
|
238
|
-
if (parsed.
|
|
239
|
-
const sessionArg = parsed.
|
|
238
|
+
if (typeof parsed.resume === "string") {
|
|
239
|
+
const sessionArg = parsed.resume;
|
|
240
240
|
if (sessionArg.includes("/") || sessionArg.includes("\\") || sessionArg.endsWith(".jsonl")) {
|
|
241
241
|
return await SessionManager.open(sessionArg, parsed.sessionDir);
|
|
242
242
|
}
|
|
@@ -258,7 +258,7 @@ async function createSessionManager(parsed: Args, cwd: string): Promise<SessionM
|
|
|
258
258
|
if (parsed.continue) {
|
|
259
259
|
return await SessionManager.continueRecent(cwd, parsed.sessionDir);
|
|
260
260
|
}
|
|
261
|
-
// --resume is handled separately (needs picker UI)
|
|
261
|
+
// --resume without value is handled separately (needs picker UI)
|
|
262
262
|
// If --session-dir provided without --continue/--resume, create new session there
|
|
263
263
|
if (parsed.sessionDir) {
|
|
264
264
|
return SessionManager.create(cwd, parsed.sessionDir);
|
|
@@ -495,26 +495,27 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
495
495
|
|
|
496
496
|
if (parsedArgs.version) {
|
|
497
497
|
writeStdout(VERSION);
|
|
498
|
-
|
|
498
|
+
process.exit(0);
|
|
499
499
|
}
|
|
500
500
|
|
|
501
501
|
if (parsedArgs.listModels !== undefined) {
|
|
502
502
|
const searchPattern = typeof parsedArgs.listModels === "string" ? parsedArgs.listModels : undefined;
|
|
503
503
|
await listModels(modelRegistry, searchPattern);
|
|
504
|
-
|
|
504
|
+
process.exit(0);
|
|
505
505
|
}
|
|
506
506
|
|
|
507
507
|
if (parsedArgs.export) {
|
|
508
|
+
let result: string;
|
|
508
509
|
try {
|
|
509
510
|
const outputPath = parsedArgs.messages.length > 0 ? parsedArgs.messages[0] : undefined;
|
|
510
|
-
|
|
511
|
-
writeStdout(`Exported to: ${result}`);
|
|
512
|
-
return;
|
|
511
|
+
result = await exportFromFile(parsedArgs.export, outputPath);
|
|
513
512
|
} catch (error: unknown) {
|
|
514
513
|
const message = error instanceof Error ? error.message : "Failed to export session";
|
|
515
514
|
writeStderr(chalk.red(`Error: ${message}`));
|
|
516
515
|
process.exit(1);
|
|
517
516
|
}
|
|
517
|
+
writeStdout(`Exported to: ${result}`);
|
|
518
|
+
process.exit(0);
|
|
518
519
|
}
|
|
519
520
|
|
|
520
521
|
if (parsedArgs.mode === "rpc" && parsedArgs.fileArgs.length > 0) {
|
|
@@ -571,8 +572,8 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
571
572
|
debugStartup("main:createSessionManager");
|
|
572
573
|
time("createSessionManager");
|
|
573
574
|
|
|
574
|
-
// Handle --resume: show session picker
|
|
575
|
-
if (parsedArgs.resume) {
|
|
575
|
+
// Handle --resume (no value): show session picker
|
|
576
|
+
if (parsedArgs.resume === true) {
|
|
576
577
|
const sessions = await SessionManager.list(cwd, parsedArgs.sessionDir);
|
|
577
578
|
time("SessionManager.list");
|
|
578
579
|
if (sessions.length === 0) {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Messages are newline-delimited JSON.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { readJsonl } from "@oh-my-pi/pi-utils";
|
|
9
9
|
import { type Subprocess, spawn } from "bun";
|
|
10
10
|
import type { JsonRpcResponse, MCPStdioServerConfig, MCPTransport } from "../../mcp/types";
|
|
11
11
|
|
|
@@ -72,13 +72,11 @@ export class StdioTransport implements MCPTransport {
|
|
|
72
72
|
|
|
73
73
|
private async startReadLoop(): Promise<void> {
|
|
74
74
|
if (!this.process?.stdout) return;
|
|
75
|
-
|
|
76
|
-
const decoder = new TextDecoder();
|
|
77
75
|
try {
|
|
78
|
-
for await (const line of
|
|
76
|
+
for await (const line of readJsonl(this.process.stdout)) {
|
|
79
77
|
if (!this._connected) break;
|
|
80
78
|
try {
|
|
81
|
-
this.handleMessage(
|
|
79
|
+
this.handleMessage(line as JsonRpcResponse);
|
|
82
80
|
} catch {
|
|
83
81
|
// Skip malformed lines
|
|
84
82
|
}
|
|
@@ -82,14 +82,6 @@ export class CustomMessageComponent extends Container {
|
|
|
82
82
|
.join("\n");
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
// Limit lines when collapsed
|
|
86
|
-
if (!this._expanded) {
|
|
87
|
-
const lines = text.split("\n");
|
|
88
|
-
if (lines.length > 5) {
|
|
89
|
-
text = `${lines.slice(0, 5).join("\n")}\n…`;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
85
|
this.box.addChild(
|
|
94
86
|
new Markdown(text, 0, 0, getMarkdownTheme(), {
|
|
95
87
|
color: (value: string) => theme.fg("customMessageText", value),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as Diff from "diff";
|
|
2
2
|
import { theme } from "../../modes/theme/theme";
|
|
3
|
+
import { replaceTabs } from "../../tools/render-utils";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Parse diff line to extract prefix, line number, and content.
|
|
@@ -11,13 +12,6 @@ function parseDiffLine(line: string): { prefix: string; lineNum: string; content
|
|
|
11
12
|
return { prefix: match[1], lineNum: match[2] ?? "", content: match[3] };
|
|
12
13
|
}
|
|
13
14
|
|
|
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
15
|
/**
|
|
22
16
|
* Compute word-level diff and render with inverse on changed parts.
|
|
23
17
|
* Uses diffWords which groups whitespace with adjacent words for cleaner highlighting.
|
|
@@ -211,11 +211,11 @@ export class FooterComponent implements Component {
|
|
|
211
211
|
|
|
212
212
|
// Truncate path if too long to fit width
|
|
213
213
|
if (pwd.length > width) {
|
|
214
|
-
const half = Math.floor(width / 2) -
|
|
215
|
-
if (half >
|
|
214
|
+
const half = Math.floor(width / 2) - 1;
|
|
215
|
+
if (half > 1) {
|
|
216
216
|
const start = pwd.slice(0, half);
|
|
217
217
|
const end = pwd.slice(-(half - 1));
|
|
218
|
-
pwd = `${start}
|
|
218
|
+
pwd = `${start}…${end}`;
|
|
219
219
|
} else {
|
|
220
220
|
pwd = pwd.slice(0, Math.max(1, width));
|
|
221
221
|
}
|
|
@@ -269,7 +269,7 @@ export class FooterComponent implements Component {
|
|
|
269
269
|
if (statsLeftWidth > width) {
|
|
270
270
|
// Truncate statsLeft to fit width (no room for right side)
|
|
271
271
|
const plainStatsLeft = statsLeft.replace(/\x1b\[[0-9;]*m/g, "");
|
|
272
|
-
statsLeft = `${plainStatsLeft.substring(0, width -
|
|
272
|
+
statsLeft = `${plainStatsLeft.substring(0, width - 1)}…`;
|
|
273
273
|
statsLeftWidth = visibleWidth(statsLeft);
|
|
274
274
|
}
|
|
275
275
|
|
|
@@ -398,6 +398,10 @@ export class ModelSelectorComponent extends Container {
|
|
|
398
398
|
}
|
|
399
399
|
} else if (this.filteredModels.length === 0) {
|
|
400
400
|
this.listContainer.addChild(new Text(theme.fg("muted", " No matching models"), 0, 0));
|
|
401
|
+
} else {
|
|
402
|
+
const selected = this.filteredModels[this.selectedIndex];
|
|
403
|
+
this.listContainer.addChild(new Spacer(1));
|
|
404
|
+
this.listContainer.addChild(new Text(theme.fg("muted", ` Model Name: ${selected.model.name}`), 0, 0));
|
|
401
405
|
}
|
|
402
406
|
}
|
|
403
407
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import { Text } from "@oh-my-pi/pi-tui";
|
|
2
|
+
import { Ellipsis, Text, truncateToWidth } from "@oh-my-pi/pi-tui";
|
|
3
3
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { theme } from "../../modes/theme/theme";
|
|
5
5
|
import type { TodoItem } from "../../modes/types";
|
|
6
|
+
import { Hasher, type RenderCache } from "../../tui";
|
|
6
7
|
|
|
7
8
|
const TODO_FILE_NAME = "todos.json";
|
|
8
9
|
|
|
@@ -29,6 +30,7 @@ export class TodoDisplayComponent {
|
|
|
29
30
|
public todos: TodoItem[] = [];
|
|
30
31
|
private expanded = false;
|
|
31
32
|
private visible = false;
|
|
33
|
+
private cached: RenderCache | undefined;
|
|
32
34
|
|
|
33
35
|
constructor(private readonly sessionFile: string | null) {}
|
|
34
36
|
|
|
@@ -44,26 +46,32 @@ export class TodoDisplayComponent {
|
|
|
44
46
|
const data = await loadTodoFile(todoPath);
|
|
45
47
|
this.todos = data?.todos ?? [];
|
|
46
48
|
this.visible = this.todos.length > 0;
|
|
49
|
+
this.cached = undefined;
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
setTodos(todos: TodoItem[]): void {
|
|
50
53
|
this.todos = todos;
|
|
51
54
|
this.visible = this.todos.length > 0;
|
|
55
|
+
this.cached = undefined;
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
setExpanded(expanded: boolean): void {
|
|
55
59
|
this.expanded = expanded;
|
|
60
|
+
this.cached = undefined;
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
isVisible(): boolean {
|
|
59
64
|
return this.visible;
|
|
60
65
|
}
|
|
61
66
|
|
|
62
|
-
render(
|
|
67
|
+
render(width: number): string[] {
|
|
63
68
|
if (!this.visible || this.todos.length === 0) {
|
|
64
69
|
return [];
|
|
65
70
|
}
|
|
66
71
|
|
|
72
|
+
const key = new Hasher().bool(this.expanded).u32(width).digest();
|
|
73
|
+
if (this.cached?.key === key) return this.cached.lines;
|
|
74
|
+
|
|
67
75
|
const lines: string[] = [];
|
|
68
76
|
const maxItems = this.expanded ? this.todos.length : Math.min(5, this.todos.length);
|
|
69
77
|
const hasMore = !this.expanded && this.todos.length > 5;
|
|
@@ -93,7 +101,9 @@ export class TodoDisplayComponent {
|
|
|
93
101
|
lines.push(theme.fg("dim", ` ${theme.tree.hook} +${this.todos.length - 5} more (Ctrl+T to expand)`));
|
|
94
102
|
}
|
|
95
103
|
|
|
96
|
-
|
|
104
|
+
const result = lines.map(l => truncateToWidth(l, width, Ellipsis.Omit));
|
|
105
|
+
this.cached = { key, lines: result };
|
|
106
|
+
return result;
|
|
97
107
|
}
|
|
98
108
|
|
|
99
109
|
getRenderedComponent(): Text | null {
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
Text,
|
|
13
13
|
type TUI,
|
|
14
14
|
} from "@oh-my-pi/pi-tui";
|
|
15
|
-
import { sanitizeText } from "@oh-my-pi/pi-utils";
|
|
15
|
+
import { logger, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
16
16
|
import type { Theme } from "../../modes/theme/theme";
|
|
17
17
|
import { theme } from "../../modes/theme/theme";
|
|
18
18
|
import { computeEditDiff, computePatchDiff, type EditDiffError, type EditDiffResult } from "../../patch";
|
|
@@ -89,6 +89,16 @@ export class ToolExecutionComponent extends Container {
|
|
|
89
89
|
private spinnerInterval: ReturnType<typeof setInterval> | null = null;
|
|
90
90
|
// Track if args are still being streamed (for edit/write spinner)
|
|
91
91
|
private argsComplete = false;
|
|
92
|
+
private renderState: {
|
|
93
|
+
spinnerFrame: number;
|
|
94
|
+
expanded: boolean;
|
|
95
|
+
isPartial: boolean;
|
|
96
|
+
renderContext?: Record<string, unknown>;
|
|
97
|
+
} = {
|
|
98
|
+
spinnerFrame: 0,
|
|
99
|
+
expanded: false,
|
|
100
|
+
isPartial: true,
|
|
101
|
+
};
|
|
92
102
|
|
|
93
103
|
constructor(
|
|
94
104
|
toolName: string,
|
|
@@ -280,8 +290,9 @@ export class ToolExecutionComponent extends Container {
|
|
|
280
290
|
const frameCount = theme.spinnerFrames.length;
|
|
281
291
|
if (frameCount === 0) return;
|
|
282
292
|
this.spinnerFrame = (this.spinnerFrame + 1) % frameCount;
|
|
283
|
-
this.
|
|
293
|
+
this.renderState.spinnerFrame = this.spinnerFrame;
|
|
284
294
|
this.ui.requestRender();
|
|
295
|
+
// NO updateDisplay() — existing component closures read from renderState
|
|
285
296
|
}, 80);
|
|
286
297
|
} else if (!needsSpinner && this.spinnerInterval) {
|
|
287
298
|
clearInterval(this.spinnerInterval);
|
|
@@ -322,6 +333,11 @@ export class ToolExecutionComponent extends Container {
|
|
|
322
333
|
? (text: string) => theme.bg("toolErrorBg", text)
|
|
323
334
|
: (text: string) => theme.bg("toolSuccessBg", text);
|
|
324
335
|
|
|
336
|
+
// Sync shared mutable render state for component closures
|
|
337
|
+
this.renderState.expanded = this.expanded;
|
|
338
|
+
this.renderState.isPartial = this.isPartial;
|
|
339
|
+
this.renderState.spinnerFrame = this.spinnerFrame;
|
|
340
|
+
|
|
325
341
|
// Check for custom tool rendering
|
|
326
342
|
if (this.tool && (this.tool.renderCall || this.tool.renderResult)) {
|
|
327
343
|
const tool = this.tool;
|
|
@@ -344,7 +360,8 @@ export class ToolExecutionComponent extends Container {
|
|
|
344
360
|
}
|
|
345
361
|
this.contentBox.addChild(component);
|
|
346
362
|
}
|
|
347
|
-
} catch {
|
|
363
|
+
} catch (err) {
|
|
364
|
+
logger.warn("Tool renderer failed", { tool: this.toolName, error: String(err) });
|
|
348
365
|
// Fall back to default on error
|
|
349
366
|
this.contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.toolLabel)), 0, 0));
|
|
350
367
|
}
|
|
@@ -364,7 +381,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
364
381
|
) => Component;
|
|
365
382
|
const resultComponent = renderResult(
|
|
366
383
|
{ content: this.result.content as any, details: this.result.details, isError: this.result.isError },
|
|
367
|
-
|
|
384
|
+
this.renderState,
|
|
368
385
|
theme,
|
|
369
386
|
this.args,
|
|
370
387
|
);
|
|
@@ -376,7 +393,8 @@ export class ToolExecutionComponent extends Container {
|
|
|
376
393
|
}
|
|
377
394
|
this.contentBox.addChild(component);
|
|
378
395
|
}
|
|
379
|
-
} catch {
|
|
396
|
+
} catch (err) {
|
|
397
|
+
logger.warn("Tool renderer failed", { tool: this.toolName, error: String(err) });
|
|
380
398
|
// Fall back to showing raw output on error
|
|
381
399
|
const output = this.getTextOutput();
|
|
382
400
|
if (output) {
|
|
@@ -401,9 +419,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
401
419
|
if (shouldRenderCall) {
|
|
402
420
|
// Render call component
|
|
403
421
|
try {
|
|
404
|
-
const callComponent = renderer.renderCall(this.args, theme,
|
|
405
|
-
spinnerFrame: this.spinnerFrame,
|
|
406
|
-
});
|
|
422
|
+
const callComponent = renderer.renderCall(this.args, theme, this.renderState);
|
|
407
423
|
if (callComponent) {
|
|
408
424
|
// Ensure component has invalidate() method for Component interface
|
|
409
425
|
const component = callComponent as any;
|
|
@@ -412,7 +428,8 @@ export class ToolExecutionComponent extends Container {
|
|
|
412
428
|
}
|
|
413
429
|
this.contentBox.addChild(component);
|
|
414
430
|
}
|
|
415
|
-
} catch {
|
|
431
|
+
} catch (err) {
|
|
432
|
+
logger.warn("Tool renderer failed", { tool: this.toolName, error: String(err) });
|
|
416
433
|
// Fall back to default on error
|
|
417
434
|
this.contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.toolLabel)), 0, 0));
|
|
418
435
|
}
|
|
@@ -423,15 +440,11 @@ export class ToolExecutionComponent extends Container {
|
|
|
423
440
|
try {
|
|
424
441
|
// Build render context for tools that need extra state
|
|
425
442
|
const renderContext = this.buildRenderContext();
|
|
443
|
+
this.renderState.renderContext = renderContext;
|
|
426
444
|
|
|
427
445
|
const resultComponent = renderer.renderResult(
|
|
428
446
|
{ content: this.result.content as any, details: this.result.details, isError: this.result.isError },
|
|
429
|
-
|
|
430
|
-
expanded: this.expanded,
|
|
431
|
-
isPartial: this.isPartial,
|
|
432
|
-
spinnerFrame: this.spinnerFrame,
|
|
433
|
-
renderContext,
|
|
434
|
-
},
|
|
447
|
+
this.renderState,
|
|
435
448
|
theme,
|
|
436
449
|
this.args, // Pass args for tools that need them
|
|
437
450
|
);
|
|
@@ -443,7 +456,8 @@ export class ToolExecutionComponent extends Container {
|
|
|
443
456
|
}
|
|
444
457
|
this.contentBox.addChild(component);
|
|
445
458
|
}
|
|
446
|
-
} catch {
|
|
459
|
+
} catch (err) {
|
|
460
|
+
logger.warn("Tool renderer failed", { tool: this.toolName, error: String(err) });
|
|
447
461
|
// Fall back to showing raw output on error
|
|
448
462
|
const output = this.getTextOutput();
|
|
449
463
|
if (output) {
|