@oh-my-pi/pi-coding-agent 3.6.1337 → 3.9.1337
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 +39 -0
- package/package.json +4 -4
- package/src/core/bash-executor.ts +115 -154
- package/src/core/index.ts +2 -0
- package/src/core/session-manager.ts +16 -6
- package/src/core/settings-manager.ts +2 -2
- package/src/core/tools/edit-diff.ts +45 -33
- package/src/core/tools/edit.ts +70 -182
- package/src/core/tools/find.ts +141 -160
- package/src/core/tools/index.ts +10 -9
- package/src/core/tools/ls.ts +64 -82
- package/src/core/tools/lsp/client.ts +66 -0
- package/src/core/tools/lsp/edits.ts +13 -4
- package/src/core/tools/lsp/index.ts +191 -85
- package/src/core/tools/notebook.ts +89 -144
- package/src/core/tools/read.ts +110 -158
- package/src/core/tools/write.ts +22 -115
- package/src/core/utils.ts +187 -0
- package/src/modes/interactive/components/{footer.ts → status-line.ts} +124 -71
- package/src/modes/interactive/components/tool-execution.ts +14 -14
- package/src/modes/interactive/interactive-mode.ts +57 -73
- package/src/modes/interactive/theme/dark.json +13 -13
- package/src/modes/interactive/theme/light.json +13 -13
- package/src/modes/interactive/theme/theme.ts +29 -28
|
@@ -6,15 +6,43 @@ import { type Component, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui"
|
|
|
6
6
|
import type { AgentSession } from "../../../core/agent-session";
|
|
7
7
|
import { theme } from "../theme/theme";
|
|
8
8
|
|
|
9
|
-
//
|
|
9
|
+
// Thinking level icons (Nerd Font)
|
|
10
|
+
const THINKING_ICONS: Record<string, string> = {
|
|
11
|
+
minimal: "🤨 min",
|
|
12
|
+
low: "🤔 low",
|
|
13
|
+
medium: "🤓 mid",
|
|
14
|
+
high: "🤯 high",
|
|
15
|
+
xhigh: "🧠 xhi",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Nerd Font icons
|
|
10
19
|
const ICONS = {
|
|
11
20
|
model: "\uf4bc", // robot/model
|
|
12
21
|
folder: "\uf115", // folder
|
|
13
|
-
branch: "\
|
|
22
|
+
branch: "\ue725", // git branch
|
|
14
23
|
sep: "\ue0b1", // powerline thin chevron
|
|
15
|
-
tokens: "\
|
|
24
|
+
tokens: "\ue26b", // coins
|
|
25
|
+
context: "\ue70f", // window
|
|
26
|
+
auto: "\udb80\udc68", // auto
|
|
16
27
|
} as const;
|
|
17
28
|
|
|
29
|
+
/** Create a colored text segment with background */
|
|
30
|
+
function plSegment(content: string, fgAnsi: string, bgAnsi: string): string {
|
|
31
|
+
return `${bgAnsi}${fgAnsi} ${content} \x1b[0m`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Create separator with background */
|
|
35
|
+
function plSep(sepAnsi: string, bgAnsi: string): string {
|
|
36
|
+
return `${bgAnsi}${sepAnsi}${ICONS.sep}\x1b[0m`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Create end cap - solid arrow transitioning bg to terminal default */
|
|
40
|
+
function plEnd(bgAnsi: string): string {
|
|
41
|
+
// Use the bg color as fg for the arrow (creates the triangle effect)
|
|
42
|
+
const fgFromBg = bgAnsi.replace("\x1b[48;", "\x1b[38;");
|
|
43
|
+
return `${fgFromBg}\ue0b0\x1b[0m`;
|
|
44
|
+
}
|
|
45
|
+
|
|
18
46
|
/**
|
|
19
47
|
* Sanitize text for display in a single-line status.
|
|
20
48
|
* Removes newlines, tabs, carriage returns, and other control characters.
|
|
@@ -50,7 +78,7 @@ function findGitHeadPath(): string | null {
|
|
|
50
78
|
/**
|
|
51
79
|
* Footer component that shows pwd, token stats, and context usage
|
|
52
80
|
*/
|
|
53
|
-
export class
|
|
81
|
+
export class StatusLineComponent implements Component {
|
|
54
82
|
private session: AgentSession;
|
|
55
83
|
private cachedBranch: string | null | undefined = undefined; // undefined = not checked yet, null = not in git repo, string = branch name
|
|
56
84
|
private gitWatcher: FSWatcher | null = null;
|
|
@@ -220,7 +248,7 @@ export class FooterComponent implements Component {
|
|
|
220
248
|
}
|
|
221
249
|
}
|
|
222
250
|
|
|
223
|
-
|
|
251
|
+
private buildStatusLine(): string {
|
|
224
252
|
const state = this.session.state;
|
|
225
253
|
|
|
226
254
|
// Calculate cumulative usage from ALL session entries
|
|
@@ -264,132 +292,157 @@ export class FooterComponent implements Component {
|
|
|
264
292
|
return `${Math.round(n / 1000000)}M`;
|
|
265
293
|
};
|
|
266
294
|
|
|
267
|
-
// Powerline separator (very dim)
|
|
268
|
-
const sep = theme.fg("footerSep", ` ${ICONS.sep} `);
|
|
269
|
-
|
|
270
295
|
// ═══════════════════════════════════════════════════════════════════════
|
|
271
|
-
// SEGMENT 1: Model
|
|
296
|
+
// SEGMENT 1: Model
|
|
272
297
|
// ═══════════════════════════════════════════════════════════════════════
|
|
273
|
-
|
|
274
|
-
|
|
298
|
+
let modelName = state.model?.name || state.model?.id || "no-model";
|
|
299
|
+
// Strip "Claude " prefix for brevity
|
|
300
|
+
if (modelName.startsWith("Claude ")) {
|
|
301
|
+
modelName = modelName.slice(7);
|
|
302
|
+
}
|
|
303
|
+
let modelContent = `${ICONS.model} ${modelName}`;
|
|
275
304
|
if (state.model?.reasoning) {
|
|
276
305
|
const level = state.thinkingLevel || "off";
|
|
277
306
|
if (level !== "off") {
|
|
278
|
-
|
|
307
|
+
modelContent += ` · ${THINKING_ICONS[level] ?? level}`;
|
|
279
308
|
}
|
|
280
309
|
}
|
|
281
310
|
|
|
282
311
|
// ═══════════════════════════════════════════════════════════════════════
|
|
283
|
-
// SEGMENT 2: Path
|
|
284
|
-
// Replace home with ~, strip /work/, color separators
|
|
312
|
+
// SEGMENT 2: Path
|
|
285
313
|
// ═══════════════════════════════════════════════════════════════════════
|
|
286
314
|
let pwd = process.cwd();
|
|
287
315
|
const home = process.env.HOME || process.env.USERPROFILE;
|
|
288
316
|
if (home && pwd.startsWith(home)) {
|
|
289
317
|
pwd = `~${pwd.slice(home.length)}`;
|
|
290
318
|
}
|
|
291
|
-
// Strip /work/ prefix
|
|
292
319
|
if (pwd.startsWith("/work/")) {
|
|
293
320
|
pwd = pwd.slice(6);
|
|
294
321
|
}
|
|
295
|
-
|
|
296
|
-
const pathColored = pwd
|
|
297
|
-
.split("/")
|
|
298
|
-
.map((part) => theme.fg("footerPath", part))
|
|
299
|
-
.join(theme.fg("footerSep", "/"));
|
|
300
|
-
const pathSegment = theme.fg("footerIcon", `${ICONS.folder} `) + pathColored;
|
|
322
|
+
const pathContent = `${ICONS.folder} ${pwd}`;
|
|
301
323
|
|
|
302
324
|
// ═══════════════════════════════════════════════════════════════════════
|
|
303
|
-
// SEGMENT 3: Git Branch + Status
|
|
325
|
+
// SEGMENT 3: Git Branch + Status
|
|
304
326
|
// ═══════════════════════════════════════════════════════════════════════
|
|
305
327
|
const branch = this.getCurrentBranch();
|
|
306
|
-
let
|
|
328
|
+
let gitContent = "";
|
|
329
|
+
let gitColorName: "statusLineGitClean" | "statusLineGitDirty" = "statusLineGitClean";
|
|
307
330
|
if (branch) {
|
|
308
331
|
const gitStatus = this.getGitStatus();
|
|
309
332
|
const isDirty = gitStatus && (gitStatus.staged > 0 || gitStatus.unstaged > 0 || gitStatus.untracked > 0);
|
|
333
|
+
gitColorName = isDirty ? "statusLineGitDirty" : "statusLineGitClean";
|
|
310
334
|
|
|
311
|
-
|
|
312
|
-
const branchColor = isDirty ? "footerDirty" : "footerBranch";
|
|
313
|
-
gitSegment = theme.fg("footerIcon", `${ICONS.branch} `) + theme.fg(branchColor, branch);
|
|
335
|
+
gitContent = `${ICONS.branch} ${branch}`;
|
|
314
336
|
|
|
315
|
-
// Add status indicators
|
|
316
337
|
if (gitStatus) {
|
|
317
338
|
const indicators: string[] = [];
|
|
318
339
|
if (gitStatus.unstaged > 0) {
|
|
319
|
-
indicators.push(theme.fg("
|
|
340
|
+
indicators.push(theme.fg("statusLineDirty", `*${gitStatus.unstaged}`));
|
|
320
341
|
}
|
|
321
342
|
if (gitStatus.staged > 0) {
|
|
322
|
-
indicators.push(theme.fg("
|
|
343
|
+
indicators.push(theme.fg("statusLineStaged", `+${gitStatus.staged}`));
|
|
323
344
|
}
|
|
324
345
|
if (gitStatus.untracked > 0) {
|
|
325
|
-
indicators.push(theme.fg("
|
|
346
|
+
indicators.push(theme.fg("statusLineUntracked", `?${gitStatus.untracked}`));
|
|
326
347
|
}
|
|
327
348
|
if (indicators.length > 0) {
|
|
328
|
-
|
|
349
|
+
gitContent += ` ${indicators.join(" ")}`;
|
|
329
350
|
}
|
|
330
351
|
}
|
|
331
352
|
}
|
|
332
353
|
|
|
333
354
|
// ═══════════════════════════════════════════════════════════════════════
|
|
334
|
-
// SEGMENT 4:
|
|
335
|
-
// Concise: total tokens, cost, context%
|
|
355
|
+
// SEGMENT 4: Context (window usage)
|
|
336
356
|
// ═══════════════════════════════════════════════════════════════════════
|
|
337
|
-
const
|
|
357
|
+
const autoIndicator = this.autoCompactEnabled ? ` ${ICONS.auto}` : "";
|
|
358
|
+
const contextText = `${contextPercentValue.toFixed(1)}%/${formatTokens(contextWindow)}${autoIndicator}`;
|
|
359
|
+
let contextContent: string;
|
|
360
|
+
if (contextPercentValue > 90) {
|
|
361
|
+
contextContent = `${ICONS.context} ${theme.fg("error", contextText)}`;
|
|
362
|
+
} else if (contextPercentValue > 70) {
|
|
363
|
+
contextContent = `${ICONS.context} ${theme.fg("warning", contextText)}`;
|
|
364
|
+
} else {
|
|
365
|
+
contextContent = `${ICONS.context} ${contextText}`;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
369
|
+
// SEGMENT 5: Spend (tokens + cost)
|
|
370
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
371
|
+
const spendParts: string[] = [];
|
|
338
372
|
|
|
339
|
-
// Total tokens (input + output + cache)
|
|
340
373
|
const totalTokens = totalInput + totalOutput + totalCacheRead + totalCacheWrite;
|
|
341
374
|
if (totalTokens) {
|
|
342
|
-
|
|
375
|
+
spendParts.push(`${ICONS.tokens} ${formatTokens(totalTokens)}`);
|
|
343
376
|
}
|
|
344
377
|
|
|
345
|
-
// Cost (pink)
|
|
346
378
|
const usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false;
|
|
347
379
|
if (totalCost || usingSubscription) {
|
|
348
|
-
const costDisplay = `$${totalCost.toFixed(
|
|
349
|
-
|
|
380
|
+
const costDisplay = `$${totalCost.toFixed(2)}${usingSubscription ? " (sub)" : ""}`;
|
|
381
|
+
spendParts.push(costDisplay);
|
|
350
382
|
}
|
|
351
383
|
|
|
352
|
-
|
|
353
|
-
const autoIndicator = this.autoCompactEnabled ? " (auto)" : "";
|
|
354
|
-
const contextDisplay = `${contextPercentValue.toFixed(1)}%/${formatTokens(contextWindow)}${autoIndicator}`;
|
|
355
|
-
let contextColored: string;
|
|
356
|
-
if (contextPercentValue > 90) {
|
|
357
|
-
contextColored = theme.fg("error", contextDisplay);
|
|
358
|
-
} else if (contextPercentValue > 70) {
|
|
359
|
-
contextColored = theme.fg("warning", contextDisplay);
|
|
360
|
-
} else {
|
|
361
|
-
contextColored = theme.fg("footerSep", contextDisplay);
|
|
362
|
-
}
|
|
363
|
-
statParts.push(contextColored);
|
|
364
|
-
|
|
365
|
-
const statsSegment = statParts.join(" ");
|
|
384
|
+
const spendContent = theme.fg("statusLineCost", spendParts.join(" · "));
|
|
366
385
|
|
|
367
386
|
// ═══════════════════════════════════════════════════════════════════════
|
|
368
|
-
// Assemble
|
|
369
|
-
// [Model] > [Path] > [Git] > [Stats]
|
|
387
|
+
// Assemble: [Model] > [Path] > [Git?] > [Context] > [Spend] >
|
|
370
388
|
// ═══════════════════════════════════════════════════════════════════════
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
389
|
+
const bgAnsi = theme.getBgAnsi("statusLineBg");
|
|
390
|
+
const sepAnsi = theme.getFgAnsi("statusLineSep");
|
|
391
|
+
|
|
392
|
+
let statusLine = "";
|
|
393
|
+
|
|
394
|
+
// Model segment
|
|
395
|
+
statusLine += plSegment(modelContent, theme.getFgAnsi("statusLineModel"), bgAnsi);
|
|
396
|
+
statusLine += plSep(sepAnsi, bgAnsi);
|
|
374
397
|
|
|
375
|
-
|
|
398
|
+
// Path segment
|
|
399
|
+
statusLine += plSegment(pathContent, theme.getFgAnsi("statusLinePath"), bgAnsi);
|
|
376
400
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
statusLine
|
|
401
|
+
if (gitContent) {
|
|
402
|
+
statusLine += plSep(sepAnsi, bgAnsi);
|
|
403
|
+
statusLine += plSegment(gitContent, theme.getFgAnsi(gitColorName), bgAnsi);
|
|
380
404
|
}
|
|
381
405
|
|
|
382
|
-
|
|
406
|
+
// Context segment
|
|
407
|
+
statusLine += plSep(sepAnsi, bgAnsi);
|
|
408
|
+
statusLine += plSegment(contextContent, theme.getFgAnsi("statusLineContext"), bgAnsi);
|
|
409
|
+
|
|
410
|
+
// Spend segment
|
|
411
|
+
statusLine += plSep(sepAnsi, bgAnsi);
|
|
412
|
+
statusLine += plSegment(spendContent, theme.getFgAnsi("statusLineSpend"), bgAnsi);
|
|
413
|
+
|
|
414
|
+
// End cap (solid arrow to terminal bg)
|
|
415
|
+
statusLine += plEnd(bgAnsi);
|
|
416
|
+
|
|
417
|
+
return statusLine;
|
|
418
|
+
}
|
|
383
419
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
420
|
+
/**
|
|
421
|
+
* Get the status line content for use as editor top border.
|
|
422
|
+
* Returns the content string and its visible width.
|
|
423
|
+
*/
|
|
424
|
+
getTopBorder(_width: number): { content: string; width: number } {
|
|
425
|
+
const content = this.buildStatusLine();
|
|
426
|
+
return {
|
|
427
|
+
content,
|
|
428
|
+
width: visibleWidth(content),
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Render only hook statuses (if any).
|
|
434
|
+
* Used when footer is integrated into editor border.
|
|
435
|
+
*/
|
|
436
|
+
render(width: number): string[] {
|
|
437
|
+
// Only render hook statuses - main status is in editor's top border
|
|
438
|
+
if (this.hookStatuses.size === 0) {
|
|
439
|
+
return [];
|
|
391
440
|
}
|
|
392
441
|
|
|
393
|
-
|
|
442
|
+
const sortedStatuses = Array.from(this.hookStatuses.entries())
|
|
443
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
444
|
+
.map(([, text]) => sanitizeStatusText(text));
|
|
445
|
+
const hookLine = sortedStatuses.join(" ");
|
|
446
|
+
return [truncateToWidth(hookLine, width, theme.fg("statusLineSep", "…"))];
|
|
394
447
|
}
|
|
395
448
|
}
|
|
@@ -503,19 +503,19 @@ export class ToolExecutionComponent extends Container {
|
|
|
503
503
|
}
|
|
504
504
|
|
|
505
505
|
// Show LSP diagnostics if available
|
|
506
|
-
if (this.result?.details?.diagnostics
|
|
506
|
+
if (this.result?.details?.diagnostics) {
|
|
507
507
|
const diag = this.result.details.diagnostics;
|
|
508
|
-
if (diag.
|
|
509
|
-
const icon = diag.
|
|
508
|
+
if (diag.messages.length > 0) {
|
|
509
|
+
const icon = diag.errored ? theme.fg("error", "●") : theme.fg("warning", "●");
|
|
510
510
|
text += `\n\n${icon} ${theme.fg("toolTitle", "LSP Diagnostics")} ${theme.fg("dim", `(${diag.summary})`)}`;
|
|
511
|
-
const maxDiags = this.expanded ? diag.
|
|
512
|
-
const displayDiags = diag.
|
|
511
|
+
const maxDiags = this.expanded ? diag.messages.length : 5;
|
|
512
|
+
const displayDiags = diag.messages.slice(0, maxDiags);
|
|
513
513
|
for (const d of displayDiags) {
|
|
514
514
|
const color = d.includes("[error]") ? "error" : d.includes("[warning]") ? "warning" : "dim";
|
|
515
515
|
text += `\n ${theme.fg(color, d)}`;
|
|
516
516
|
}
|
|
517
|
-
if (diag.
|
|
518
|
-
text += theme.fg("dim", `\n ... (${diag.
|
|
517
|
+
if (diag.messages.length > maxDiags) {
|
|
518
|
+
text += theme.fg("dim", `\n ... (${diag.messages.length - maxDiags} more)`);
|
|
519
519
|
}
|
|
520
520
|
}
|
|
521
521
|
}
|
|
@@ -552,19 +552,19 @@ export class ToolExecutionComponent extends Container {
|
|
|
552
552
|
}
|
|
553
553
|
|
|
554
554
|
// Show LSP diagnostics if available
|
|
555
|
-
if (this.result?.details?.diagnostics
|
|
555
|
+
if (this.result?.details?.diagnostics) {
|
|
556
556
|
const diag = this.result.details.diagnostics;
|
|
557
|
-
if (diag.
|
|
558
|
-
const icon = diag.
|
|
557
|
+
if (diag.messages.length > 0) {
|
|
558
|
+
const icon = diag.errored ? theme.fg("error", "●") : theme.fg("warning", "●");
|
|
559
559
|
text += `\n\n${icon} ${theme.fg("toolTitle", "LSP Diagnostics")} ${theme.fg("dim", `(${diag.summary})`)}`;
|
|
560
|
-
const maxDiags = this.expanded ? diag.
|
|
561
|
-
const displayDiags = diag.
|
|
560
|
+
const maxDiags = this.expanded ? diag.messages.length : 5;
|
|
561
|
+
const displayDiags = diag.messages.slice(0, maxDiags);
|
|
562
562
|
for (const d of displayDiags) {
|
|
563
563
|
const color = d.includes("[error]") ? "error" : d.includes("[warning]") ? "warning" : "dim";
|
|
564
564
|
text += `\n ${theme.fg(color, d)}`;
|
|
565
565
|
}
|
|
566
|
-
if (diag.
|
|
567
|
-
text += theme.fg("dim", `\n ... (${diag.
|
|
566
|
+
if (diag.messages.length > maxDiags) {
|
|
567
|
+
text += theme.fg("dim", `\n ... (${diag.messages.length - maxDiags} more)`);
|
|
568
568
|
}
|
|
569
569
|
}
|
|
570
570
|
}
|
|
@@ -43,7 +43,6 @@ import { CompactionSummaryMessageComponent } from "./components/compaction-summa
|
|
|
43
43
|
import { CustomEditor } from "./components/custom-editor";
|
|
44
44
|
import { DynamicBorder } from "./components/dynamic-border";
|
|
45
45
|
import { ExtensionDashboard } from "./components/extensions";
|
|
46
|
-
import { FooterComponent } from "./components/footer";
|
|
47
46
|
import { HookEditorComponent } from "./components/hook-editor";
|
|
48
47
|
import { HookInputComponent } from "./components/hook-input";
|
|
49
48
|
import { HookMessageComponent } from "./components/hook-message";
|
|
@@ -52,6 +51,7 @@ import { ModelSelectorComponent } from "./components/model-selector";
|
|
|
52
51
|
import { OAuthSelectorComponent } from "./components/oauth-selector";
|
|
53
52
|
import { SessionSelectorComponent } from "./components/session-selector";
|
|
54
53
|
import { SettingsSelectorComponent } from "./components/settings-selector";
|
|
54
|
+
import { StatusLineComponent } from "./components/status-line";
|
|
55
55
|
import { ToolExecutionComponent } from "./components/tool-execution";
|
|
56
56
|
import { TreeSelectorComponent } from "./components/tree-selector";
|
|
57
57
|
import { TtsrNotificationComponent } from "./components/ttsr-notification";
|
|
@@ -85,7 +85,7 @@ export class InteractiveMode {
|
|
|
85
85
|
private statusContainer: Container;
|
|
86
86
|
private editor: CustomEditor;
|
|
87
87
|
private editorContainer: Container;
|
|
88
|
-
private
|
|
88
|
+
private statusLine: StatusLineComponent;
|
|
89
89
|
private version: string;
|
|
90
90
|
private isInitialized = false;
|
|
91
91
|
private onInputCallback?: (input: { text: string; images?: ImageContent[] }) => void;
|
|
@@ -143,9 +143,6 @@ export class InteractiveMode {
|
|
|
143
143
|
// Custom tools for custom rendering
|
|
144
144
|
private customTools: Map<string, LoadedCustomTool>;
|
|
145
145
|
|
|
146
|
-
// Title generation state
|
|
147
|
-
private titleGenerationAttempted = false;
|
|
148
|
-
|
|
149
146
|
// Convenience accessors
|
|
150
147
|
private get agent() {
|
|
151
148
|
return this.session.agent;
|
|
@@ -179,8 +176,8 @@ export class InteractiveMode {
|
|
|
179
176
|
this.editor = new CustomEditor(getEditorTheme());
|
|
180
177
|
this.editorContainer = new Container();
|
|
181
178
|
this.editorContainer.addChild(this.editor);
|
|
182
|
-
this.
|
|
183
|
-
this.
|
|
179
|
+
this.statusLine = new StatusLineComponent(session);
|
|
180
|
+
this.statusLine.setAutoCompactEnabled(session.autoCompactionEnabled);
|
|
184
181
|
|
|
185
182
|
// Define slash commands for autocomplete
|
|
186
183
|
const slashCommands: SlashCommand[] = [
|
|
@@ -261,7 +258,6 @@ export class InteractiveMode {
|
|
|
261
258
|
const existingTitle = this.sessionManager.getSessionTitle();
|
|
262
259
|
if (existingTitle) {
|
|
263
260
|
setTerminalTitle(`pi: ${existingTitle}`);
|
|
264
|
-
this.titleGenerationAttempted = true; // Don't try to generate again
|
|
265
261
|
}
|
|
266
262
|
|
|
267
263
|
// Setup UI layout
|
|
@@ -291,7 +287,7 @@ export class InteractiveMode {
|
|
|
291
287
|
this.ui.addChild(this.statusContainer);
|
|
292
288
|
this.ui.addChild(new Spacer(1));
|
|
293
289
|
this.ui.addChild(this.editorContainer);
|
|
294
|
-
this.ui.addChild(this.
|
|
290
|
+
this.ui.addChild(this.statusLine); // Only renders hook statuses (main status in editor border)
|
|
295
291
|
this.ui.setFocus(this.editor);
|
|
296
292
|
|
|
297
293
|
this.setupKeyHandlers();
|
|
@@ -315,9 +311,13 @@ export class InteractiveMode {
|
|
|
315
311
|
});
|
|
316
312
|
|
|
317
313
|
// Set up git branch watcher
|
|
318
|
-
this.
|
|
314
|
+
this.statusLine.watchBranch(() => {
|
|
315
|
+
this.updateEditorTopBorder();
|
|
319
316
|
this.ui.requestRender();
|
|
320
317
|
});
|
|
318
|
+
|
|
319
|
+
// Initial top border update
|
|
320
|
+
this.updateEditorTopBorder();
|
|
321
321
|
}
|
|
322
322
|
|
|
323
323
|
// =========================================================================
|
|
@@ -401,7 +401,6 @@ export class InteractiveMode {
|
|
|
401
401
|
this.streamingComponent = undefined;
|
|
402
402
|
this.streamingMessage = undefined;
|
|
403
403
|
this.pendingTools.clear();
|
|
404
|
-
this.titleGenerationAttempted = false;
|
|
405
404
|
|
|
406
405
|
this.chatContainer.addChild(new Spacer(1));
|
|
407
406
|
this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
|
|
@@ -497,7 +496,7 @@ export class InteractiveMode {
|
|
|
497
496
|
* Set hook status text in the footer.
|
|
498
497
|
*/
|
|
499
498
|
private setHookStatus(key: string, text: string | undefined): void {
|
|
500
|
-
this.
|
|
499
|
+
this.statusLine.setHookStatus(key, text);
|
|
501
500
|
this.ui.requestRender();
|
|
502
501
|
}
|
|
503
502
|
|
|
@@ -903,6 +902,21 @@ export class InteractiveMode {
|
|
|
903
902
|
// First, move any pending bash components to chat
|
|
904
903
|
this.flushPendingBashComponents();
|
|
905
904
|
|
|
905
|
+
// Generate session title on first message
|
|
906
|
+
const hasUserMessages = this.agent.state.messages.some((m) => m.role === "user");
|
|
907
|
+
if (!hasUserMessages && !this.sessionManager.getSessionTitle()) {
|
|
908
|
+
const registry = this.session.modelRegistry;
|
|
909
|
+
const smolModel = this.settingsManager.getModelRole("smol");
|
|
910
|
+
generateSessionTitle(text, registry, smolModel)
|
|
911
|
+
.then((title) => {
|
|
912
|
+
if (title) {
|
|
913
|
+
this.sessionManager.setSessionTitle(title);
|
|
914
|
+
setTerminalTitle(`omp: ${title}`);
|
|
915
|
+
}
|
|
916
|
+
})
|
|
917
|
+
.catch(() => {});
|
|
918
|
+
}
|
|
919
|
+
|
|
906
920
|
if (this.onInputCallback) {
|
|
907
921
|
// Include any pending images from clipboard paste
|
|
908
922
|
const images = this.pendingImages.length > 0 ? [...this.pendingImages] : undefined;
|
|
@@ -924,7 +938,8 @@ export class InteractiveMode {
|
|
|
924
938
|
await this.init();
|
|
925
939
|
}
|
|
926
940
|
|
|
927
|
-
this.
|
|
941
|
+
this.statusLine.invalidate();
|
|
942
|
+
this.updateEditorTopBorder();
|
|
928
943
|
|
|
929
944
|
switch (event.type) {
|
|
930
945
|
case "agent_start":
|
|
@@ -1029,7 +1044,8 @@ export class InteractiveMode {
|
|
|
1029
1044
|
}
|
|
1030
1045
|
this.streamingComponent = undefined;
|
|
1031
1046
|
this.streamingMessage = undefined;
|
|
1032
|
-
this.
|
|
1047
|
+
this.statusLine.invalidate();
|
|
1048
|
+
this.updateEditorTopBorder();
|
|
1033
1049
|
}
|
|
1034
1050
|
this.ui.requestRender();
|
|
1035
1051
|
break;
|
|
@@ -1085,12 +1101,6 @@ export class InteractiveMode {
|
|
|
1085
1101
|
}
|
|
1086
1102
|
this.pendingTools.clear();
|
|
1087
1103
|
this.ui.requestRender();
|
|
1088
|
-
|
|
1089
|
-
// Generate session title after first turn (if not already titled)
|
|
1090
|
-
if (!this.titleGenerationAttempted && !this.sessionManager.getSessionTitle()) {
|
|
1091
|
-
this.titleGenerationAttempted = true;
|
|
1092
|
-
this.maybeGenerateTitle();
|
|
1093
|
-
}
|
|
1094
1104
|
break;
|
|
1095
1105
|
|
|
1096
1106
|
case "auto_compaction_start": {
|
|
@@ -1143,7 +1153,8 @@ export class InteractiveMode {
|
|
|
1143
1153
|
summary: event.result.summary,
|
|
1144
1154
|
timestamp: Date.now(),
|
|
1145
1155
|
});
|
|
1146
|
-
this.
|
|
1156
|
+
this.statusLine.invalidate();
|
|
1157
|
+
this.updateEditorTopBorder();
|
|
1147
1158
|
}
|
|
1148
1159
|
this.ui.requestRender();
|
|
1149
1160
|
break;
|
|
@@ -1320,7 +1331,7 @@ export class InteractiveMode {
|
|
|
1320
1331
|
this.pendingTools.clear();
|
|
1321
1332
|
|
|
1322
1333
|
if (options.updateFooter) {
|
|
1323
|
-
this.
|
|
1334
|
+
this.statusLine.invalidate();
|
|
1324
1335
|
this.updateEditorBorderColor();
|
|
1325
1336
|
}
|
|
1326
1337
|
|
|
@@ -1488,17 +1499,24 @@ export class InteractiveMode {
|
|
|
1488
1499
|
const level = this.session.thinkingLevel || "off";
|
|
1489
1500
|
this.editor.borderColor = theme.getThinkingBorderColor(level);
|
|
1490
1501
|
}
|
|
1502
|
+
// Update footer content in editor's top border
|
|
1503
|
+
this.updateEditorTopBorder();
|
|
1491
1504
|
this.ui.requestRender();
|
|
1492
1505
|
}
|
|
1493
1506
|
|
|
1507
|
+
private updateEditorTopBorder(): void {
|
|
1508
|
+
const width = this.ui.getWidth();
|
|
1509
|
+
const topBorder = this.statusLine.getTopBorder(width);
|
|
1510
|
+
this.editor.setTopBorder(topBorder);
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1494
1513
|
private cycleThinkingLevel(): void {
|
|
1495
1514
|
const newLevel = this.session.cycleThinkingLevel();
|
|
1496
1515
|
if (newLevel === undefined) {
|
|
1497
1516
|
this.showStatus("Current model does not support thinking");
|
|
1498
1517
|
} else {
|
|
1499
|
-
this.
|
|
1518
|
+
this.statusLine.invalidate();
|
|
1500
1519
|
this.updateEditorBorderColor();
|
|
1501
|
-
this.showStatus(`Thinking level: ${newLevel}`);
|
|
1502
1520
|
}
|
|
1503
1521
|
}
|
|
1504
1522
|
|
|
@@ -1509,7 +1527,7 @@ export class InteractiveMode {
|
|
|
1509
1527
|
const msg = this.session.scopedModels.length > 0 ? "Only one model in scope" : "Only one model available";
|
|
1510
1528
|
this.showStatus(msg);
|
|
1511
1529
|
} else {
|
|
1512
|
-
this.
|
|
1530
|
+
this.statusLine.invalidate();
|
|
1513
1531
|
this.updateEditorBorderColor();
|
|
1514
1532
|
const thinkingStr =
|
|
1515
1533
|
result.model.reasoning && result.thinkingLevel !== "off" ? ` (thinking: ${result.thinkingLevel})` : "";
|
|
@@ -1635,42 +1653,6 @@ export class InteractiveMode {
|
|
|
1635
1653
|
this.ui.requestRender();
|
|
1636
1654
|
}
|
|
1637
1655
|
|
|
1638
|
-
/**
|
|
1639
|
-
* Generate a title for the session based on the first user message.
|
|
1640
|
-
* Runs in background, doesn't block UI.
|
|
1641
|
-
*/
|
|
1642
|
-
private maybeGenerateTitle(): void {
|
|
1643
|
-
// Find the first user message
|
|
1644
|
-
const messages = this.agent.state.messages;
|
|
1645
|
-
const firstUserMessage = messages.find((m) => m.role === "user");
|
|
1646
|
-
if (!firstUserMessage) return;
|
|
1647
|
-
|
|
1648
|
-
// Extract text content
|
|
1649
|
-
let messageText = "";
|
|
1650
|
-
for (const content of firstUserMessage.content) {
|
|
1651
|
-
if (typeof content === "string") {
|
|
1652
|
-
messageText += content;
|
|
1653
|
-
} else if (content.type === "text") {
|
|
1654
|
-
messageText += content.text;
|
|
1655
|
-
}
|
|
1656
|
-
}
|
|
1657
|
-
if (!messageText.trim()) return;
|
|
1658
|
-
|
|
1659
|
-
// Generate title in background
|
|
1660
|
-
const registry = this.session.modelRegistry;
|
|
1661
|
-
const smolModel = this.settingsManager.getModelRole("smol");
|
|
1662
|
-
generateSessionTitle(messageText, registry, smolModel)
|
|
1663
|
-
.then((title) => {
|
|
1664
|
-
if (title) {
|
|
1665
|
-
this.sessionManager.setSessionTitle(title);
|
|
1666
|
-
setTerminalTitle(`omp: ${title}`);
|
|
1667
|
-
}
|
|
1668
|
-
})
|
|
1669
|
-
.catch(() => {
|
|
1670
|
-
// Errors logged via logger in title-generator
|
|
1671
|
-
});
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
1656
|
private updatePendingMessagesDisplay(): void {
|
|
1675
1657
|
this.pendingMessagesContainer.clear();
|
|
1676
1658
|
const queuedMessages = this.session.getQueuedMessages();
|
|
@@ -1781,7 +1763,7 @@ export class InteractiveMode {
|
|
|
1781
1763
|
// Session-managed settings (not in SettingsManager)
|
|
1782
1764
|
case "autoCompact":
|
|
1783
1765
|
this.session.setAutoCompactionEnabled(value as boolean);
|
|
1784
|
-
this.
|
|
1766
|
+
this.statusLine.setAutoCompactEnabled(value as boolean);
|
|
1785
1767
|
break;
|
|
1786
1768
|
case "queueMode":
|
|
1787
1769
|
this.session.setQueueMode(value as "all" | "one-at-a-time");
|
|
@@ -1791,7 +1773,7 @@ export class InteractiveMode {
|
|
|
1791
1773
|
break;
|
|
1792
1774
|
case "thinkingLevel":
|
|
1793
1775
|
this.session.setThinkingLevel(value as ThinkingLevel);
|
|
1794
|
-
this.
|
|
1776
|
+
this.statusLine.invalidate();
|
|
1795
1777
|
this.updateEditorBorderColor();
|
|
1796
1778
|
break;
|
|
1797
1779
|
|
|
@@ -1840,7 +1822,7 @@ export class InteractiveMode {
|
|
|
1840
1822
|
// Only update agent state for default role
|
|
1841
1823
|
if (role === "default") {
|
|
1842
1824
|
await this.session.setModel(model, role);
|
|
1843
|
-
this.
|
|
1825
|
+
this.statusLine.invalidate();
|
|
1844
1826
|
this.updateEditorBorderColor();
|
|
1845
1827
|
}
|
|
1846
1828
|
// For other roles (small), just show status - settings already updated by selector
|
|
@@ -2076,13 +2058,7 @@ export class InteractiveMode {
|
|
|
2076
2058
|
}
|
|
2077
2059
|
this.ui.requestRender();
|
|
2078
2060
|
|
|
2079
|
-
|
|
2080
|
-
process.platform === "darwin"
|
|
2081
|
-
? "open"
|
|
2082
|
-
: process.platform === "win32"
|
|
2083
|
-
? "start"
|
|
2084
|
-
: "xdg-open";
|
|
2085
|
-
Bun.spawn([openCmd, info.url], { stdin: "ignore", stdout: "ignore", stderr: "ignore" });
|
|
2061
|
+
this.openInBrowser(info.url);
|
|
2086
2062
|
},
|
|
2087
2063
|
onPrompt: async (prompt: { message: string; placeholder?: string }) => {
|
|
2088
2064
|
this.chatContainer.addChild(new Spacer(1));
|
|
@@ -2156,6 +2132,11 @@ export class InteractiveMode {
|
|
|
2156
2132
|
// Command handlers
|
|
2157
2133
|
// =========================================================================
|
|
2158
2134
|
|
|
2135
|
+
private openInBrowser(urlOrPath: string): void {
|
|
2136
|
+
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
2137
|
+
Bun.spawn([openCmd, urlOrPath], { stdin: "ignore", stdout: "ignore", stderr: "ignore" });
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2159
2140
|
private async handleExportCommand(text: string): Promise<void> {
|
|
2160
2141
|
const parts = text.split(/\s+/);
|
|
2161
2142
|
const arg = parts.length > 1 ? parts[1] : undefined;
|
|
@@ -2180,6 +2161,7 @@ export class InteractiveMode {
|
|
|
2180
2161
|
try {
|
|
2181
2162
|
const filePath = await this.session.exportToHtml(arg);
|
|
2182
2163
|
this.showStatus(`Session exported to: ${filePath}`);
|
|
2164
|
+
this.openInBrowser(filePath);
|
|
2183
2165
|
} catch (error: unknown) {
|
|
2184
2166
|
this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2185
2167
|
}
|
|
@@ -2293,6 +2275,7 @@ export class InteractiveMode {
|
|
|
2293
2275
|
// Create the preview URL
|
|
2294
2276
|
const previewUrl = `https://gistpreview.github.io/?${gistId}`;
|
|
2295
2277
|
this.showStatus(`Share URL: ${previewUrl}\nGist: ${gistUrl}`);
|
|
2278
|
+
this.openInBrowser(previewUrl);
|
|
2296
2279
|
} catch (error: unknown) {
|
|
2297
2280
|
if (!loader.signal.aborted) {
|
|
2298
2281
|
restoreEditor();
|
|
@@ -2564,7 +2547,8 @@ export class InteractiveMode {
|
|
|
2564
2547
|
const msg = createCompactionSummaryMessage(result.summary, result.tokensBefore, new Date().toISOString());
|
|
2565
2548
|
this.addMessageToChat(msg);
|
|
2566
2549
|
|
|
2567
|
-
this.
|
|
2550
|
+
this.statusLine.invalidate();
|
|
2551
|
+
this.updateEditorTopBorder();
|
|
2568
2552
|
} catch (error) {
|
|
2569
2553
|
const message = error instanceof Error ? error.message : String(error);
|
|
2570
2554
|
if (message === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError")) {
|
|
@@ -2584,7 +2568,7 @@ export class InteractiveMode {
|
|
|
2584
2568
|
this.loadingAnimation.stop();
|
|
2585
2569
|
this.loadingAnimation = undefined;
|
|
2586
2570
|
}
|
|
2587
|
-
this.
|
|
2571
|
+
this.statusLine.dispose();
|
|
2588
2572
|
if (this.unsubscribe) {
|
|
2589
2573
|
this.unsubscribe();
|
|
2590
2574
|
}
|