@oh-my-pi/pi-coding-agent 3.31.0 → 3.33.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 +29 -0
- package/README.md +12 -0
- package/package.json +4 -4
- package/src/core/agent-session.ts +2 -1
- package/src/core/custom-commands/bundled/review/index.ts +5 -7
- package/src/core/file-mentions.ts +147 -5
- package/src/core/model-resolver.ts +12 -8
- package/src/core/sdk.ts +10 -2
- package/src/core/session-manager.ts +2 -1
- package/src/core/settings-manager.ts +70 -0
- package/src/core/slash-commands.ts +39 -13
- package/src/core/title-generator.ts +108 -54
- package/src/core/tools/index.ts +7 -1
- package/src/core/tools/lsp/client.ts +26 -10
- package/src/core/tools/lsp/index.ts +17 -3
- package/src/core/tools/output.ts +36 -1
- package/src/core/tools/task/commands.ts +4 -0
- package/src/core/tools/task/index.ts +3 -2
- package/src/core/tools/task/render.ts +10 -16
- package/src/main.ts +3 -2
- package/src/modes/interactive/components/bash-execution.ts +9 -10
- package/src/modes/interactive/components/tree-selector.ts +9 -12
- package/src/modes/interactive/interactive-mode.ts +4 -26
- package/src/modes/interactive/theme/theme.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [3.33.0] - 2026-01-08
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Added `env` support in `settings.json` for automatically setting environment variables on startup
|
|
9
|
+
- Added environment variable management methods to SettingsManager (get/set/clear)
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- Fixed bash output previews to recompute on resize, preventing TUI line width overflow crashes
|
|
13
|
+
- Fixed session title generation to retry alternate smol models when the primary model errors or is rate-limited
|
|
14
|
+
- Fixed file mentions to resolve extensionless paths and directories, using read tool truncation limits for injected content
|
|
15
|
+
- Fixed interactive UI to show auto-read file mention indicators
|
|
16
|
+
- Fixed task tool tree rendering to use consistent tree connectors for progress, findings, and results
|
|
17
|
+
- Fixed last-branch tree connector symbol in the TUI
|
|
18
|
+
- Fixed output tool previews to use compact JSON when outputs are formatted with leading braces
|
|
19
|
+
|
|
20
|
+
## [3.32.0] - 2026-01-08
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- Added progress indicator when starting LSP servers at session startup
|
|
24
|
+
- Added bundled `/init` slash command available by default
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Changed LSP server warmup to use a 5-second timeout, falling back to lazy initialization for slow servers
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- Fixed Task tool subagent model selection to inherit explicit CLI `--model` overrides
|
|
33
|
+
|
|
5
34
|
## [3.31.0] - 2026-01-08
|
|
6
35
|
|
|
7
36
|
### Added
|
package/README.md
CHANGED
|
@@ -338,10 +338,22 @@ When disabled, neither case triggers automatic compaction (use `/compact` manual
|
|
|
338
338
|
"enabled": true,
|
|
339
339
|
"reserveTokens": 16384,
|
|
340
340
|
"keepRecentTokens": 20000
|
|
341
|
+
},
|
|
342
|
+
"env": {
|
|
343
|
+
"ANTHROPIC_API_KEY": "sk-ant-...",
|
|
344
|
+
"OPENAI_API_KEY": "sk-proj-...",
|
|
345
|
+
"GEMINI_API_KEY": "AIzaSyD...",
|
|
346
|
+
"CUSTOM_VAR": "custom-value"
|
|
341
347
|
}
|
|
342
348
|
}
|
|
343
349
|
```
|
|
344
350
|
|
|
351
|
+
**Environment Variables (`env`):**
|
|
352
|
+
- Automatically sets environment variables when the application starts
|
|
353
|
+
- Only sets variables that aren't already present in `process.env`
|
|
354
|
+
- Supports any environment variable, not just API keys
|
|
355
|
+
- Order of precedence: existing env vars > settings.json env vars > auth.json env vars
|
|
356
|
+
|
|
345
357
|
> **Note:** Compaction is lossy. The agent loses full conversation access afterward. Size tasks to avoid context limits when possible. For critical context, ask the agent to write a summary to a file, iterate on it until it covers everything, then start a new session with that file. The full session history is preserved in the JSONL file; use `/tree` to revisit any previous point.
|
|
346
358
|
|
|
347
359
|
See [docs/compaction.md](docs/compaction.md) for how compaction works internally and how to customize it via hooks.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.33.0",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -40,9 +40,9 @@
|
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@mariozechner/pi-ai": "^0.37.8",
|
|
43
|
-
"@oh-my-pi/pi-agent-core": "3.
|
|
44
|
-
"@oh-my-pi/pi-git-tool": "3.
|
|
45
|
-
"@oh-my-pi/pi-tui": "3.
|
|
43
|
+
"@oh-my-pi/pi-agent-core": "3.33.0",
|
|
44
|
+
"@oh-my-pi/pi-git-tool": "3.33.0",
|
|
45
|
+
"@oh-my-pi/pi-tui": "3.33.0",
|
|
46
46
|
"@openai/agents": "^0.3.7",
|
|
47
47
|
"@sinclair/typebox": "^0.34.46",
|
|
48
48
|
"ajv": "^8.17.1",
|
|
@@ -377,7 +377,8 @@ export class AgentSession {
|
|
|
377
377
|
} else if (
|
|
378
378
|
event.message.role === "user" ||
|
|
379
379
|
event.message.role === "assistant" ||
|
|
380
|
-
event.message.role === "toolResult"
|
|
380
|
+
event.message.role === "toolResult" ||
|
|
381
|
+
event.message.role === "fileMention"
|
|
381
382
|
) {
|
|
382
383
|
// Regular LLM message - persist as SessionMessageEntry
|
|
383
384
|
this.sessionManager.appendMessage(event.message);
|
|
@@ -435,13 +435,11 @@ export function createReviewCommand(api: CustomCommandAPI): CustomCommand {
|
|
|
435
435
|
if (hasDiff) {
|
|
436
436
|
const stats = parseDiff(diffResult.stdout);
|
|
437
437
|
// Even if all files filtered, include the custom instructions
|
|
438
|
-
return (
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
) + `\n\n### Additional Instructions\n\n${instructions}`
|
|
444
|
-
);
|
|
438
|
+
return `${buildReviewPrompt(
|
|
439
|
+
`Custom review: ${instructions.split("\n")[0].slice(0, 60)}...`,
|
|
440
|
+
stats,
|
|
441
|
+
diffResult.stdout,
|
|
442
|
+
)}\n\n### Additional Instructions\n\n${instructions}`;
|
|
445
443
|
}
|
|
446
444
|
|
|
447
445
|
// No diff available, just pass instructions
|
|
@@ -9,14 +9,148 @@
|
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
11
11
|
import type { FileMentionMessage } from "./messages";
|
|
12
|
+
import { resolveReadPath } from "./tools/path-utils";
|
|
13
|
+
import { formatAge } from "./tools/render-utils";
|
|
14
|
+
import { DEFAULT_MAX_BYTES, formatSize, truncateHead, truncateStringToBytesFromStart } from "./tools/truncate";
|
|
12
15
|
|
|
13
16
|
/** Regex to match @filepath patterns in text */
|
|
14
|
-
const FILE_MENTION_REGEX = /@(
|
|
17
|
+
const FILE_MENTION_REGEX = /@([^\s@]+)/g;
|
|
18
|
+
const LEADING_PUNCTUATION_REGEX = /^[`"'([{<]+/;
|
|
19
|
+
const TRAILING_PUNCTUATION_REGEX = /[)\]}>.,;:!?"'`]+$/;
|
|
20
|
+
const MENTION_BOUNDARY_REGEX = /[\s([{<"'`]/;
|
|
21
|
+
const DEFAULT_DIR_LIMIT = 500;
|
|
22
|
+
|
|
23
|
+
function isMentionBoundary(text: string, index: number): boolean {
|
|
24
|
+
if (index === 0) return true;
|
|
25
|
+
return MENTION_BOUNDARY_REGEX.test(text[index - 1]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sanitizeMentionPath(rawPath: string): string | null {
|
|
29
|
+
let cleaned = rawPath.trim();
|
|
30
|
+
cleaned = cleaned.replace(LEADING_PUNCTUATION_REGEX, "");
|
|
31
|
+
cleaned = cleaned.replace(TRAILING_PUNCTUATION_REGEX, "");
|
|
32
|
+
cleaned = cleaned.trim();
|
|
33
|
+
return cleaned.length > 0 ? cleaned : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildTextOutput(textContent: string): { output: string; lineCount: number } {
|
|
37
|
+
const allLines = textContent.split("\n");
|
|
38
|
+
const totalFileLines = allLines.length;
|
|
39
|
+
const truncation = truncateHead(textContent);
|
|
40
|
+
|
|
41
|
+
if (truncation.firstLineExceedsLimit) {
|
|
42
|
+
const firstLine = allLines[0] ?? "";
|
|
43
|
+
const firstLineBytes = Buffer.byteLength(firstLine, "utf-8");
|
|
44
|
+
const snippet = truncateStringToBytesFromStart(firstLine, DEFAULT_MAX_BYTES);
|
|
45
|
+
let outputText = snippet.text;
|
|
46
|
+
|
|
47
|
+
if (outputText.length > 0) {
|
|
48
|
+
outputText += `\n\n[Line 1 is ${formatSize(firstLineBytes)}, exceeds ${formatSize(
|
|
49
|
+
DEFAULT_MAX_BYTES,
|
|
50
|
+
)} limit. Showing first ${formatSize(snippet.bytes)} of the line.]`;
|
|
51
|
+
} else {
|
|
52
|
+
outputText = `[Line 1 is ${formatSize(firstLineBytes)}, exceeds ${formatSize(
|
|
53
|
+
DEFAULT_MAX_BYTES,
|
|
54
|
+
)} limit. Unable to display a valid UTF-8 snippet.]`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { output: outputText, lineCount: totalFileLines };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let outputText = truncation.content;
|
|
61
|
+
|
|
62
|
+
if (truncation.truncated) {
|
|
63
|
+
const endLineDisplay = truncation.outputLines;
|
|
64
|
+
const nextOffset = endLineDisplay + 1;
|
|
65
|
+
|
|
66
|
+
if (truncation.truncatedBy === "lines") {
|
|
67
|
+
outputText += `\n\n[Showing lines 1-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue]`;
|
|
68
|
+
} else {
|
|
69
|
+
outputText += `\n\n[Showing lines 1-${endLineDisplay} of ${totalFileLines} (${formatSize(
|
|
70
|
+
DEFAULT_MAX_BYTES,
|
|
71
|
+
)} limit). Use offset=${nextOffset} to continue]`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { output: outputText, lineCount: totalFileLines };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function buildDirectoryListing(absolutePath: string): Promise<{ output: string; lineCount: number }> {
|
|
79
|
+
let entries: string[];
|
|
80
|
+
try {
|
|
81
|
+
entries = await Array.fromAsync(new Bun.Glob("*").scan({ cwd: absolutePath, dot: true, onlyFiles: false }));
|
|
82
|
+
} catch {
|
|
83
|
+
return { output: "(empty directory)", lineCount: 1 };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
entries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
87
|
+
|
|
88
|
+
const results: string[] = [];
|
|
89
|
+
let entryLimitReached = false;
|
|
90
|
+
|
|
91
|
+
for (const entry of entries) {
|
|
92
|
+
if (results.length >= DEFAULT_DIR_LIMIT) {
|
|
93
|
+
entryLimitReached = true;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const fullPath = path.join(absolutePath, entry);
|
|
98
|
+
let suffix = "";
|
|
99
|
+
let age = "";
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const stat = await Bun.file(fullPath).stat();
|
|
103
|
+
if (stat.isDirectory()) {
|
|
104
|
+
suffix = "/";
|
|
105
|
+
}
|
|
106
|
+
const ageSeconds = Math.floor((Date.now() - stat.mtimeMs) / 1000);
|
|
107
|
+
age = formatAge(ageSeconds);
|
|
108
|
+
} catch {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const line = age ? `${entry}${suffix} (${age})` : `${entry}${suffix}`;
|
|
113
|
+
results.push(line);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (results.length === 0) {
|
|
117
|
+
return { output: "(empty directory)", lineCount: 1 };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const rawOutput = results.join("\n");
|
|
121
|
+
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
122
|
+
let output = truncation.content;
|
|
123
|
+
|
|
124
|
+
const notices: string[] = [];
|
|
125
|
+
if (entryLimitReached) {
|
|
126
|
+
notices.push(`${DEFAULT_DIR_LIMIT} entries limit reached. Use limit=${DEFAULT_DIR_LIMIT * 2} for more`);
|
|
127
|
+
}
|
|
128
|
+
if (truncation.truncated) {
|
|
129
|
+
notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
|
|
130
|
+
}
|
|
131
|
+
if (notices.length > 0) {
|
|
132
|
+
output += `\n\n[${notices.join(". ")}]`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { output, lineCount: output.split("\n").length };
|
|
136
|
+
}
|
|
15
137
|
|
|
16
138
|
/** Extract all @filepath mentions from text */
|
|
17
139
|
export function extractFileMentions(text: string): string[] {
|
|
18
140
|
const matches = [...text.matchAll(FILE_MENTION_REGEX)];
|
|
19
|
-
|
|
141
|
+
const mentions: string[] = [];
|
|
142
|
+
|
|
143
|
+
for (const match of matches) {
|
|
144
|
+
const index = match.index ?? 0;
|
|
145
|
+
if (!isMentionBoundary(text, index)) continue;
|
|
146
|
+
|
|
147
|
+
const cleaned = sanitizeMentionPath(match[1]);
|
|
148
|
+
if (!cleaned) continue;
|
|
149
|
+
|
|
150
|
+
mentions.push(cleaned);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return [...new Set(mentions)];
|
|
20
154
|
}
|
|
21
155
|
|
|
22
156
|
/**
|
|
@@ -29,11 +163,19 @@ export async function generateFileMentionMessages(filePaths: string[], cwd: stri
|
|
|
29
163
|
const files: FileMentionMessage["files"] = [];
|
|
30
164
|
|
|
31
165
|
for (const filePath of filePaths) {
|
|
166
|
+
const absolutePath = resolveReadPath(filePath, cwd);
|
|
167
|
+
|
|
32
168
|
try {
|
|
33
|
-
const
|
|
169
|
+
const stat = await Bun.file(absolutePath).stat();
|
|
170
|
+
if (stat.isDirectory()) {
|
|
171
|
+
const { output, lineCount } = await buildDirectoryListing(absolutePath);
|
|
172
|
+
files.push({ path: filePath, content: output, lineCount });
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
34
176
|
const content = await Bun.file(absolutePath).text();
|
|
35
|
-
const lineCount = content
|
|
36
|
-
files.push({ path: filePath, content, lineCount });
|
|
177
|
+
const { output, lineCount } = buildTextOutput(content);
|
|
178
|
+
files.push({ path: filePath, content: output, lineCount });
|
|
37
179
|
} catch {
|
|
38
180
|
// File doesn't exist or isn't readable - skip silently
|
|
39
181
|
}
|
|
@@ -33,7 +33,7 @@ export interface ScopedModel {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/** Priority chain for auto-discovering smol/fast models */
|
|
36
|
-
export const SMOL_MODEL_PRIORITY = ["claude-haiku-4-5", "haiku", "flash", "mini"];
|
|
36
|
+
export const SMOL_MODEL_PRIORITY = ["cerebras/zai-glm-4.6", "claude-haiku-4-5", "haiku", "flash", "mini"];
|
|
37
37
|
|
|
38
38
|
/** Priority chain for auto-discovering slow/comprehensive models (reasoning, codex) */
|
|
39
39
|
export const SLOW_MODEL_PRIORITY = ["gpt-5.2-codex", "gpt-5.2", "codex", "gpt", "opus", "pro"];
|
|
@@ -79,7 +79,7 @@ function tryMatchModel(modelPattern: string, availableModels: Model<Api>[]): Mod
|
|
|
79
79
|
const provider = modelPattern.substring(0, slashIndex);
|
|
80
80
|
const modelId = modelPattern.substring(slashIndex + 1);
|
|
81
81
|
const providerMatch = availableModels.find(
|
|
82
|
-
(m) => m.provider.toLowerCase() === provider.toLowerCase() && m.id.toLowerCase() === modelId.toLowerCase()
|
|
82
|
+
(m) => m.provider.toLowerCase() === provider.toLowerCase() && m.id.toLowerCase() === modelId.toLowerCase()
|
|
83
83
|
);
|
|
84
84
|
if (providerMatch) {
|
|
85
85
|
return providerMatch;
|
|
@@ -97,7 +97,7 @@ function tryMatchModel(modelPattern: string, availableModels: Model<Api>[]): Mod
|
|
|
97
97
|
const matches = availableModels.filter(
|
|
98
98
|
(m) =>
|
|
99
99
|
m.id.toLowerCase().includes(modelPattern.toLowerCase()) ||
|
|
100
|
-
m.name?.toLowerCase().includes(modelPattern.toLowerCase())
|
|
100
|
+
m.name?.toLowerCase().includes(modelPattern.toLowerCase())
|
|
101
101
|
);
|
|
102
102
|
|
|
103
103
|
if (matches.length === 0) {
|
|
@@ -351,7 +351,7 @@ export async function restoreModelFromSession(
|
|
|
351
351
|
savedModelId: string,
|
|
352
352
|
currentModel: Model<Api> | undefined,
|
|
353
353
|
shouldPrintMessages: boolean,
|
|
354
|
-
modelRegistry: ModelRegistry
|
|
354
|
+
modelRegistry: ModelRegistry
|
|
355
355
|
): Promise<{ model: Model<Api> | undefined; fallbackMessage: string | undefined }> {
|
|
356
356
|
const restoredModel = modelRegistry.find(savedProvider, savedModelId);
|
|
357
357
|
|
|
@@ -427,7 +427,7 @@ export async function restoreModelFromSession(
|
|
|
427
427
|
*/
|
|
428
428
|
export async function findSmolModel(
|
|
429
429
|
modelRegistry: ModelRegistry,
|
|
430
|
-
savedModel?: string
|
|
430
|
+
savedModel?: string
|
|
431
431
|
): Promise<Model<Api> | undefined> {
|
|
432
432
|
const availableModels = modelRegistry.getAvailable();
|
|
433
433
|
if (availableModels.length === 0) return undefined;
|
|
@@ -443,12 +443,16 @@ export async function findSmolModel(
|
|
|
443
443
|
|
|
444
444
|
// 2. Try priority chain
|
|
445
445
|
for (const pattern of SMOL_MODEL_PRIORITY) {
|
|
446
|
+
// Try exact match with provider prefix
|
|
447
|
+
const providerMatch = availableModels.find((m) => `${m.provider}/${m.id}`.toLowerCase() === pattern);
|
|
448
|
+
if (providerMatch) return providerMatch;
|
|
449
|
+
|
|
446
450
|
// Try exact match first
|
|
447
|
-
const exactMatch = availableModels.find((m) => m.id.toLowerCase() === pattern
|
|
451
|
+
const exactMatch = availableModels.find((m) => m.id.toLowerCase() === pattern);
|
|
448
452
|
if (exactMatch) return exactMatch;
|
|
449
453
|
|
|
450
454
|
// Try fuzzy match (substring)
|
|
451
|
-
const fuzzyMatch = availableModels.find((m) => m.id.toLowerCase().includes(pattern
|
|
455
|
+
const fuzzyMatch = availableModels.find((m) => m.id.toLowerCase().includes(pattern));
|
|
452
456
|
if (fuzzyMatch) return fuzzyMatch;
|
|
453
457
|
}
|
|
454
458
|
|
|
@@ -466,7 +470,7 @@ export async function findSmolModel(
|
|
|
466
470
|
*/
|
|
467
471
|
export async function findSlowModel(
|
|
468
472
|
modelRegistry: ModelRegistry,
|
|
469
|
-
savedModel?: string
|
|
473
|
+
savedModel?: string
|
|
470
474
|
): Promise<Model<Api> | undefined> {
|
|
471
475
|
const availableModels = modelRegistry.getAvailable();
|
|
472
476
|
if (availableModels.length === 0) return undefined;
|
package/src/core/sdk.ts
CHANGED
|
@@ -61,7 +61,7 @@ import { logger } from "./logger";
|
|
|
61
61
|
import { discoverAndLoadMCPTools, type MCPManager, type MCPToolsLoadResult } from "./mcp/index";
|
|
62
62
|
import { convertToLlm } from "./messages";
|
|
63
63
|
import { ModelRegistry } from "./model-registry";
|
|
64
|
-
import { parseModelString } from "./model-resolver";
|
|
64
|
+
import { formatModelString, parseModelString } from "./model-resolver";
|
|
65
65
|
import { loadPromptTemplates as loadPromptTemplatesInternal, type PromptTemplate } from "./prompt-templates";
|
|
66
66
|
import { SessionManager } from "./session-manager";
|
|
67
67
|
import { type Settings, SettingsManager, type SkillsSettings } from "./settings-manager";
|
|
@@ -520,6 +520,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
520
520
|
time("loadSession");
|
|
521
521
|
const hasExistingSession = existingSession.messages.length > 0;
|
|
522
522
|
|
|
523
|
+
const hasExplicitModel = options.model !== undefined;
|
|
523
524
|
let model = options.model;
|
|
524
525
|
let modelFallbackMessage: string | undefined;
|
|
525
526
|
|
|
@@ -617,6 +618,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
617
618
|
requireCompleteTool: options.requireCompleteTool,
|
|
618
619
|
getSessionFile: () => sessionManager.getSessionFile() ?? null,
|
|
619
620
|
getSessionSpawns: () => options.spawns ?? "*",
|
|
621
|
+
getModelString: () => (hasExplicitModel && model ? formatModelString(model) : undefined),
|
|
620
622
|
settings: settingsManager,
|
|
621
623
|
};
|
|
622
624
|
|
|
@@ -931,7 +933,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
931
933
|
let lspServers: CreateAgentSessionResult["lspServers"];
|
|
932
934
|
if (settingsManager.getLspDiagnosticsOnWrite()) {
|
|
933
935
|
try {
|
|
934
|
-
const result = await warmupLspServers(cwd
|
|
936
|
+
const result = await warmupLspServers(cwd, {
|
|
937
|
+
onConnecting: (serverNames) => {
|
|
938
|
+
if (options.hasUI && serverNames.length > 0) {
|
|
939
|
+
process.stderr.write(chalk.gray(`Starting LSP servers: ${serverNames.join(", ")}...\n`));
|
|
940
|
+
}
|
|
941
|
+
},
|
|
942
|
+
});
|
|
935
943
|
lspServers = result.servers;
|
|
936
944
|
time("warmupLspServers");
|
|
937
945
|
} catch {
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
createBranchSummaryMessage,
|
|
12
12
|
createCompactionSummaryMessage,
|
|
13
13
|
createCustomMessage,
|
|
14
|
+
type FileMentionMessage,
|
|
14
15
|
type HookMessage,
|
|
15
16
|
} from "./messages";
|
|
16
17
|
import type { SessionStorage, SessionStorageWriter } from "./session-storage";
|
|
@@ -1179,7 +1180,7 @@ export class SessionManager {
|
|
|
1179
1180
|
* so it is easier to find them.
|
|
1180
1181
|
* These need to be appended via appendCompaction() and appendBranchSummary() methods.
|
|
1181
1182
|
*/
|
|
1182
|
-
appendMessage(message: Message | CustomMessage | HookMessage | BashExecutionMessage): string {
|
|
1183
|
+
appendMessage(message: Message | CustomMessage | HookMessage | BashExecutionMessage | FileMentionMessage): string {
|
|
1183
1184
|
const entry: SessionMessageEntry = {
|
|
1184
1185
|
type: "message",
|
|
1185
1186
|
id: generateId(this.byId),
|
|
@@ -179,6 +179,8 @@ export interface Settings {
|
|
|
179
179
|
shellPath?: string; // Custom shell path (e.g., for Cygwin users on Windows)
|
|
180
180
|
collapseChangelog?: boolean; // Show condensed changelog after update (use /changelog for full)
|
|
181
181
|
doubleEscapeAction?: "branch" | "tree"; // Action for double-escape with empty editor (default: "tree")
|
|
182
|
+
/** Environment variables to set automatically on startup */
|
|
183
|
+
env?: Record<string, string>;
|
|
182
184
|
extensions?: string[]; // Array of extension file paths
|
|
183
185
|
skills?: SkillsSettings;
|
|
184
186
|
commands?: CommandsSettings;
|
|
@@ -379,6 +381,29 @@ export class SettingsManager {
|
|
|
379
381
|
this.globalSettings = initialSettings;
|
|
380
382
|
const projectSettings = this.loadProjectSettings();
|
|
381
383
|
this.settings = normalizeSettings(deepMergeSettings(this.globalSettings, projectSettings));
|
|
384
|
+
|
|
385
|
+
// Apply environment variables from settings
|
|
386
|
+
this.applyEnvironmentVariables();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Apply environment variables from settings to process.env
|
|
391
|
+
* Only sets variables that are not already set in the environment
|
|
392
|
+
*/
|
|
393
|
+
applyEnvironmentVariables(): void {
|
|
394
|
+
const envVars = this.settings.env;
|
|
395
|
+
if (!envVars || typeof envVars !== "object") {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
for (const [key, value] of Object.entries(envVars)) {
|
|
400
|
+
if (typeof key === "string" && typeof value === "string") {
|
|
401
|
+
// Only set if not already present in environment (allow override with env vars)
|
|
402
|
+
if (!(key in process.env)) {
|
|
403
|
+
process.env[key] = value;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
382
407
|
}
|
|
383
408
|
|
|
384
409
|
/** Create a SettingsManager that loads from files */
|
|
@@ -1169,4 +1194,49 @@ export class SettingsManager {
|
|
|
1169
1194
|
this.globalSettings.doubleEscapeAction = action;
|
|
1170
1195
|
this.save();
|
|
1171
1196
|
}
|
|
1197
|
+
|
|
1198
|
+
/**
|
|
1199
|
+
* Get environment variables from settings
|
|
1200
|
+
*/
|
|
1201
|
+
getEnvironmentVariables(): Record<string, string> {
|
|
1202
|
+
return { ...(this.settings.env ?? {}) };
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/**
|
|
1206
|
+
* Set environment variables in settings (not process.env)
|
|
1207
|
+
* This will be applied on next startup or reload
|
|
1208
|
+
*/
|
|
1209
|
+
setEnvironmentVariables(envVars: Record<string, string>): void {
|
|
1210
|
+
this.globalSettings.env = { ...envVars };
|
|
1211
|
+
this.save();
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/**
|
|
1215
|
+
* Clear all environment variables from settings
|
|
1216
|
+
*/
|
|
1217
|
+
clearEnvironmentVariables(): void {
|
|
1218
|
+
delete this.globalSettings.env;
|
|
1219
|
+
this.save();
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/**
|
|
1223
|
+
* Set a single environment variable in settings
|
|
1224
|
+
*/
|
|
1225
|
+
setEnvironmentVariable(key: string, value: string): void {
|
|
1226
|
+
if (!this.globalSettings.env) {
|
|
1227
|
+
this.globalSettings.env = {};
|
|
1228
|
+
}
|
|
1229
|
+
this.globalSettings.env[key] = value;
|
|
1230
|
+
this.save();
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/**
|
|
1234
|
+
* Remove a single environment variable from settings
|
|
1235
|
+
*/
|
|
1236
|
+
removeEnvironmentVariable(key: string): void {
|
|
1237
|
+
if (this.globalSettings.env) {
|
|
1238
|
+
delete this.globalSettings.env[key];
|
|
1239
|
+
this.save();
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1172
1242
|
}
|
|
@@ -2,6 +2,7 @@ import { slashCommandCapability } from "../capability/slash-command";
|
|
|
2
2
|
import type { SlashCommand } from "../discovery";
|
|
3
3
|
import { loadSync } from "../discovery";
|
|
4
4
|
import { parseFrontmatter } from "../discovery/helpers";
|
|
5
|
+
import { EMBEDDED_COMMAND_TEMPLATES } from "./tools/task/commands";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Represents a custom slash command loaded from a file
|
|
@@ -15,6 +16,25 @@ export interface FileSlashCommand {
|
|
|
15
16
|
_source?: { providerName: string; level: "user" | "project" | "native" };
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
const EMBEDDED_SLASH_COMMANDS = EMBEDDED_COMMAND_TEMPLATES;
|
|
20
|
+
|
|
21
|
+
function parseCommandTemplate(content: string): { description: string; body: string } {
|
|
22
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
23
|
+
const frontmatterDesc = typeof frontmatter.description === "string" ? frontmatter.description.trim() : "";
|
|
24
|
+
|
|
25
|
+
// Get description from frontmatter or first non-empty line
|
|
26
|
+
let description = frontmatterDesc;
|
|
27
|
+
if (!description) {
|
|
28
|
+
const firstLine = body.split("\n").find((line) => line.trim());
|
|
29
|
+
if (firstLine) {
|
|
30
|
+
description = firstLine.slice(0, 60);
|
|
31
|
+
if (firstLine.length > 60) description += "...";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { description, body };
|
|
36
|
+
}
|
|
37
|
+
|
|
18
38
|
/**
|
|
19
39
|
* Parse command arguments respecting quoted strings (bash-style)
|
|
20
40
|
* Returns array of arguments
|
|
@@ -90,19 +110,8 @@ export interface LoadSlashCommandsOptions {
|
|
|
90
110
|
export function loadSlashCommands(options: LoadSlashCommandsOptions = {}): FileSlashCommand[] {
|
|
91
111
|
const result = loadSync<SlashCommand>(slashCommandCapability.id, { cwd: options.cwd });
|
|
92
112
|
|
|
93
|
-
|
|
94
|
-
const {
|
|
95
|
-
const frontmatterDesc = typeof frontmatter.description === "string" ? frontmatter.description.trim() : "";
|
|
96
|
-
|
|
97
|
-
// Get description from frontmatter or first non-empty line
|
|
98
|
-
let description = frontmatterDesc;
|
|
99
|
-
if (!description) {
|
|
100
|
-
const firstLine = body.split("\n").find((line) => line.trim());
|
|
101
|
-
if (firstLine) {
|
|
102
|
-
description = firstLine.slice(0, 60);
|
|
103
|
-
if (firstLine.length > 60) description += "...";
|
|
104
|
-
}
|
|
105
|
-
}
|
|
113
|
+
const fileCommands: FileSlashCommand[] = result.items.map((cmd) => {
|
|
114
|
+
const { description, body } = parseCommandTemplate(cmd.content);
|
|
106
115
|
|
|
107
116
|
// Format source label: "via ProviderName Level"
|
|
108
117
|
const capitalizedLevel = cmd.level.charAt(0).toUpperCase() + cmd.level.slice(1);
|
|
@@ -116,6 +125,23 @@ export function loadSlashCommands(options: LoadSlashCommandsOptions = {}): FileS
|
|
|
116
125
|
_source: { providerName: cmd._source.providerName, level: cmd.level },
|
|
117
126
|
};
|
|
118
127
|
});
|
|
128
|
+
|
|
129
|
+
const seenNames = new Set(fileCommands.map((cmd) => cmd.name));
|
|
130
|
+
for (const cmd of EMBEDDED_SLASH_COMMANDS) {
|
|
131
|
+
const name = cmd.name.replace(/\.md$/, "");
|
|
132
|
+
if (seenNames.has(name)) continue;
|
|
133
|
+
|
|
134
|
+
const { description, body } = parseCommandTemplate(cmd.content);
|
|
135
|
+
fileCommands.push({
|
|
136
|
+
name,
|
|
137
|
+
description,
|
|
138
|
+
content: body,
|
|
139
|
+
source: "bundled",
|
|
140
|
+
});
|
|
141
|
+
seenNames.add(name);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return fileCommands;
|
|
119
145
|
}
|
|
120
146
|
|
|
121
147
|
/**
|
|
@@ -2,17 +2,54 @@
|
|
|
2
2
|
* Generate session titles using a smol, fast model.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { Model } from "@mariozechner/pi-ai";
|
|
5
|
+
import type { Api, Model } from "@mariozechner/pi-ai";
|
|
6
6
|
import { completeSimple } from "@mariozechner/pi-ai";
|
|
7
7
|
import titleSystemPrompt from "../prompts/title-system.md" with { type: "text" };
|
|
8
8
|
import { logger } from "./logger";
|
|
9
9
|
import type { ModelRegistry } from "./model-registry";
|
|
10
|
-
import {
|
|
10
|
+
import { parseModelString, SMOL_MODEL_PRIORITY } from "./model-resolver";
|
|
11
11
|
|
|
12
12
|
const TITLE_SYSTEM_PROMPT = titleSystemPrompt;
|
|
13
13
|
|
|
14
14
|
const MAX_INPUT_CHARS = 2000;
|
|
15
15
|
|
|
16
|
+
function getTitleModelCandidates(registry: ModelRegistry, savedSmolModel?: string): Model<Api>[] {
|
|
17
|
+
const availableModels = registry.getAvailable();
|
|
18
|
+
if (availableModels.length === 0) return [];
|
|
19
|
+
|
|
20
|
+
const candidates: Model<Api>[] = [];
|
|
21
|
+
const addCandidate = (model?: Model<Api>): void => {
|
|
22
|
+
if (!model) return;
|
|
23
|
+
const exists = candidates.some((candidate) => candidate.provider === model.provider && candidate.id === model.id);
|
|
24
|
+
if (!exists) {
|
|
25
|
+
candidates.push(model);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
if (savedSmolModel) {
|
|
30
|
+
const parsed = parseModelString(savedSmolModel);
|
|
31
|
+
if (parsed) {
|
|
32
|
+
const match = availableModels.find((model) => model.provider === parsed.provider && model.id === parsed.id);
|
|
33
|
+
addCandidate(match);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const pattern of SMOL_MODEL_PRIORITY) {
|
|
38
|
+
const needle = pattern.toLowerCase();
|
|
39
|
+
const exactMatch = availableModels.find((model) => model.id.toLowerCase() === needle);
|
|
40
|
+
addCandidate(exactMatch);
|
|
41
|
+
|
|
42
|
+
const fuzzyMatch = availableModels.find((model) => model.id.toLowerCase().includes(needle));
|
|
43
|
+
addCandidate(fuzzyMatch);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const model of availableModels) {
|
|
47
|
+
addCandidate(model);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return candidates;
|
|
51
|
+
}
|
|
52
|
+
|
|
16
53
|
/**
|
|
17
54
|
* Find the best available model for title generation.
|
|
18
55
|
* Uses the configured smol model if set, otherwise auto-discovers using priority chain.
|
|
@@ -20,9 +57,9 @@ const MAX_INPUT_CHARS = 2000;
|
|
|
20
57
|
* @param registry Model registry
|
|
21
58
|
* @param savedSmolModel Optional saved smol model from settings (provider/modelId format)
|
|
22
59
|
*/
|
|
23
|
-
export async function findTitleModel(registry: ModelRegistry, savedSmolModel?: string): Promise<Model<
|
|
24
|
-
const
|
|
25
|
-
return
|
|
60
|
+
export async function findTitleModel(registry: ModelRegistry, savedSmolModel?: string): Promise<Model<Api> | null> {
|
|
61
|
+
const candidates = getTitleModelCandidates(registry, savedSmolModel);
|
|
62
|
+
return candidates[0] ?? null;
|
|
26
63
|
}
|
|
27
64
|
|
|
28
65
|
/**
|
|
@@ -37,68 +74,85 @@ export async function generateSessionTitle(
|
|
|
37
74
|
registry: ModelRegistry,
|
|
38
75
|
savedSmolModel?: string,
|
|
39
76
|
): Promise<string | null> {
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
77
|
+
const candidates = getTitleModelCandidates(registry, savedSmolModel);
|
|
78
|
+
if (candidates.length === 0) {
|
|
42
79
|
logger.debug("title-generator: no smol model found");
|
|
43
80
|
return null;
|
|
44
81
|
}
|
|
45
82
|
|
|
46
|
-
const apiKey = await registry.getApiKey(model);
|
|
47
|
-
if (!apiKey) {
|
|
48
|
-
logger.debug("title-generator: no API key for model", { provider: model.provider, id: model.id });
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
83
|
// Truncate message if too long
|
|
53
84
|
const truncatedMessage =
|
|
54
85
|
firstMessage.length > MAX_INPUT_CHARS ? `${firstMessage.slice(0, MAX_INPUT_CHARS)}...` : firstMessage;
|
|
86
|
+
const userMessage = `<user-message>\n${truncatedMessage}\n</user-message>`;
|
|
55
87
|
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
};
|
|
62
|
-
logger.debug("title-generator: request", request);
|
|
63
|
-
|
|
64
|
-
try {
|
|
65
|
-
const response = await completeSimple(
|
|
66
|
-
model,
|
|
67
|
-
{
|
|
68
|
-
systemPrompt: request.systemPrompt,
|
|
69
|
-
messages: [{ role: "user", content: request.userMessage, timestamp: Date.now() }],
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
apiKey,
|
|
73
|
-
maxTokens: 30,
|
|
74
|
-
},
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
// Extract title from response text content
|
|
78
|
-
let title = "";
|
|
79
|
-
for (const content of response.content) {
|
|
80
|
-
if (content.type === "text") {
|
|
81
|
-
title += content.text;
|
|
82
|
-
}
|
|
88
|
+
for (const model of candidates) {
|
|
89
|
+
const apiKey = await registry.getApiKey(model);
|
|
90
|
+
if (!apiKey) {
|
|
91
|
+
logger.debug("title-generator: no API key for model", { provider: model.provider, id: model.id });
|
|
92
|
+
continue;
|
|
83
93
|
}
|
|
84
|
-
title = title.trim();
|
|
85
94
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
95
|
+
const request = {
|
|
96
|
+
model: `${model.provider}/${model.id}`,
|
|
97
|
+
systemPrompt: TITLE_SYSTEM_PROMPT,
|
|
98
|
+
userMessage,
|
|
99
|
+
maxTokens: 30,
|
|
100
|
+
};
|
|
101
|
+
logger.debug("title-generator: request", request);
|
|
91
102
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
103
|
+
try {
|
|
104
|
+
const response = await completeSimple(
|
|
105
|
+
model,
|
|
106
|
+
{
|
|
107
|
+
systemPrompt: request.systemPrompt,
|
|
108
|
+
messages: [{ role: "user", content: request.userMessage, timestamp: Date.now() }],
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
apiKey,
|
|
112
|
+
maxTokens: 30,
|
|
113
|
+
},
|
|
114
|
+
);
|
|
95
115
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
116
|
+
if (response.stopReason === "error") {
|
|
117
|
+
logger.debug("title-generator: response error", {
|
|
118
|
+
model: request.model,
|
|
119
|
+
stopReason: response.stopReason,
|
|
120
|
+
errorMessage: response.errorMessage,
|
|
121
|
+
});
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Extract title from response text content
|
|
126
|
+
let title = "";
|
|
127
|
+
for (const content of response.content) {
|
|
128
|
+
if (content.type === "text") {
|
|
129
|
+
title += content.text;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
title = title.trim();
|
|
133
|
+
|
|
134
|
+
logger.debug("title-generator: response", {
|
|
135
|
+
model: request.model,
|
|
136
|
+
title,
|
|
137
|
+
usage: response.usage,
|
|
138
|
+
stopReason: response.stopReason,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
if (!title) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Clean up: remove quotes, trailing punctuation
|
|
146
|
+
return title.replace(/^["']|["']$/g, "").replace(/[.!?]$/, "");
|
|
147
|
+
} catch (err) {
|
|
148
|
+
logger.debug("title-generator: error", {
|
|
149
|
+
model: request.model,
|
|
150
|
+
error: err instanceof Error ? err.message : String(err),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
101
153
|
}
|
|
154
|
+
|
|
155
|
+
return null;
|
|
102
156
|
}
|
|
103
157
|
|
|
104
158
|
/**
|
package/src/core/tools/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ export {
|
|
|
17
17
|
getLspStatus,
|
|
18
18
|
type LspServerStatus,
|
|
19
19
|
type LspToolDetails,
|
|
20
|
+
type LspWarmupOptions,
|
|
20
21
|
type LspWarmupResult,
|
|
21
22
|
warmupLspServers,
|
|
22
23
|
} from "./lsp/index";
|
|
@@ -93,6 +94,8 @@ export interface ToolSession {
|
|
|
93
94
|
getSessionFile: () => string | null;
|
|
94
95
|
/** Get session spawns */
|
|
95
96
|
getSessionSpawns: () => string | null;
|
|
97
|
+
/** Get resolved model string if explicitly set for this session */
|
|
98
|
+
getModelString?: () => string | undefined;
|
|
96
99
|
/** Settings manager (optional) */
|
|
97
100
|
settings?: {
|
|
98
101
|
getImageAutoResize(): boolean;
|
|
@@ -148,7 +151,10 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
148
151
|
|
|
149
152
|
const entries = requestedTools
|
|
150
153
|
? requestedTools.filter((name) => name in allTools).map((name) => [name, allTools[name]] as const)
|
|
151
|
-
: [
|
|
154
|
+
: [
|
|
155
|
+
...Object.entries(BUILTIN_TOOLS),
|
|
156
|
+
...(includeComplete ? ([["complete", HIDDEN_TOOLS.complete]] as const) : []),
|
|
157
|
+
];
|
|
152
158
|
const results = await Promise.all(entries.map(([, factory]) => factory(session)));
|
|
153
159
|
const tools = results.filter((t): t is Tool => t !== null);
|
|
154
160
|
|
|
@@ -373,10 +373,16 @@ async function sendResponse(
|
|
|
373
373
|
// Client Management
|
|
374
374
|
// =============================================================================
|
|
375
375
|
|
|
376
|
+
/** Timeout for warmup initialize requests (5 seconds) */
|
|
377
|
+
export const WARMUP_TIMEOUT_MS = 5000;
|
|
378
|
+
|
|
376
379
|
/**
|
|
377
380
|
* Get or create an LSP client for the given server configuration and working directory.
|
|
381
|
+
* @param config - Server configuration
|
|
382
|
+
* @param cwd - Working directory
|
|
383
|
+
* @param initTimeoutMs - Optional timeout for the initialize request (defaults to 30s)
|
|
378
384
|
*/
|
|
379
|
-
export async function getOrCreateClient(config: ServerConfig, cwd: string): Promise<LspClient> {
|
|
385
|
+
export async function getOrCreateClient(config: ServerConfig, cwd: string, initTimeoutMs?: number): Promise<LspClient> {
|
|
380
386
|
const key = `${config.command}:${cwd}`;
|
|
381
387
|
|
|
382
388
|
// Check if client already exists
|
|
@@ -430,14 +436,20 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string): Prom
|
|
|
430
436
|
|
|
431
437
|
try {
|
|
432
438
|
// Send initialize request
|
|
433
|
-
const initResult = (await sendRequest(
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
439
|
+
const initResult = (await sendRequest(
|
|
440
|
+
client,
|
|
441
|
+
"initialize",
|
|
442
|
+
{
|
|
443
|
+
processId: process.pid,
|
|
444
|
+
rootUri: fileToUri(cwd),
|
|
445
|
+
rootPath: cwd,
|
|
446
|
+
capabilities: CLIENT_CAPABILITIES,
|
|
447
|
+
initializationOptions: config.initOptions ?? {},
|
|
448
|
+
workspaceFolders: [{ uri: fileToUri(cwd), name: cwd.split("/").pop() ?? "workspace" }],
|
|
449
|
+
},
|
|
450
|
+
undefined, // signal
|
|
451
|
+
initTimeoutMs,
|
|
452
|
+
)) as { capabilities?: unknown };
|
|
441
453
|
|
|
442
454
|
if (!initResult) {
|
|
443
455
|
throw new Error("Failed to initialize LSP: no response");
|
|
@@ -662,6 +674,9 @@ export function shutdownClient(key: string): void {
|
|
|
662
674
|
// LSP Protocol Methods
|
|
663
675
|
// =============================================================================
|
|
664
676
|
|
|
677
|
+
/** Default timeout for LSP requests (30 seconds) */
|
|
678
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
679
|
+
|
|
665
680
|
/**
|
|
666
681
|
* Send an LSP request and wait for response.
|
|
667
682
|
*/
|
|
@@ -670,6 +685,7 @@ export async function sendRequest(
|
|
|
670
685
|
method: string,
|
|
671
686
|
params: unknown,
|
|
672
687
|
signal?: AbortSignal,
|
|
688
|
+
timeoutMs: number = DEFAULT_REQUEST_TIMEOUT_MS,
|
|
673
689
|
): Promise<unknown> {
|
|
674
690
|
// Atomically increment and capture request ID
|
|
675
691
|
const id = ++client.requestId;
|
|
@@ -712,7 +728,7 @@ export async function sendRequest(
|
|
|
712
728
|
cleanup();
|
|
713
729
|
reject(err);
|
|
714
730
|
}
|
|
715
|
-
},
|
|
731
|
+
}, timeoutMs);
|
|
716
732
|
if (signal) {
|
|
717
733
|
signal.addEventListener("abort", abortHandler, { once: true });
|
|
718
734
|
if (signal.aborted) {
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
sendRequest,
|
|
20
20
|
setIdleTimeout,
|
|
21
21
|
syncContent,
|
|
22
|
+
WARMUP_TIMEOUT_MS,
|
|
22
23
|
} from "./client";
|
|
23
24
|
import { getLinterClient } from "./clients";
|
|
24
25
|
import { getServersForFile, hasCapability, type LspConfig, loadConfig } from "./config";
|
|
@@ -72,23 +73,36 @@ export interface LspWarmupResult {
|
|
|
72
73
|
}>;
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
/** Options for warming up LSP servers */
|
|
77
|
+
export interface LspWarmupOptions {
|
|
78
|
+
/** Called when starting to connect to servers */
|
|
79
|
+
onConnecting?: (serverNames: string[]) => void;
|
|
80
|
+
}
|
|
81
|
+
|
|
75
82
|
/**
|
|
76
83
|
* Warm up LSP servers for a directory by connecting to all detected servers.
|
|
77
84
|
* This should be called at startup to avoid cold-start delays.
|
|
78
85
|
*
|
|
79
86
|
* @param cwd - Working directory to detect and start servers for
|
|
87
|
+
* @param options - Optional callbacks for progress reporting
|
|
80
88
|
* @returns Status of each server that was started
|
|
81
89
|
*/
|
|
82
|
-
export async function warmupLspServers(cwd: string): Promise<LspWarmupResult> {
|
|
90
|
+
export async function warmupLspServers(cwd: string, options?: LspWarmupOptions): Promise<LspWarmupResult> {
|
|
83
91
|
const config = await loadConfig(cwd);
|
|
84
92
|
setIdleTimeout(config.idleTimeoutMs);
|
|
85
93
|
const servers: LspWarmupResult["servers"] = [];
|
|
86
94
|
const lspServers = getLspServers(config);
|
|
87
95
|
|
|
88
|
-
//
|
|
96
|
+
// Notify caller which servers we're connecting to
|
|
97
|
+
if (lspServers.length > 0 && options?.onConnecting) {
|
|
98
|
+
options.onConnecting(lspServers.map(([name]) => name));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Start all detected servers in parallel with a short timeout
|
|
102
|
+
// Servers that don't respond quickly will be initialized lazily on first use
|
|
89
103
|
const results = await Promise.allSettled(
|
|
90
104
|
lspServers.map(async ([name, serverConfig]) => {
|
|
91
|
-
const client = await getOrCreateClient(serverConfig, cwd);
|
|
105
|
+
const client = await getOrCreateClient(serverConfig, cwd, WARMUP_TIMEOUT_MS);
|
|
92
106
|
return { name, client, fileTypes: serverConfig.fileTypes };
|
|
93
107
|
}),
|
|
94
108
|
);
|
package/src/core/tools/output.ts
CHANGED
|
@@ -189,11 +189,46 @@ function parseOutputProvenance(id: string): OutputProvenance | undefined {
|
|
|
189
189
|
function extractPreviewLines(content: string, maxLines: number): string[] {
|
|
190
190
|
const lines = content.split("\n");
|
|
191
191
|
const preview: string[] = [];
|
|
192
|
+
const structuralTokens = new Set(["{", "}", "[", "]"]);
|
|
193
|
+
|
|
194
|
+
const isStructuralLine = (line: string): boolean => {
|
|
195
|
+
const trimmed = line.trim();
|
|
196
|
+
if (!trimmed) return true;
|
|
197
|
+
const cleaned = trimmed.replace(/,+$/, "");
|
|
198
|
+
return structuralTokens.has(cleaned);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const trimmedContent = content.trim();
|
|
202
|
+
const firstMeaningful = lines.find((line) => line.trim());
|
|
203
|
+
if (
|
|
204
|
+
firstMeaningful &&
|
|
205
|
+
isStructuralLine(firstMeaningful) &&
|
|
206
|
+
(trimmedContent.startsWith("{") || trimmedContent.startsWith("[")) &&
|
|
207
|
+
trimmedContent.length <= 200_000
|
|
208
|
+
) {
|
|
209
|
+
try {
|
|
210
|
+
const parsed = JSON.parse(trimmedContent);
|
|
211
|
+
const minified = JSON.stringify(parsed);
|
|
212
|
+
if (minified) return [minified];
|
|
213
|
+
} catch {
|
|
214
|
+
// Fall back to line-based previews.
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
192
218
|
for (const line of lines) {
|
|
193
|
-
if (
|
|
219
|
+
if (isStructuralLine(line)) continue;
|
|
194
220
|
preview.push(line);
|
|
195
221
|
if (preview.length >= maxLines) break;
|
|
196
222
|
}
|
|
223
|
+
|
|
224
|
+
if (preview.length === 0) {
|
|
225
|
+
for (const line of lines) {
|
|
226
|
+
if (!line.trim()) continue;
|
|
227
|
+
preview.push(line);
|
|
228
|
+
if (preview.length >= maxLines) break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
197
232
|
return preview;
|
|
198
233
|
}
|
|
199
234
|
|
|
@@ -12,13 +12,17 @@ import { loadSync } from "../../../discovery";
|
|
|
12
12
|
import architectPlanMd from "../../../prompts/architect-plan.md" with { type: "text" };
|
|
13
13
|
import implementMd from "../../../prompts/implement.md" with { type: "text" };
|
|
14
14
|
import implementWithCriticMd from "../../../prompts/implement-with-critic.md" with { type: "text" };
|
|
15
|
+
import initMd from "../../../prompts/init.md" with { type: "text" };
|
|
15
16
|
|
|
16
17
|
const EMBEDDED_COMMANDS: { name: string; content: string }[] = [
|
|
17
18
|
{ name: "architect-plan.md", content: architectPlanMd },
|
|
18
19
|
{ name: "implement-with-critic.md", content: implementWithCriticMd },
|
|
19
20
|
{ name: "implement.md", content: implementMd },
|
|
21
|
+
{ name: "init.md", content: initMd },
|
|
20
22
|
];
|
|
21
23
|
|
|
24
|
+
export const EMBEDDED_COMMAND_TEMPLATES: ReadonlyArray<{ name: string; content: string }> = EMBEDDED_COMMANDS;
|
|
25
|
+
|
|
22
26
|
/** Workflow command definition */
|
|
23
27
|
export interface WorkflowCommand {
|
|
24
28
|
name: string;
|
|
@@ -135,6 +135,7 @@ export async function createTaskTool(
|
|
|
135
135
|
const startTime = Date.now();
|
|
136
136
|
const { agents, projectAgentsDir } = await discoverAgents(session.cwd);
|
|
137
137
|
const { agent: agentName, context, model, output: outputSchema } = params;
|
|
138
|
+
const modelOverride = model ?? session.getModelString?.();
|
|
138
139
|
|
|
139
140
|
// Validate agent exists
|
|
140
141
|
const agent = getAgent(agents, agentName);
|
|
@@ -323,7 +324,7 @@ export async function createTaskTool(
|
|
|
323
324
|
toolCount: 0,
|
|
324
325
|
tokens: 0,
|
|
325
326
|
durationMs: 0,
|
|
326
|
-
modelOverride
|
|
327
|
+
modelOverride,
|
|
327
328
|
description: t.description,
|
|
328
329
|
});
|
|
329
330
|
}
|
|
@@ -342,7 +343,7 @@ export async function createTaskTool(
|
|
|
342
343
|
index,
|
|
343
344
|
taskId: task.taskId,
|
|
344
345
|
context: undefined, // Already prepended above
|
|
345
|
-
modelOverride
|
|
346
|
+
modelOverride,
|
|
346
347
|
outputSchema,
|
|
347
348
|
sessionFile,
|
|
348
349
|
persistArtifacts: !!artifactsDir,
|
|
@@ -131,7 +131,7 @@ function renderJsonTreeLines(
|
|
|
131
131
|
pushLine(`${prefix}${iconArray} ${header}`);
|
|
132
132
|
if (val.length === 0) {
|
|
133
133
|
pushLine(
|
|
134
|
-
`${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.
|
|
134
|
+
`${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.last)} ${theme.fg(
|
|
135
135
|
"dim",
|
|
136
136
|
"[]",
|
|
137
137
|
)}`,
|
|
@@ -140,7 +140,7 @@ function renderJsonTreeLines(
|
|
|
140
140
|
}
|
|
141
141
|
if (depth >= maxDepth) {
|
|
142
142
|
pushLine(
|
|
143
|
-
`${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.
|
|
143
|
+
`${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.last)} ${theme.fg(
|
|
144
144
|
"dim",
|
|
145
145
|
theme.format.ellipsis,
|
|
146
146
|
)}`,
|
|
@@ -164,7 +164,7 @@ function renderJsonTreeLines(
|
|
|
164
164
|
const entries = Object.entries(val as Record<string, unknown>);
|
|
165
165
|
if (entries.length === 0) {
|
|
166
166
|
pushLine(
|
|
167
|
-
`${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.
|
|
167
|
+
`${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.last)} ${theme.fg(
|
|
168
168
|
"dim",
|
|
169
169
|
"{}",
|
|
170
170
|
)}`,
|
|
@@ -173,7 +173,7 @@ function renderJsonTreeLines(
|
|
|
173
173
|
}
|
|
174
174
|
if (depth >= maxDepth) {
|
|
175
175
|
pushLine(
|
|
176
|
-
`${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.
|
|
176
|
+
`${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.last)} ${theme.fg(
|
|
177
177
|
"dim",
|
|
178
178
|
theme.format.ellipsis,
|
|
179
179
|
)}`,
|
|
@@ -288,10 +288,8 @@ function renderAgentProgress(
|
|
|
288
288
|
spinnerFrame?: number,
|
|
289
289
|
): string[] {
|
|
290
290
|
const lines: string[] = [];
|
|
291
|
-
const prefix = isLast
|
|
292
|
-
|
|
293
|
-
: `${theme.boxSharp.teeRight}${theme.boxSharp.horizontal}`;
|
|
294
|
-
const continuePrefix = isLast ? " " : `${theme.boxSharp.vertical} `;
|
|
291
|
+
const prefix = isLast ? theme.tree.last : theme.tree.branch;
|
|
292
|
+
const continuePrefix = isLast ? " " : `${theme.tree.vertical} `;
|
|
295
293
|
|
|
296
294
|
const icon = getStatusIcon(progress.status, theme, spinnerFrame);
|
|
297
295
|
const iconColor =
|
|
@@ -460,10 +458,8 @@ function renderFindings(
|
|
|
460
458
|
for (let i = 0; i < displayCount; i++) {
|
|
461
459
|
const finding = findings[i];
|
|
462
460
|
const isLastFinding = i === displayCount - 1 && (expanded || findings.length <= 3);
|
|
463
|
-
const findingPrefix = isLastFinding
|
|
464
|
-
|
|
465
|
-
: `${theme.boxSharp.teeRight}${theme.boxSharp.horizontal}`;
|
|
466
|
-
const findingContinue = isLastFinding ? " " : `${theme.boxSharp.vertical} `;
|
|
461
|
+
const findingPrefix = isLastFinding ? theme.tree.last : theme.tree.branch;
|
|
462
|
+
const findingContinue = isLastFinding ? " " : `${theme.tree.vertical} `;
|
|
467
463
|
|
|
468
464
|
const priority = PRIORITY_LABELS[finding.priority] ?? "P?";
|
|
469
465
|
const color = finding.priority === 0 ? "error" : finding.priority === 1 ? "warning" : "muted";
|
|
@@ -496,10 +492,8 @@ function renderFindings(
|
|
|
496
492
|
*/
|
|
497
493
|
function renderAgentResult(result: SingleResult, isLast: boolean, expanded: boolean, theme: Theme): string[] {
|
|
498
494
|
const lines: string[] = [];
|
|
499
|
-
const prefix = isLast
|
|
500
|
-
|
|
501
|
-
: `${theme.boxSharp.teeRight}${theme.boxSharp.horizontal}`;
|
|
502
|
-
const continuePrefix = isLast ? " " : `${theme.boxSharp.vertical} `;
|
|
495
|
+
const prefix = isLast ? theme.tree.last : theme.tree.branch;
|
|
496
|
+
const continuePrefix = isLast ? " " : `${theme.tree.vertical} `;
|
|
503
497
|
|
|
504
498
|
const aborted = result.aborted ?? false;
|
|
505
499
|
const success = !aborted && result.exitCode === 0;
|
package/src/main.ts
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* createAgentSession() options. The SDK does the heavy lifting.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { type ImageContent, supportsXhigh } from "@mariozechner/pi-ai";
|
|
9
|
-
import chalk from "chalk";
|
|
10
8
|
import { homedir, tmpdir } from "node:os";
|
|
11
9
|
import { join, resolve } from "node:path";
|
|
10
|
+
import { type ImageContent, supportsXhigh } from "@mariozechner/pi-ai";
|
|
11
|
+
import chalk from "chalk";
|
|
12
12
|
import { type Args, parseArgs, printHelp } from "./cli/args";
|
|
13
13
|
import { processFileArguments } from "./cli/file-processor";
|
|
14
14
|
import { listModels } from "./cli/list-models";
|
|
@@ -421,6 +421,7 @@ export async function main(args: string[]) {
|
|
|
421
421
|
|
|
422
422
|
const cwd = process.cwd();
|
|
423
423
|
const settingsManager = SettingsManager.create(cwd);
|
|
424
|
+
settingsManager.applyEnvironmentVariables();
|
|
424
425
|
time("SettingsManager.create");
|
|
425
426
|
const { initialMessage, initialImages } = await prepareInitialMessage(parsed, settingsManager.getImageAutoResize());
|
|
426
427
|
time("prepareInitialMessage");
|
|
@@ -27,12 +27,10 @@ export class BashExecutionComponent extends Container {
|
|
|
27
27
|
private fullOutputPath?: string;
|
|
28
28
|
private expanded = false;
|
|
29
29
|
private contentContainer: Container;
|
|
30
|
-
private ui: TUI;
|
|
31
30
|
|
|
32
31
|
constructor(command: string, ui: TUI, excludeFromContext = false) {
|
|
33
32
|
super();
|
|
34
33
|
this.command = command;
|
|
35
|
-
this.ui = ui;
|
|
36
34
|
|
|
37
35
|
// Use dim border for excluded-from-context commands (!! prefix)
|
|
38
36
|
const colorKey = excludeFromContext ? "dim" : "bashMode";
|
|
@@ -142,15 +140,16 @@ export class BashExecutionComponent extends Container {
|
|
|
142
140
|
const displayText = availableLines.map((line) => theme.fg("muted", line)).join("\n");
|
|
143
141
|
this.contentContainer.addChild(new Text(`\n${displayText}`, 1, 0));
|
|
144
142
|
} else {
|
|
145
|
-
// Use shared visual truncation utility
|
|
143
|
+
// Use shared visual truncation utility, recomputed per render width
|
|
146
144
|
const styledOutput = previewLogicalLines.map((line) => theme.fg("muted", line)).join("\n");
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
145
|
+
const previewText = `\n${styledOutput}`;
|
|
146
|
+
this.contentContainer.addChild({
|
|
147
|
+
render: (width: number) => {
|
|
148
|
+
const { visualLines } = truncateToVisualLines(previewText, PREVIEW_LINES, width, 1);
|
|
149
|
+
return visualLines;
|
|
150
|
+
},
|
|
151
|
+
invalidate: () => {},
|
|
152
|
+
});
|
|
154
153
|
}
|
|
155
154
|
}
|
|
156
155
|
|
|
@@ -437,13 +437,10 @@ class TreeList implements Component {
|
|
|
437
437
|
|
|
438
438
|
// Build prefix with gutters at their correct positions
|
|
439
439
|
// Each gutter has a position (displayIndent where its connector was shown)
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
: `${theme.boxSharp.teeRight}${theme.boxSharp.horizontal} `
|
|
445
|
-
: "";
|
|
446
|
-
const connectorPosition = connector ? displayIndent - 1 : -1;
|
|
440
|
+
const hasConnector = flatNode.showConnector && !flatNode.isVirtualRootChild;
|
|
441
|
+
const connectorSymbol = hasConnector ? (flatNode.isLast ? theme.tree.last : theme.tree.branch) : "";
|
|
442
|
+
const connectorChars = hasConnector ? Array.from(connectorSymbol) : [];
|
|
443
|
+
const connectorPosition = hasConnector ? displayIndent - 1 : -1;
|
|
447
444
|
|
|
448
445
|
// Build prefix char by char, placing gutters and connector at their positions
|
|
449
446
|
const totalChars = displayIndent * 3;
|
|
@@ -456,18 +453,18 @@ class TreeList implements Component {
|
|
|
456
453
|
const gutter = flatNode.gutters.find((g) => g.position === level);
|
|
457
454
|
if (gutter) {
|
|
458
455
|
if (posInLevel === 0) {
|
|
459
|
-
prefixChars.push(gutter.show ? theme.
|
|
456
|
+
prefixChars.push(gutter.show ? theme.tree.vertical : " ");
|
|
460
457
|
} else {
|
|
461
458
|
prefixChars.push(" ");
|
|
462
459
|
}
|
|
463
|
-
} else if (
|
|
460
|
+
} else if (hasConnector && level === connectorPosition) {
|
|
464
461
|
// Connector at this level
|
|
465
462
|
if (posInLevel === 0) {
|
|
466
|
-
prefixChars.push(
|
|
463
|
+
prefixChars.push(connectorChars[0] ?? " ");
|
|
467
464
|
} else if (posInLevel === 1) {
|
|
468
|
-
prefixChars.push(theme.
|
|
465
|
+
prefixChars.push(connectorChars[1] ?? theme.tree.horizontal);
|
|
469
466
|
} else {
|
|
470
|
-
prefixChars.push(" ");
|
|
467
|
+
prefixChars.push(connectorChars[2] ?? " ");
|
|
471
468
|
}
|
|
472
469
|
} else {
|
|
473
470
|
prefixChars.push(" ");
|
|
@@ -1215,6 +1215,9 @@ export class InteractiveMode {
|
|
|
1215
1215
|
this.editor.setText("");
|
|
1216
1216
|
this.updatePendingMessagesDisplay();
|
|
1217
1217
|
this.ui.requestRender();
|
|
1218
|
+
} else if (event.message.role === "fileMention") {
|
|
1219
|
+
this.addMessageToChat(event.message);
|
|
1220
|
+
this.ui.requestRender();
|
|
1218
1221
|
} else if (event.message.role === "assistant") {
|
|
1219
1222
|
this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock);
|
|
1220
1223
|
this.streamingMessage = event.message;
|
|
@@ -1566,7 +1569,7 @@ export class InteractiveMode {
|
|
|
1566
1569
|
case "fileMention": {
|
|
1567
1570
|
// Render compact file mention display
|
|
1568
1571
|
for (const file of message.files) {
|
|
1569
|
-
const text = `${theme.fg("dim", `${theme.tree.
|
|
1572
|
+
const text = `${theme.fg("dim", `${theme.tree.last} `)}${theme.fg("muted", "Read")} ${theme.fg(
|
|
1570
1573
|
"accent",
|
|
1571
1574
|
file.path,
|
|
1572
1575
|
)} ${theme.fg("dim", `(${file.lineCount} lines)`)}`;
|
|
@@ -1906,31 +1909,6 @@ export class InteractiveMode {
|
|
|
1906
1909
|
this.voiceSupervisor.notifyProgress(text);
|
|
1907
1910
|
}
|
|
1908
1911
|
|
|
1909
|
-
private async toggleVoiceListening(): Promise<void> {
|
|
1910
|
-
if (!this.settingsManager.getVoiceEnabled()) {
|
|
1911
|
-
this.settingsManager.setVoiceEnabled(true);
|
|
1912
|
-
this.showStatus("Voice mode enabled.");
|
|
1913
|
-
}
|
|
1914
|
-
|
|
1915
|
-
if (this.voiceAutoModeEnabled) {
|
|
1916
|
-
this.voiceAutoModeEnabled = false;
|
|
1917
|
-
this.stopVoiceProgressTimer();
|
|
1918
|
-
await this.voiceSupervisor.stop();
|
|
1919
|
-
this.setVoiceStatus(undefined);
|
|
1920
|
-
this.showStatus("Voice mode disabled.");
|
|
1921
|
-
return;
|
|
1922
|
-
}
|
|
1923
|
-
|
|
1924
|
-
this.voiceAutoModeEnabled = true;
|
|
1925
|
-
try {
|
|
1926
|
-
await this.voiceSupervisor.start();
|
|
1927
|
-
} catch (error) {
|
|
1928
|
-
this.voiceAutoModeEnabled = false;
|
|
1929
|
-
this.setVoiceStatus(undefined);
|
|
1930
|
-
this.showError(error instanceof Error ? error.message : String(error));
|
|
1931
|
-
}
|
|
1932
|
-
}
|
|
1933
|
-
|
|
1934
1912
|
private async submitVoiceText(text: string): Promise<void> {
|
|
1935
1913
|
const cleaned = text.trim();
|
|
1936
1914
|
if (!cleaned) {
|
|
@@ -209,8 +209,8 @@ const UNICODE_SYMBOLS: SymbolMap = {
|
|
|
209
209
|
"tree.vertical": "│",
|
|
210
210
|
// pick: ─ | alt: ━ ═ ╌ ┄
|
|
211
211
|
"tree.horizontal": "─",
|
|
212
|
-
// pick:
|
|
213
|
-
"tree.hook": "
|
|
212
|
+
// pick: └ | alt: ⎿ ╰ ↳
|
|
213
|
+
"tree.hook": "\u2514",
|
|
214
214
|
// Box Drawing - Rounded
|
|
215
215
|
// pick: ╭ | alt: ┌ ┏ ╔
|
|
216
216
|
"boxRound.topLeft": "╭",
|