@oh-my-pi/pi-coding-agent 1.338.0 → 1.341.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 +60 -1
- package/package.json +3 -3
- package/src/cli/args.ts +8 -0
- package/src/core/agent-session.ts +32 -14
- package/src/core/export-html/index.ts +48 -15
- package/src/core/export-html/template.html +3 -11
- package/src/core/mcp/client.ts +43 -16
- package/src/core/mcp/config.ts +152 -6
- package/src/core/mcp/index.ts +6 -2
- package/src/core/mcp/loader.ts +30 -3
- package/src/core/mcp/manager.ts +69 -10
- package/src/core/mcp/types.ts +9 -3
- package/src/core/model-resolver.ts +101 -0
- package/src/core/sdk.ts +65 -18
- package/src/core/session-manager.ts +117 -14
- package/src/core/settings-manager.ts +107 -19
- package/src/core/title-generator.ts +94 -0
- package/src/core/tools/bash.ts +1 -2
- package/src/core/tools/edit-diff.ts +2 -2
- package/src/core/tools/edit.ts +43 -5
- package/src/core/tools/grep.ts +3 -2
- package/src/core/tools/index.ts +73 -13
- package/src/core/tools/lsp/client.ts +45 -20
- package/src/core/tools/lsp/config.ts +708 -34
- package/src/core/tools/lsp/index.ts +423 -23
- package/src/core/tools/lsp/types.ts +5 -0
- package/src/core/tools/task/bundled-agents/explore.md +1 -1
- package/src/core/tools/task/bundled-agents/reviewer.md +1 -1
- package/src/core/tools/task/model-resolver.ts +52 -3
- package/src/core/tools/write.ts +67 -4
- package/src/index.ts +5 -0
- package/src/main.ts +23 -2
- package/src/modes/interactive/components/model-selector.ts +96 -18
- package/src/modes/interactive/components/session-selector.ts +20 -7
- package/src/modes/interactive/components/settings-defs.ts +59 -2
- package/src/modes/interactive/components/settings-selector.ts +8 -11
- package/src/modes/interactive/components/tool-execution.ts +18 -0
- package/src/modes/interactive/components/tree-selector.ts +2 -2
- package/src/modes/interactive/components/welcome.ts +40 -3
- package/src/modes/interactive/interactive-mode.ts +87 -10
- package/src/core/export-html/vendor/highlight.min.js +0 -1213
- package/src/core/export-html/vendor/marked.min.js +0 -6
package/src/core/tools/write.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
|
2
2
|
import { Type } from "@sinclair/typebox";
|
|
3
3
|
import { mkdir, writeFile } from "fs/promises";
|
|
4
4
|
import { dirname } from "path";
|
|
5
|
+
import type { FileDiagnosticsResult, FileFormatResult } from "./lsp/index.js";
|
|
5
6
|
import { resolveToCwd } from "./path-utils.js";
|
|
6
7
|
|
|
7
8
|
const writeSchema = Type.Object({
|
|
@@ -9,7 +10,30 @@ const writeSchema = Type.Object({
|
|
|
9
10
|
content: Type.String({ description: "Content to write to the file" }),
|
|
10
11
|
});
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
/** Options for creating the write tool */
|
|
14
|
+
export interface WriteToolOptions {
|
|
15
|
+
/** Callback to format file using LSP after writing */
|
|
16
|
+
formatOnWrite?: (absolutePath: string) => Promise<FileFormatResult>;
|
|
17
|
+
/** Callback to get LSP diagnostics after writing a file */
|
|
18
|
+
getDiagnostics?: (absolutePath: string) => Promise<FileDiagnosticsResult>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Details returned by the write tool for TUI rendering */
|
|
22
|
+
export interface WriteToolDetails {
|
|
23
|
+
/** Whether the file was formatted */
|
|
24
|
+
wasFormatted: boolean;
|
|
25
|
+
/** Format result (if available) */
|
|
26
|
+
formatResult?: FileFormatResult;
|
|
27
|
+
/** Whether LSP diagnostics were retrieved */
|
|
28
|
+
hasDiagnostics: boolean;
|
|
29
|
+
/** Diagnostic result (if available) */
|
|
30
|
+
diagnostics?: FileDiagnosticsResult;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createWriteTool(
|
|
34
|
+
cwd: string,
|
|
35
|
+
options: WriteToolOptions = {},
|
|
36
|
+
): AgentTool<typeof writeSchema, WriteToolDetails> {
|
|
13
37
|
return {
|
|
14
38
|
name: "write",
|
|
15
39
|
label: "Write",
|
|
@@ -30,7 +54,7 @@ Usage:
|
|
|
30
54
|
const absolutePath = resolveToCwd(path, cwd);
|
|
31
55
|
const dir = dirname(absolutePath);
|
|
32
56
|
|
|
33
|
-
return new Promise<{ content: Array<{ type: "text"; text: string }>; details:
|
|
57
|
+
return new Promise<{ content: Array<{ type: "text"; text: string }>; details: WriteToolDetails }>(
|
|
34
58
|
(resolve, reject) => {
|
|
35
59
|
// Check if already aborted
|
|
36
60
|
if (signal?.aborted) {
|
|
@@ -74,9 +98,48 @@ Usage:
|
|
|
74
98
|
signal.removeEventListener("abort", onAbort);
|
|
75
99
|
}
|
|
76
100
|
|
|
101
|
+
// Format file if callback provided (before diagnostics)
|
|
102
|
+
let formatResult: FileFormatResult | undefined;
|
|
103
|
+
if (options.formatOnWrite) {
|
|
104
|
+
try {
|
|
105
|
+
formatResult = await options.formatOnWrite(absolutePath);
|
|
106
|
+
} catch {
|
|
107
|
+
// Ignore formatting errors - don't fail the write
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Get LSP diagnostics if callback provided (after formatting)
|
|
112
|
+
let diagnosticsResult: FileDiagnosticsResult | undefined;
|
|
113
|
+
if (options.getDiagnostics) {
|
|
114
|
+
try {
|
|
115
|
+
diagnosticsResult = await options.getDiagnostics(absolutePath);
|
|
116
|
+
} catch {
|
|
117
|
+
// Ignore diagnostics errors - don't fail the write
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Build result text
|
|
122
|
+
let resultText = `Successfully wrote ${content.length} bytes to ${path}`;
|
|
123
|
+
|
|
124
|
+
// Note if file was formatted
|
|
125
|
+
if (formatResult?.formatted) {
|
|
126
|
+
resultText += ` (formatted by ${formatResult.serverName})`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Append diagnostics if available and there are issues
|
|
130
|
+
if (diagnosticsResult?.available && diagnosticsResult.diagnostics.length > 0) {
|
|
131
|
+
resultText += `\n\nLSP Diagnostics (${diagnosticsResult.summary}):\n`;
|
|
132
|
+
resultText += diagnosticsResult.diagnostics.map((d) => ` ${d}`).join("\n");
|
|
133
|
+
}
|
|
134
|
+
|
|
77
135
|
resolve({
|
|
78
|
-
content: [{ type: "text", text:
|
|
79
|
-
details:
|
|
136
|
+
content: [{ type: "text", text: resultText }],
|
|
137
|
+
details: {
|
|
138
|
+
wasFormatted: formatResult?.formatted ?? false,
|
|
139
|
+
formatResult,
|
|
140
|
+
hasDiagnostics: diagnosticsResult?.available ?? false,
|
|
141
|
+
diagnostics: diagnosticsResult,
|
|
142
|
+
},
|
|
80
143
|
});
|
|
81
144
|
} catch (error: any) {
|
|
82
145
|
// Clean up abort handler
|
package/src/index.ts
CHANGED
|
@@ -123,6 +123,7 @@ export {
|
|
|
123
123
|
} from "./core/session-manager.js";
|
|
124
124
|
export {
|
|
125
125
|
type CompactionSettings,
|
|
126
|
+
type LspSettings,
|
|
126
127
|
type RetrySettings,
|
|
127
128
|
type Settings,
|
|
128
129
|
SettingsManager,
|
|
@@ -143,6 +144,7 @@ export {
|
|
|
143
144
|
export {
|
|
144
145
|
type BashToolDetails,
|
|
145
146
|
bashTool,
|
|
147
|
+
type CodingToolsOptions,
|
|
146
148
|
codingTools,
|
|
147
149
|
editTool,
|
|
148
150
|
type FindToolDetails,
|
|
@@ -154,8 +156,11 @@ export {
|
|
|
154
156
|
type ReadToolDetails,
|
|
155
157
|
readTool,
|
|
156
158
|
type TruncationResult,
|
|
159
|
+
type WriteToolDetails,
|
|
160
|
+
type WriteToolOptions,
|
|
157
161
|
writeTool,
|
|
158
162
|
} from "./core/tools/index.js";
|
|
163
|
+
export type { FileDiagnosticsResult } from "./core/tools/lsp/index.js";
|
|
159
164
|
// Main entry point
|
|
160
165
|
export { main } from "./main.js";
|
|
161
166
|
// UI components for hooks and custom tools
|
package/src/main.ts
CHANGED
|
@@ -62,11 +62,20 @@ async function runInteractiveMode(
|
|
|
62
62
|
initialMessages: string[],
|
|
63
63
|
customTools: LoadedCustomTool[],
|
|
64
64
|
setToolUIContext: (uiContext: HookUIContext, hasUI: boolean) => void,
|
|
65
|
+
lspServers: Array<{ name: string; status: "ready" | "error"; fileTypes: string[] }> | undefined,
|
|
65
66
|
initialMessage?: string,
|
|
66
67
|
initialImages?: ImageContent[],
|
|
67
68
|
fdPath: string | undefined = undefined,
|
|
68
69
|
): Promise<void> {
|
|
69
|
-
const mode = new InteractiveMode(
|
|
70
|
+
const mode = new InteractiveMode(
|
|
71
|
+
session,
|
|
72
|
+
version,
|
|
73
|
+
changelogMarkdown,
|
|
74
|
+
customTools,
|
|
75
|
+
setToolUIContext,
|
|
76
|
+
lspServers,
|
|
77
|
+
fdPath,
|
|
78
|
+
);
|
|
70
79
|
|
|
71
80
|
await mode.init();
|
|
72
81
|
|
|
@@ -347,6 +356,17 @@ export async function main(args: string[]) {
|
|
|
347
356
|
|
|
348
357
|
const settingsManager = SettingsManager.create(cwd);
|
|
349
358
|
time("SettingsManager.create");
|
|
359
|
+
|
|
360
|
+
// Apply model role overrides from CLI args or env vars (ephemeral, not persisted)
|
|
361
|
+
const smolModel = parsed.smol ?? process.env.PI_SMOL_MODEL;
|
|
362
|
+
const slowModel = parsed.slow ?? process.env.PI_SLOW_MODEL;
|
|
363
|
+
if (smolModel || slowModel) {
|
|
364
|
+
const roleOverrides: Record<string, string> = {};
|
|
365
|
+
if (smolModel) roleOverrides.smol = smolModel;
|
|
366
|
+
if (slowModel) roleOverrides.slow = slowModel;
|
|
367
|
+
settingsManager.applyOverrides({ modelRoles: roleOverrides });
|
|
368
|
+
}
|
|
369
|
+
|
|
350
370
|
initTheme(settingsManager.getTheme(), isInteractive);
|
|
351
371
|
time("initTheme");
|
|
352
372
|
|
|
@@ -393,7 +413,7 @@ export async function main(args: string[]) {
|
|
|
393
413
|
}
|
|
394
414
|
|
|
395
415
|
time("buildSessionOptions");
|
|
396
|
-
const { session, customToolsResult, modelFallbackMessage } = await createAgentSession(sessionOptions);
|
|
416
|
+
const { session, customToolsResult, modelFallbackMessage, lspServers } = await createAgentSession(sessionOptions);
|
|
397
417
|
time("createAgentSession");
|
|
398
418
|
|
|
399
419
|
if (!isInteractive && !session.model) {
|
|
@@ -449,6 +469,7 @@ export async function main(args: string[]) {
|
|
|
449
469
|
parsed.messages,
|
|
450
470
|
customToolsResult.tools,
|
|
451
471
|
customToolsResult.setUIContext,
|
|
472
|
+
lspServers,
|
|
452
473
|
initialMessage,
|
|
453
474
|
initialImages,
|
|
454
475
|
fdPath,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Model, modelsAreEqual } from "@oh-my-pi/pi-ai";
|
|
2
2
|
import { Container, Input, isArrowDown, isArrowUp, isEnter, isEscape, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
|
|
3
3
|
import type { ModelRegistry } from "../../../core/model-registry.js";
|
|
4
|
+
import { parseModelString } from "../../../core/model-resolver.js";
|
|
4
5
|
import type { SettingsManager } from "../../../core/settings-manager.js";
|
|
5
6
|
import { fuzzyFilter } from "../../../utils/fuzzy.js";
|
|
6
7
|
import { theme } from "../theme/theme.js";
|
|
@@ -18,7 +19,11 @@ interface ScopedModelItem {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
|
-
* Component that renders a model selector with search
|
|
22
|
+
* Component that renders a model selector with search.
|
|
23
|
+
* - Enter: Set selected model as default
|
|
24
|
+
* - S: Set selected model as smol
|
|
25
|
+
* - L: Set selected model as slow
|
|
26
|
+
* - Escape: Close selector
|
|
22
27
|
*/
|
|
23
28
|
export class ModelSelectorComponent extends Container {
|
|
24
29
|
private searchInput: Input;
|
|
@@ -27,9 +32,12 @@ export class ModelSelectorComponent extends Container {
|
|
|
27
32
|
private filteredModels: ModelItem[] = [];
|
|
28
33
|
private selectedIndex: number = 0;
|
|
29
34
|
private currentModel?: Model<any>;
|
|
35
|
+
private defaultModel?: Model<any>;
|
|
36
|
+
private smolModel?: Model<any>;
|
|
37
|
+
private slowModel?: Model<any>;
|
|
30
38
|
private settingsManager: SettingsManager;
|
|
31
39
|
private modelRegistry: ModelRegistry;
|
|
32
|
-
private onSelectCallback: (model: Model<any
|
|
40
|
+
private onSelectCallback: (model: Model<any>, role: string) => void;
|
|
33
41
|
private onCancelCallback: () => void;
|
|
34
42
|
private errorMessage?: string;
|
|
35
43
|
private tui: TUI;
|
|
@@ -41,7 +49,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
41
49
|
settingsManager: SettingsManager,
|
|
42
50
|
modelRegistry: ModelRegistry,
|
|
43
51
|
scopedModels: ReadonlyArray<ScopedModelItem>,
|
|
44
|
-
onSelect: (model: Model<any
|
|
52
|
+
onSelect: (model: Model<any>, role: string) => void,
|
|
45
53
|
onCancel: () => void,
|
|
46
54
|
) {
|
|
47
55
|
super();
|
|
@@ -54,24 +62,28 @@ export class ModelSelectorComponent extends Container {
|
|
|
54
62
|
this.onSelectCallback = onSelect;
|
|
55
63
|
this.onCancelCallback = onCancel;
|
|
56
64
|
|
|
65
|
+
// Load current role assignments from settings
|
|
66
|
+
this._loadRoleModels();
|
|
67
|
+
|
|
57
68
|
// Add top border
|
|
58
69
|
this.addChild(new DynamicBorder());
|
|
59
70
|
this.addChild(new Spacer(1));
|
|
60
71
|
|
|
61
|
-
// Add hint about model filtering
|
|
72
|
+
// Add hint about model filtering and key bindings
|
|
62
73
|
const hintText =
|
|
63
74
|
scopedModels.length > 0
|
|
64
75
|
? "Showing models from --models scope"
|
|
65
76
|
: "Only showing models with configured API keys (see README for details)";
|
|
66
77
|
this.addChild(new Text(theme.fg("warning", hintText), 0, 0));
|
|
78
|
+
this.addChild(new Text(theme.fg("muted", "Enter: default S: smol L: slow Esc: close"), 0, 0));
|
|
67
79
|
this.addChild(new Spacer(1));
|
|
68
80
|
|
|
69
81
|
// Create search input
|
|
70
82
|
this.searchInput = new Input();
|
|
71
83
|
this.searchInput.onSubmit = () => {
|
|
72
|
-
// Enter on search input
|
|
84
|
+
// Enter on search input sets as default
|
|
73
85
|
if (this.filteredModels[this.selectedIndex]) {
|
|
74
|
-
this.handleSelect(this.filteredModels[this.selectedIndex].model);
|
|
86
|
+
this.handleSelect(this.filteredModels[this.selectedIndex].model, "default");
|
|
75
87
|
}
|
|
76
88
|
};
|
|
77
89
|
this.addChild(this.searchInput);
|
|
@@ -95,6 +107,38 @@ export class ModelSelectorComponent extends Container {
|
|
|
95
107
|
});
|
|
96
108
|
}
|
|
97
109
|
|
|
110
|
+
private _loadRoleModels(): void {
|
|
111
|
+
const roles = this.settingsManager.getModelRoles();
|
|
112
|
+
const allModels = this.modelRegistry.getAll();
|
|
113
|
+
|
|
114
|
+
// Load default model
|
|
115
|
+
const defaultStr = roles.default;
|
|
116
|
+
if (defaultStr) {
|
|
117
|
+
const parsed = parseModelString(defaultStr);
|
|
118
|
+
if (parsed) {
|
|
119
|
+
this.defaultModel = allModels.find((m) => m.provider === parsed.provider && m.id === parsed.id);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Load smol model
|
|
124
|
+
const smolStr = roles.smol;
|
|
125
|
+
if (smolStr) {
|
|
126
|
+
const parsed = parseModelString(smolStr);
|
|
127
|
+
if (parsed) {
|
|
128
|
+
this.smolModel = allModels.find((m) => m.provider === parsed.provider && m.id === parsed.id);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Load slow model
|
|
133
|
+
const slowStr = roles.slow;
|
|
134
|
+
if (slowStr) {
|
|
135
|
+
const parsed = parseModelString(slowStr);
|
|
136
|
+
if (parsed) {
|
|
137
|
+
this.slowModel = allModels.find((m) => m.provider === parsed.provider && m.id === parsed.id);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
98
142
|
private async loadModels(): Promise<void> {
|
|
99
143
|
let models: ModelItem[];
|
|
100
144
|
|
|
@@ -167,20 +211,26 @@ export class ModelSelectorComponent extends Container {
|
|
|
167
211
|
if (!item) continue;
|
|
168
212
|
|
|
169
213
|
const isSelected = i === this.selectedIndex;
|
|
170
|
-
const
|
|
214
|
+
const isDefault = modelsAreEqual(this.defaultModel, item.model);
|
|
215
|
+
const isSmol = modelsAreEqual(this.smolModel, item.model);
|
|
216
|
+
const isSlow = modelsAreEqual(this.slowModel, item.model);
|
|
217
|
+
|
|
218
|
+
// Build role markers: ✓ for default, ⚡ for smol, 🧠 for slow
|
|
219
|
+
let markers = "";
|
|
220
|
+
if (isDefault) markers += theme.fg("success", " ✓");
|
|
221
|
+
if (isSmol) markers += theme.fg("warning", " ⚡");
|
|
222
|
+
if (isSlow) markers += theme.fg("accent", " 🧠");
|
|
171
223
|
|
|
172
224
|
let line = "";
|
|
173
225
|
if (isSelected) {
|
|
174
226
|
const prefix = theme.fg("accent", "→ ");
|
|
175
227
|
const modelText = `${item.id}`;
|
|
176
228
|
const providerBadge = theme.fg("muted", `[${item.provider}]`);
|
|
177
|
-
|
|
178
|
-
line = `${prefix + theme.fg("accent", modelText)} ${providerBadge}${checkmark}`;
|
|
229
|
+
line = `${prefix + theme.fg("accent", modelText)} ${providerBadge}${markers}`;
|
|
179
230
|
} else {
|
|
180
231
|
const modelText = ` ${item.id}`;
|
|
181
232
|
const providerBadge = theme.fg("muted", `[${item.provider}]`);
|
|
182
|
-
|
|
183
|
-
line = `${modelText} ${providerBadge}${checkmark}`;
|
|
233
|
+
line = `${modelText} ${providerBadge}${markers}`;
|
|
184
234
|
}
|
|
185
235
|
|
|
186
236
|
this.listContainer.addChild(new Text(line, 0, 0));
|
|
@@ -217,14 +267,28 @@ export class ModelSelectorComponent extends Container {
|
|
|
217
267
|
this.selectedIndex = this.selectedIndex === this.filteredModels.length - 1 ? 0 : this.selectedIndex + 1;
|
|
218
268
|
this.updateList();
|
|
219
269
|
}
|
|
220
|
-
// Enter
|
|
270
|
+
// Enter - set as default model (don't close)
|
|
221
271
|
else if (isEnter(keyData)) {
|
|
222
272
|
const selectedModel = this.filteredModels[this.selectedIndex];
|
|
223
273
|
if (selectedModel) {
|
|
224
|
-
this.handleSelect(selectedModel.model);
|
|
274
|
+
this.handleSelect(selectedModel.model, "default");
|
|
225
275
|
}
|
|
226
276
|
}
|
|
227
|
-
//
|
|
277
|
+
// S key - set as smol model (don't close)
|
|
278
|
+
else if (keyData === "s" || keyData === "S") {
|
|
279
|
+
const selectedModel = this.filteredModels[this.selectedIndex];
|
|
280
|
+
if (selectedModel) {
|
|
281
|
+
this.handleSelect(selectedModel.model, "smol");
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// L key - set as slow model (don't close)
|
|
285
|
+
else if (keyData === "l" || keyData === "L") {
|
|
286
|
+
const selectedModel = this.filteredModels[this.selectedIndex];
|
|
287
|
+
if (selectedModel) {
|
|
288
|
+
this.handleSelect(selectedModel.model, "slow");
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// Escape - close
|
|
228
292
|
else if (isEscape(keyData)) {
|
|
229
293
|
this.onCancelCallback();
|
|
230
294
|
}
|
|
@@ -235,10 +299,24 @@ export class ModelSelectorComponent extends Container {
|
|
|
235
299
|
}
|
|
236
300
|
}
|
|
237
301
|
|
|
238
|
-
private handleSelect(model: Model<any
|
|
239
|
-
// Save
|
|
240
|
-
this.settingsManager.
|
|
241
|
-
|
|
302
|
+
private handleSelect(model: Model<any>, role: string): void {
|
|
303
|
+
// Save to settings
|
|
304
|
+
this.settingsManager.setModelRole(role, `${model.provider}/${model.id}`);
|
|
305
|
+
|
|
306
|
+
// Update local state for UI
|
|
307
|
+
if (role === "default") {
|
|
308
|
+
this.defaultModel = model;
|
|
309
|
+
} else if (role === "smol") {
|
|
310
|
+
this.smolModel = model;
|
|
311
|
+
} else if (role === "slow") {
|
|
312
|
+
this.slowModel = model;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Notify caller (for updating agent state if needed)
|
|
316
|
+
this.onSelectCallback(model, role);
|
|
317
|
+
|
|
318
|
+
// Update list to show new markers
|
|
319
|
+
this.updateList();
|
|
242
320
|
}
|
|
243
321
|
|
|
244
322
|
getSearchInput(): Input {
|
|
@@ -90,7 +90,7 @@ class SessionList implements Component {
|
|
|
90
90
|
);
|
|
91
91
|
const endIndex = Math.min(startIndex + this.maxVisible, this.filteredSessions.length);
|
|
92
92
|
|
|
93
|
-
// Render visible sessions (2 lines per session + blank line)
|
|
93
|
+
// Render visible sessions (2-3 lines per session + blank line)
|
|
94
94
|
for (let i = startIndex; i < endIndex; i++) {
|
|
95
95
|
const session = this.filteredSessions[i];
|
|
96
96
|
const isSelected = i === this.selectedIndex;
|
|
@@ -98,19 +98,32 @@ class SessionList implements Component {
|
|
|
98
98
|
// Normalize first message to single line
|
|
99
99
|
const normalizedMessage = session.firstMessage.replace(/\n/g, " ").trim();
|
|
100
100
|
|
|
101
|
-
// First line: cursor +
|
|
101
|
+
// First line: cursor + title (or first message if no title)
|
|
102
102
|
const cursor = isSelected ? theme.fg("accent", "› ") : " ";
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
const maxWidth = width - 2; // Account for cursor (2 visible chars)
|
|
104
|
+
|
|
105
|
+
if (session.title) {
|
|
106
|
+
// Has title: show title on first line, dimmed first message on second line
|
|
107
|
+
const truncatedTitle = truncateToWidth(session.title, maxWidth, "...");
|
|
108
|
+
const titleLine = cursor + (isSelected ? theme.bold(truncatedTitle) : truncatedTitle);
|
|
109
|
+
lines.push(titleLine);
|
|
110
|
+
|
|
111
|
+
// Second line: dimmed first message preview
|
|
112
|
+
const truncatedPreview = truncateToWidth(normalizedMessage, maxWidth, "...");
|
|
113
|
+
lines.push(` ${theme.fg("dim", truncatedPreview)}`);
|
|
114
|
+
} else {
|
|
115
|
+
// No title: show first message as main line
|
|
116
|
+
const truncatedMsg = truncateToWidth(normalizedMessage, maxWidth, "...");
|
|
117
|
+
const messageLine = cursor + (isSelected ? theme.bold(truncatedMsg) : truncatedMsg);
|
|
118
|
+
lines.push(messageLine);
|
|
119
|
+
}
|
|
106
120
|
|
|
107
|
-
//
|
|
121
|
+
// Metadata line: date + message count
|
|
108
122
|
const modified = formatDate(session.modified);
|
|
109
123
|
const msgCount = `${session.messageCount} message${session.messageCount !== 1 ? "s" : ""}`;
|
|
110
124
|
const metadata = ` ${modified} · ${msgCount}`;
|
|
111
125
|
const metadataLine = theme.fg("dim", truncateToWidth(metadata, width, ""));
|
|
112
126
|
|
|
113
|
-
lines.push(messageLine);
|
|
114
127
|
lines.push(metadataLine);
|
|
115
128
|
lines.push(""); // Blank line between sessions
|
|
116
129
|
}
|
|
@@ -20,7 +20,7 @@ interface BaseSettingDef {
|
|
|
20
20
|
id: string;
|
|
21
21
|
label: string;
|
|
22
22
|
description: string;
|
|
23
|
-
tab:
|
|
23
|
+
tab: string;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
// Boolean toggle setting
|
|
@@ -99,6 +99,16 @@ export const SETTINGS_DEFS: SettingDef[] = [
|
|
|
99
99
|
get: (sm) => sm.getQueueMode(),
|
|
100
100
|
set: (sm, v) => sm.setQueueMode(v as "all" | "one-at-a-time"), // Also handled in session
|
|
101
101
|
},
|
|
102
|
+
{
|
|
103
|
+
id: "interruptMode",
|
|
104
|
+
tab: "config",
|
|
105
|
+
type: "enum",
|
|
106
|
+
label: "Interrupt mode",
|
|
107
|
+
description: "When to process queued messages: immediately (interrupt tools) or wait for turn to complete",
|
|
108
|
+
values: ["immediate", "wait"],
|
|
109
|
+
get: (sm) => sm.getInterruptMode(),
|
|
110
|
+
set: (sm, v) => sm.setInterruptMode(v as "immediate" | "wait"), // Also handled in session
|
|
111
|
+
},
|
|
102
112
|
{
|
|
103
113
|
id: "hideThinking",
|
|
104
114
|
tab: "config",
|
|
@@ -126,6 +136,24 @@ export const SETTINGS_DEFS: SettingDef[] = [
|
|
|
126
136
|
get: (sm) => sm.getBashInterceptorEnabled(),
|
|
127
137
|
set: (sm, v) => sm.setBashInterceptorEnabled(v),
|
|
128
138
|
},
|
|
139
|
+
{
|
|
140
|
+
id: "mcpProjectConfig",
|
|
141
|
+
tab: "config",
|
|
142
|
+
type: "boolean",
|
|
143
|
+
label: "MCP project config",
|
|
144
|
+
description: "Load .mcp.json/mcp.json from project root",
|
|
145
|
+
get: (sm) => sm.getMCPProjectConfigEnabled(),
|
|
146
|
+
set: (sm, v) => sm.setMCPProjectConfigEnabled(v),
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: "editFuzzyMatch",
|
|
150
|
+
tab: "config",
|
|
151
|
+
type: "boolean",
|
|
152
|
+
label: "Edit fuzzy match",
|
|
153
|
+
description: "Accept high-confidence fuzzy matches for whitespace/indentation differences",
|
|
154
|
+
get: (sm) => sm.getEditFuzzyMatch(),
|
|
155
|
+
set: (sm, v) => sm.setEditFuzzyMatch(v),
|
|
156
|
+
},
|
|
129
157
|
{
|
|
130
158
|
id: "thinkingLevel",
|
|
131
159
|
tab: "config",
|
|
@@ -152,6 +180,35 @@ export const SETTINGS_DEFS: SettingDef[] = [
|
|
|
152
180
|
getOptions: () => [], // Filled dynamically from context
|
|
153
181
|
},
|
|
154
182
|
|
|
183
|
+
// LSP tab
|
|
184
|
+
{
|
|
185
|
+
id: "lspFormatOnWrite",
|
|
186
|
+
tab: "lsp",
|
|
187
|
+
type: "boolean",
|
|
188
|
+
label: "Format on write",
|
|
189
|
+
description: "Automatically format code files using LSP after writing",
|
|
190
|
+
get: (sm) => sm.getLspFormatOnWrite(),
|
|
191
|
+
set: (sm, v) => sm.setLspFormatOnWrite(v),
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
id: "lspDiagnosticsOnWrite",
|
|
195
|
+
tab: "lsp",
|
|
196
|
+
type: "boolean",
|
|
197
|
+
label: "Diagnostics on write",
|
|
198
|
+
description: "Return LSP diagnostics (errors/warnings) after writing code files",
|
|
199
|
+
get: (sm) => sm.getLspDiagnosticsOnWrite(),
|
|
200
|
+
set: (sm, v) => sm.setLspDiagnosticsOnWrite(v),
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
id: "lspDiagnosticsOnEdit",
|
|
204
|
+
tab: "lsp",
|
|
205
|
+
type: "boolean",
|
|
206
|
+
label: "Diagnostics on edit",
|
|
207
|
+
description: "Return LSP diagnostics (errors/warnings) after editing code files",
|
|
208
|
+
get: (sm) => sm.getLspDiagnosticsOnEdit(),
|
|
209
|
+
set: (sm, v) => sm.setLspDiagnosticsOnEdit(v),
|
|
210
|
+
},
|
|
211
|
+
|
|
155
212
|
// Exa tab
|
|
156
213
|
{
|
|
157
214
|
id: "exaEnabled",
|
|
@@ -210,7 +267,7 @@ export const SETTINGS_DEFS: SettingDef[] = [
|
|
|
210
267
|
];
|
|
211
268
|
|
|
212
269
|
/** Get settings for a specific tab */
|
|
213
|
-
export function getSettingsForTab(tab:
|
|
270
|
+
export function getSettingsForTab(tab: string): SettingDef[] {
|
|
214
271
|
return SETTINGS_DEFS.filter((def) => def.tab === tab);
|
|
215
272
|
}
|
|
216
273
|
|
|
@@ -93,10 +93,11 @@ class SelectSubmenu extends Container {
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
type TabId =
|
|
96
|
+
type TabId = string;
|
|
97
97
|
|
|
98
98
|
const SETTINGS_TABS: Tab[] = [
|
|
99
99
|
{ id: "config", label: "Config" },
|
|
100
|
+
{ id: "lsp", label: "LSP" },
|
|
100
101
|
{ id: "exa", label: "Exa" },
|
|
101
102
|
{ id: "plugins", label: "Plugins" },
|
|
102
103
|
];
|
|
@@ -189,14 +190,10 @@ export class SettingsSelectorComponent extends Container {
|
|
|
189
190
|
const bottomBorder = this.children[this.children.length - 1];
|
|
190
191
|
this.removeChild(bottomBorder);
|
|
191
192
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
break;
|
|
197
|
-
case "plugins":
|
|
198
|
-
this.showPluginsTab();
|
|
199
|
-
break;
|
|
193
|
+
if (tabId === "plugins") {
|
|
194
|
+
this.showPluginsTab();
|
|
195
|
+
} else {
|
|
196
|
+
this.showSettingsTab(tabId);
|
|
200
197
|
}
|
|
201
198
|
|
|
202
199
|
// Re-add bottom border
|
|
@@ -301,9 +298,9 @@ export class SettingsSelectorComponent extends Container {
|
|
|
301
298
|
}
|
|
302
299
|
|
|
303
300
|
/**
|
|
304
|
-
* Show a settings tab
|
|
301
|
+
* Show a settings tab using definitions.
|
|
305
302
|
*/
|
|
306
|
-
private showSettingsTab(tabId:
|
|
303
|
+
private showSettingsTab(tabId: string): void {
|
|
307
304
|
const defs = getSettingsForTab(tabId);
|
|
308
305
|
const items: SettingItem[] = [];
|
|
309
306
|
|
|
@@ -501,6 +501,24 @@ export class ToolExecutionComponent extends Container {
|
|
|
501
501
|
text += theme.fg("toolOutput", `\n... (${remaining} more lines, ${totalLines} total)`);
|
|
502
502
|
}
|
|
503
503
|
}
|
|
504
|
+
|
|
505
|
+
// Show LSP diagnostics if available
|
|
506
|
+
if (this.result?.details?.diagnostics?.available) {
|
|
507
|
+
const diag = this.result.details.diagnostics;
|
|
508
|
+
if (diag.diagnostics.length > 0) {
|
|
509
|
+
const icon = diag.hasErrors ? theme.fg("error", "●") : theme.fg("warning", "●");
|
|
510
|
+
text += `\n\n${icon} ${theme.fg("toolTitle", "LSP Diagnostics")} ${theme.fg("dim", `(${diag.summary})`)}`;
|
|
511
|
+
const maxDiags = this.expanded ? diag.diagnostics.length : 5;
|
|
512
|
+
const displayDiags = diag.diagnostics.slice(0, maxDiags);
|
|
513
|
+
for (const d of displayDiags) {
|
|
514
|
+
const color = d.includes("[error]") ? "error" : d.includes("[warning]") ? "warning" : "dim";
|
|
515
|
+
text += `\n ${theme.fg(color, d)}`;
|
|
516
|
+
}
|
|
517
|
+
if (diag.diagnostics.length > maxDiags) {
|
|
518
|
+
text += theme.fg("dim", `\n ... (${diag.diagnostics.length - maxDiags} more)`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
504
522
|
} else if (this.toolName === "edit") {
|
|
505
523
|
const rawPath = this.args?.file_path || this.args?.path || "";
|
|
506
524
|
const path = shortenPath(rawPath);
|
|
@@ -356,7 +356,7 @@ class TreeList implements Component {
|
|
|
356
356
|
parts.push("branch summary", entry.summary);
|
|
357
357
|
break;
|
|
358
358
|
case "model_change":
|
|
359
|
-
parts.push("model", entry.
|
|
359
|
+
parts.push("model", entry.model);
|
|
360
360
|
break;
|
|
361
361
|
case "thinking_level_change":
|
|
362
362
|
parts.push("thinking", entry.thinkingLevel);
|
|
@@ -558,7 +558,7 @@ class TreeList implements Component {
|
|
|
558
558
|
result = theme.fg("warning", `[branch summary]: `) + normalize(entry.summary);
|
|
559
559
|
break;
|
|
560
560
|
case "model_change":
|
|
561
|
-
result = theme.fg("dim", `[model: ${entry.
|
|
561
|
+
result = theme.fg("dim", `[model: ${entry.model}]`);
|
|
562
562
|
break;
|
|
563
563
|
case "thinking_level_change":
|
|
564
564
|
result = theme.fg("dim", `[thinking: ${entry.thinkingLevel}]`);
|