@thegitai/cli 1.0.0-preview.1 → 1.0.0-preview.11
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/README.md +16 -4
- package/dist/bin/ai.js +57 -287
- package/dist/src/api/auth.js +2 -2
- package/dist/src/api/browser-login.js +72 -3
- package/dist/src/api/chat.js +147 -30
- package/dist/src/api/http.js +16 -3
- package/dist/src/api/models.js +9 -4
- package/dist/src/help-text.js +19 -7
- package/dist/src/patcher.js +96 -9
- package/dist/src/project-index.js +13 -1
- package/dist/src/project-orientation.js +99 -0
- package/dist/src/scratch-dir.js +51 -33
- package/dist/src/session-store.js +52 -20
- package/dist/src/session.js +8 -0
- package/dist/src/tool-executor.js +38 -6
- package/dist/src/tools/delete-file.js +22 -4
- package/dist/src/tools/patch-file.js +30 -5
- package/dist/src/tools/read-file.js +3 -1
- package/dist/src/tools/replace-document-text.js +7 -1
- package/dist/src/tools/run-command.js +37 -19
- package/dist/src/tools/run-node-script.js +24 -4
- package/dist/src/tools/str-replace.js +30 -5
- package/dist/src/tools/write-file.js +25 -5
- package/dist/src/turn-failure-marker.js +11 -0
- package/dist/src/ui/prompt-history-store.js +1 -1
- package/dist/src/ui/repl.js +197 -51
- package/dist/src/ui/tui/bridge.js +3 -0
- package/dist/src/ui/tui/build-frame.js +179 -82
- package/dist/src/ui/tui/markdown-render.js +72 -73
- package/dist/src/ui/tui/shell-input.js +42 -13
- package/dist/src/ui/tui/terminal-title.js +3 -0
- package/dist/src/ui/tui/terminal-writes.js +48 -0
- package/dist/src/ui/tui/text.js +158 -4
- package/dist/src/utils.js +9 -0
- package/package.json +18 -6
- package/dist/src/markdown-renderer.js +0 -112
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { agentModeLabel } from '../../agent-mode.js';
|
|
2
|
-
import { truncate } from '../../utils.js';
|
|
2
|
+
import { singleLinePreview, truncate } from '../../utils.js';
|
|
3
3
|
import { formatClientTokenUsage } from '../repl.js';
|
|
4
4
|
import { renderFormattedBodyLines, renderPreformattedBodyLines, } from './markdown-render.js';
|
|
5
|
-
import { line, plainLine, span, wrapText } from './text.js';
|
|
5
|
+
import { displayWidth, line, plainLine, sliceToWidth, span, wrapText, } from './text.js';
|
|
6
6
|
const WORKING_CLOCK_ICON = '◷';
|
|
7
7
|
const BRAILLE_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴'];
|
|
8
8
|
const TODO_PANEL_MAX_ROWS = 12;
|
|
@@ -17,9 +17,9 @@ const OVERLAY_PANEL_MAX_WIDTH = 86;
|
|
|
17
17
|
const OVERLAY_PANEL_MARGIN_LINES = 2;
|
|
18
18
|
const OVERLAY_BORDER_COLOR = 'yellow';
|
|
19
19
|
const OVERLAY_WARNING_COLOR = 'ansi256(208)';
|
|
20
|
-
const MODEL_PICKER_PANEL_MAX_WIDTH =
|
|
20
|
+
const MODEL_PICKER_PANEL_MAX_WIDTH = 144;
|
|
21
21
|
const MODEL_PICKER_PANEL_MARGIN_LINES = 2;
|
|
22
|
-
const MODEL_PICKER_BORDER_COLOR = '
|
|
22
|
+
const MODEL_PICKER_BORDER_COLOR = 'cyan';
|
|
23
23
|
const MODEL_PICKER_ACCENT_COLOR = 'cyan';
|
|
24
24
|
const MODEL_PICKER_HIGHLIGHT_BG = 'ansi256(87)';
|
|
25
25
|
const MODEL_PICKER_META_INDENT = ' ';
|
|
@@ -133,14 +133,14 @@ function diffLinePrefix(kind) {
|
|
|
133
133
|
return ' ';
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
|
-
function fitLine(content, maxWidth) {
|
|
136
|
+
export function fitLine(content, maxWidth) {
|
|
137
137
|
if (maxWidth <= 0)
|
|
138
138
|
return '';
|
|
139
|
-
if (content
|
|
139
|
+
if (displayWidth(content) <= maxWidth)
|
|
140
140
|
return content;
|
|
141
141
|
if (maxWidth === 1)
|
|
142
142
|
return '…';
|
|
143
|
-
return `${content
|
|
143
|
+
return `${sliceToWidth(content, maxWidth - 1)}…`;
|
|
144
144
|
}
|
|
145
145
|
export function formatPromptDirectoryLabel(projectRoot, homeDir = process.env.HOME ?? '') {
|
|
146
146
|
const trimmed = String(projectRoot ?? '').trim();
|
|
@@ -163,15 +163,18 @@ const CLIENT_SLASH_COMMANDS = [
|
|
|
163
163
|
{ command: '/model', description: 'Switch the active model' },
|
|
164
164
|
{ command: '/resume', description: 'Open the session picker to resume a previous session' },
|
|
165
165
|
{ command: '/jobs', description: 'Background jobs: pick to view output or kill' },
|
|
166
|
-
{ command: '/
|
|
166
|
+
{ command: '/new', description: 'start a new conversation; this session remains saved' },
|
|
167
167
|
{ command: '/exit', description: 'Quit the current session' },
|
|
168
168
|
];
|
|
169
169
|
function buildModelPickerOptions(currentModelId, serverModels) {
|
|
170
170
|
return serverModels.map((model) => ({
|
|
171
171
|
id: model.id,
|
|
172
172
|
label: model.label,
|
|
173
|
-
|
|
173
|
+
publicId: model.id,
|
|
174
|
+
costRating: model.costRating,
|
|
175
|
+
current: model.id === currentModelId,
|
|
174
176
|
disabled: false,
|
|
177
|
+
note: model.description,
|
|
175
178
|
}));
|
|
176
179
|
}
|
|
177
180
|
function getInputCommandToken(input) {
|
|
@@ -209,6 +212,9 @@ export function getSlashCommandSuggestions(input) {
|
|
|
209
212
|
function formatModelLabel(modelId, serverModels) {
|
|
210
213
|
return serverModels.find((model) => model.id === modelId)?.label ?? 'Unknown model';
|
|
211
214
|
}
|
|
215
|
+
function pickerModelLabel(modelId, serverModels) {
|
|
216
|
+
return formatModelLabel(modelId, serverModels).replace(/\s*\([^)]*\)\s*$/, '');
|
|
217
|
+
}
|
|
212
218
|
function filterResumeSessions(sessions, filter, serverModels) {
|
|
213
219
|
const q = filter.trim().toLowerCase();
|
|
214
220
|
if (!q)
|
|
@@ -402,15 +408,18 @@ function composerFooterLines(state) {
|
|
|
402
408
|
}));
|
|
403
409
|
return lines;
|
|
404
410
|
}
|
|
411
|
+
const busyHelperText = state.queuedMessage
|
|
412
|
+
? 'Enter re-queues • ↑ edit queued • Esc / Ctrl+C clear queued'
|
|
413
|
+
: state.input
|
|
414
|
+
? 'Enter queues • Esc cancels turn • Ctrl+C clears draft'
|
|
415
|
+
: 'Enter queues • Esc / Ctrl+C cancel turn';
|
|
405
416
|
const helperText = state.busy
|
|
406
|
-
?
|
|
407
|
-
? 'Enter re-queues • ↑ edit queued • Esc cancels queued'
|
|
408
|
-
: 'Enter queues • Esc / Ctrl+C cancel turn'
|
|
417
|
+
? busyHelperText
|
|
409
418
|
: process.platform === 'win32'
|
|
410
|
-
? 'Enter sends • Shift+Tab mode • Alt+V image • Esc cancel turn • Ctrl+C quits'
|
|
419
|
+
? 'Enter sends • Shift+Tab mode • Alt+V image • Esc cancel turn • Ctrl+C clears / quits'
|
|
411
420
|
: process.platform === 'darwin'
|
|
412
|
-
? 'Enter sends • Shift+Tab mode • Ctrl+V image • Esc cancel turn • Ctrl+C quits'
|
|
413
|
-
: 'Enter sends • Shift+Tab mode • Ctrl+V image • Esc cancel turn • Ctrl+C quits';
|
|
421
|
+
? 'Enter sends • Shift+Tab mode • Ctrl+V image • Esc cancel turn • Ctrl+C clears / quits'
|
|
422
|
+
: 'Enter sends • Shift+Tab mode • Ctrl+V image • Esc cancel turn • Ctrl+C clears / quits';
|
|
414
423
|
const agentLabel = agentModeLabel(state.agentMode).padEnd(AGENT_MODE_LABEL_WIDTH);
|
|
415
424
|
const tokenUsageText = state.tokenUsage || formatClientTokenUsage(null);
|
|
416
425
|
const footerSpans = [
|
|
@@ -543,7 +552,12 @@ function buildLiveLines(state, width, spinnerFrame, elapsedSeconds) {
|
|
|
543
552
|
}
|
|
544
553
|
lines.push(plainLine(''));
|
|
545
554
|
}
|
|
546
|
-
|
|
555
|
+
const busyLabel = state.analyzingImages > 0
|
|
556
|
+
? state.analyzingImages > 1
|
|
557
|
+
? 'Analyzing images'
|
|
558
|
+
: 'Analyzing image'
|
|
559
|
+
: 'Working';
|
|
560
|
+
lines.push(plainLine(`${WORKING_CLOCK_ICON} ${busyLabel} · ${elapsedSeconds < 60 ? `${elapsedSeconds}s` : `${Math.floor(elapsedSeconds / 60)}m ${String(elapsedSeconds % 60).padStart(2, '0')}s`}`, { color: 'yellow' }));
|
|
547
561
|
const todos = state.todos ?? [];
|
|
548
562
|
if (todos.length > 0) {
|
|
549
563
|
lines.push(plainLine(''));
|
|
@@ -570,7 +584,7 @@ function buildLiveLines(state, width, spinnerFrame, elapsedSeconds) {
|
|
|
570
584
|
return lines;
|
|
571
585
|
}
|
|
572
586
|
function lineCharCount(row) {
|
|
573
|
-
return row.spans.reduce((total, item) => total +
|
|
587
|
+
return row.spans.reduce((total, item) => total + displayWidth(item.text), 0);
|
|
574
588
|
}
|
|
575
589
|
function overlayPanelLine(row, width, color) {
|
|
576
590
|
const padding = Math.max(0, width - lineCharCount(row));
|
|
@@ -598,77 +612,118 @@ function padSpansToInnerWidth(spans, innerWidth, fill) {
|
|
|
598
612
|
function modelPickerPanelSideLine(content, innerWidth) {
|
|
599
613
|
return overlayPanelLine(content, innerWidth, MODEL_PICKER_BORDER_COLOR);
|
|
600
614
|
}
|
|
615
|
+
const MODEL_PICKER_COST_WIDTH = 6;
|
|
616
|
+
const MODEL_PICKER_MODEL_WIDTH = 42;
|
|
617
|
+
const MODEL_PICKER_NUMBER_WIDTH = 4;
|
|
618
|
+
const MODEL_PICKER_SEPARATOR = ' │ ';
|
|
619
|
+
const MODEL_PICKER_WIDE_MIN_WIDTH = 78;
|
|
601
620
|
function modelPickerTopBorder(panelWidth) {
|
|
602
|
-
const
|
|
603
|
-
const
|
|
604
|
-
const
|
|
605
|
-
|
|
621
|
+
const fullTitle = ' TheGitAI - Model Selection ';
|
|
622
|
+
const compactTitle = ' Model Selection ';
|
|
623
|
+
const title = panelWidth >= fullTitle.length + 4 ? fullTitle : compactTitle;
|
|
624
|
+
const available = Math.max(0, panelWidth - 2 - [...title].length);
|
|
625
|
+
const left = Math.floor(available / 2);
|
|
626
|
+
const right = available - left;
|
|
627
|
+
return line(span(`╭${'─'.repeat(left)}`, { color: MODEL_PICKER_BORDER_COLOR }), span(title, { color: MODEL_PICKER_ACCENT_COLOR, bold: true }), span(`${'─'.repeat(right)}╮`, { color: MODEL_PICKER_BORDER_COLOR }));
|
|
628
|
+
}
|
|
629
|
+
function modelPickerDivider(panelWidth) {
|
|
630
|
+
return plainLine(`├${'─'.repeat(panelWidth - 2)}┤`, {
|
|
631
|
+
color: MODEL_PICKER_BORDER_COLOR,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
function modelPickerCell(text, width, style = {}) {
|
|
635
|
+
const fitted = fitLine(text, width);
|
|
636
|
+
return span(`${fitted}${' '.repeat(Math.max(0, width - [...fitted].length))}`, style);
|
|
637
|
+
}
|
|
638
|
+
function modelPickerCostText(rating) {
|
|
639
|
+
const steps = Math.max(1, Math.min(3, Math.round(rating)));
|
|
640
|
+
return '$'.repeat(steps);
|
|
641
|
+
}
|
|
642
|
+
function modelPickerNotesWidth(innerWidth) {
|
|
643
|
+
return Math.max(18, innerWidth -
|
|
644
|
+
MODEL_PICKER_NUMBER_WIDTH -
|
|
645
|
+
MODEL_PICKER_MODEL_WIDTH -
|
|
646
|
+
MODEL_PICKER_COST_WIDTH -
|
|
647
|
+
MODEL_PICKER_SEPARATOR.length * 2);
|
|
648
|
+
}
|
|
649
|
+
function modelPickerModelSpans(option, selected, width, showCurrentTag, labelStyle, selectedStyle) {
|
|
650
|
+
const tag = option.current && showCurrentTag ? ' (current)' : '';
|
|
651
|
+
const labelWidth = Math.max(1, width - [...tag].length);
|
|
652
|
+
const label = fitLine(`${selected ? '▶ ' : ' '}${option.label}`, labelWidth);
|
|
653
|
+
const used = [...label].length + [...tag].length;
|
|
654
|
+
return [
|
|
655
|
+
span(label, { ...labelStyle, ...selectedStyle }),
|
|
656
|
+
span(tag, { color: 'gray', ...selectedStyle }),
|
|
657
|
+
span(' '.repeat(Math.max(0, width - used)), selectedStyle),
|
|
658
|
+
];
|
|
606
659
|
}
|
|
607
660
|
function modelPickerItemLines(option, selected, innerWidth) {
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
span('
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
], innerWidth, highlight);
|
|
628
|
-
const lines = [modelPickerPanelSideLine(line(...titleSpans), innerWidth)];
|
|
629
|
-
if (option.meta) {
|
|
630
|
-
const metaSpans = padSpansToInnerWidth([
|
|
631
|
-
span(`${MODEL_PICKER_META_INDENT}${option.meta}`, {
|
|
632
|
-
color: 'gray',
|
|
633
|
-
...highlight,
|
|
634
|
-
}),
|
|
635
|
-
], innerWidth, highlight);
|
|
636
|
-
lines.push(modelPickerPanelSideLine(line(...metaSpans), innerWidth));
|
|
637
|
-
}
|
|
638
|
-
return lines;
|
|
661
|
+
const selectedStyle = selected ? { bgColor: MODEL_PICKER_HIGHLIGHT_BG } : {};
|
|
662
|
+
const labelStyle = option.disabled
|
|
663
|
+
? { color: 'gray' }
|
|
664
|
+
: selected
|
|
665
|
+
? { color: 'cyan', bold: true }
|
|
666
|
+
: {};
|
|
667
|
+
const numberCell = modelPickerCell(String(option.publicId), MODEL_PICKER_NUMBER_WIDTH, { color: 'cyan', bold: selected, ...selectedStyle });
|
|
668
|
+
const cost = modelPickerCostText(option.costRating);
|
|
669
|
+
if (innerWidth < MODEL_PICKER_WIDE_MIN_WIDTH) {
|
|
670
|
+
const modelWidth = Math.max(12, innerWidth - MODEL_PICKER_NUMBER_WIDTH - MODEL_PICKER_COST_WIDTH - 2);
|
|
671
|
+
const row = [
|
|
672
|
+
numberCell,
|
|
673
|
+
...modelPickerModelSpans(option, selected, modelWidth, false, labelStyle, selectedStyle),
|
|
674
|
+
span(' ', selectedStyle),
|
|
675
|
+
modelPickerCell(cost, MODEL_PICKER_COST_WIDTH, selectedStyle),
|
|
676
|
+
];
|
|
677
|
+
return [
|
|
678
|
+
modelPickerPanelSideLine(line(...padSpansToInnerWidth(row, innerWidth, selectedStyle)), innerWidth),
|
|
679
|
+
];
|
|
639
680
|
}
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
681
|
+
const row = [
|
|
682
|
+
numberCell,
|
|
683
|
+
...modelPickerModelSpans(option, selected, MODEL_PICKER_MODEL_WIDTH, true, labelStyle, selectedStyle),
|
|
684
|
+
span(MODEL_PICKER_SEPARATOR, { color: 'gray', ...selectedStyle }),
|
|
685
|
+
modelPickerCell(cost, MODEL_PICKER_COST_WIDTH, selectedStyle),
|
|
686
|
+
span(MODEL_PICKER_SEPARATOR, { color: 'gray', ...selectedStyle }),
|
|
687
|
+
modelPickerCell(option.note, modelPickerNotesWidth(innerWidth), {
|
|
688
|
+
color: 'gray',
|
|
689
|
+
...selectedStyle,
|
|
690
|
+
}),
|
|
691
|
+
];
|
|
692
|
+
return [
|
|
693
|
+
modelPickerPanelSideLine(line(...padSpansToInnerWidth(row, innerWidth, selectedStyle)), innerWidth),
|
|
643
694
|
];
|
|
644
|
-
if (option.meta) {
|
|
645
|
-
lines.push(modelPickerPanelSideLine(line(span(`${MODEL_PICKER_META_INDENT}${option.meta}`, { color: 'gray' })), innerWidth));
|
|
646
|
-
}
|
|
647
|
-
return lines;
|
|
648
695
|
}
|
|
649
|
-
function
|
|
650
|
-
|
|
696
|
+
function modelPickerHeaderLine(innerWidth) {
|
|
697
|
+
const heading = { color: MODEL_PICKER_ACCENT_COLOR, bold: true };
|
|
698
|
+
if (innerWidth < MODEL_PICKER_WIDE_MIN_WIDTH) {
|
|
699
|
+
return line(modelPickerCell('#', MODEL_PICKER_NUMBER_WIDTH, heading), modelPickerCell('Model', Math.max(1, innerWidth - MODEL_PICKER_NUMBER_WIDTH), heading));
|
|
700
|
+
}
|
|
701
|
+
return line(modelPickerCell('#', MODEL_PICKER_NUMBER_WIDTH, heading), modelPickerCell('Model', MODEL_PICKER_MODEL_WIDTH, heading), span(MODEL_PICKER_SEPARATOR, { color: 'gray' }), modelPickerCell('Cost', MODEL_PICKER_COST_WIDTH, heading), span(MODEL_PICKER_SEPARATOR, { color: 'gray' }), modelPickerCell('Notes', modelPickerNotesWidth(innerWidth), heading));
|
|
651
702
|
}
|
|
652
|
-
function buildModelPickerPanel(options, selectedIndex, width) {
|
|
703
|
+
function buildModelPickerPanel(options, selectedIndex, width, availableHeight) {
|
|
653
704
|
const panelWidth = Math.max(28, Math.min(width, MODEL_PICKER_PANEL_MAX_WIDTH));
|
|
654
705
|
const innerWidth = Math.max(1, panelWidth - 4);
|
|
655
|
-
const
|
|
706
|
+
const compactBodyLineCount = options.length + 6;
|
|
707
|
+
const spacerLineCount = Math.max(0, options.length - 1);
|
|
708
|
+
const useRowSpacing = compactBodyLineCount + spacerLineCount <= availableHeight;
|
|
709
|
+
const bodyLineCount = compactBodyLineCount + (useRowSpacing ? spacerLineCount : 0);
|
|
710
|
+
const marginLineCount = Math.max(0, Math.min(MODEL_PICKER_PANEL_MARGIN_LINES, Math.floor((availableHeight - bodyLineCount) / 2)));
|
|
711
|
+
const margin = Array.from({ length: marginLineCount }, () => plainLine(''));
|
|
656
712
|
const body = [
|
|
657
|
-
plainLine('TheGitAI - Model Selection', {
|
|
658
|
-
color: MODEL_PICKER_ACCENT_COLOR,
|
|
659
|
-
bold: true,
|
|
660
|
-
}),
|
|
661
|
-
plainLine(''),
|
|
662
713
|
modelPickerTopBorder(panelWidth),
|
|
663
|
-
modelPickerPanelSideLine(
|
|
714
|
+
modelPickerPanelSideLine(modelPickerHeaderLine(innerWidth), innerWidth),
|
|
715
|
+
modelPickerDivider(panelWidth),
|
|
664
716
|
];
|
|
665
717
|
options.forEach((option, index) => {
|
|
666
718
|
body.push(...modelPickerItemLines(option, index === selectedIndex, innerWidth));
|
|
667
|
-
if (index < options.length - 1) {
|
|
668
|
-
body.push(
|
|
719
|
+
if (useRowSpacing && index < options.length - 1) {
|
|
720
|
+
body.push(modelPickerPanelSideLine(plainLine(''), innerWidth));
|
|
669
721
|
}
|
|
670
722
|
});
|
|
671
|
-
|
|
723
|
+
const fullHint = '↑/↓ navigate • Enter select • Esc cancel';
|
|
724
|
+
const compactHint = '↑/↓ • enter • esc';
|
|
725
|
+
const hint = [...fullHint].length <= innerWidth ? fullHint : compactHint;
|
|
726
|
+
body.push(modelPickerDivider(panelWidth), modelPickerPanelSideLine(plainLine(fitLine(hint, innerWidth), { color: 'gray' }), innerWidth), plainLine(`╰${'─'.repeat(panelWidth - 2)}╯`, { color: MODEL_PICKER_BORDER_COLOR }));
|
|
672
727
|
return [...margin, ...body, ...margin];
|
|
673
728
|
}
|
|
674
729
|
function commandPaletteTopBorder(panelWidth) {
|
|
@@ -677,6 +732,9 @@ function commandPaletteTopBorder(panelWidth) {
|
|
|
677
732
|
const dashCount = Math.max(0, panelWidth - prefix.length - suffix.length);
|
|
678
733
|
return line(span('╭─ ', { color: MODEL_PICKER_BORDER_COLOR }), span('Commands', { color: MODEL_PICKER_ACCENT_COLOR, bold: true }), span(` ${'─'.repeat(dashCount)}╮`, { color: MODEL_PICKER_BORDER_COLOR }));
|
|
679
734
|
}
|
|
735
|
+
function commandPaletteSeparatorLine(innerWidth) {
|
|
736
|
+
return modelPickerPanelSideLine(line(span('┈'.repeat(Math.max(1, innerWidth)), { color: 'gray', dim: true })), innerWidth);
|
|
737
|
+
}
|
|
680
738
|
function commandPaletteItemLines(option, selected, innerWidth) {
|
|
681
739
|
const highlight = { bgColor: MODEL_PICKER_HIGHLIGHT_BG };
|
|
682
740
|
if (selected) {
|
|
@@ -729,13 +787,13 @@ function buildCommandPalettePanel(suggestions, selectedIndex, width) {
|
|
|
729
787
|
suggestions.forEach((suggestion, index) => {
|
|
730
788
|
body.push(...commandPaletteItemLines(suggestion, index === selectedIndex, innerWidth));
|
|
731
789
|
if (index < suggestions.length - 1) {
|
|
732
|
-
body.push(
|
|
790
|
+
body.push(commandPaletteSeparatorLine(innerWidth));
|
|
733
791
|
}
|
|
734
792
|
});
|
|
735
793
|
body.push(modelPickerPanelSideLine(plainLine(''), innerWidth), modelPickerPanelSideLine(line(span('─'.repeat(innerWidth), { color: MODEL_PICKER_BORDER_COLOR })), innerWidth), modelPickerPanelSideLine(plainLine('↑/↓ choose • Tab or Enter accept • Esc cancel', { color: 'gray' }), innerWidth), plainLine(`╰${'─'.repeat(panelWidth - 2)}╯`, { color: MODEL_PICKER_BORDER_COLOR }));
|
|
736
794
|
return [...margin, ...body, ...margin];
|
|
737
795
|
}
|
|
738
|
-
function buildOverlayLines(state, width, nowMs) {
|
|
796
|
+
function buildOverlayLines(state, width, height, nowMs) {
|
|
739
797
|
const lines = [];
|
|
740
798
|
const panelWidth = Math.max(24, Math.min(width, OVERLAY_PANEL_MAX_WIDTH));
|
|
741
799
|
const innerWidth = Math.max(1, panelWidth - 4);
|
|
@@ -808,25 +866,61 @@ function buildOverlayLines(state, width, nowMs) {
|
|
|
808
866
|
}
|
|
809
867
|
if (state.modelPickerOpen) {
|
|
810
868
|
const options = buildModelPickerOptions(state.currentModelId, state.serverModels);
|
|
811
|
-
lines.push(...buildModelPickerPanel(options, state.modelPickerIndex, width));
|
|
869
|
+
lines.push(...buildModelPickerPanel(options, state.modelPickerIndex, width, height));
|
|
812
870
|
}
|
|
813
871
|
if (state.jobsPickerOpen) {
|
|
814
872
|
lines.push(...buildJobsPickerLines(state, width, nowMs));
|
|
815
873
|
}
|
|
816
874
|
if (state.resumePickerOpen) {
|
|
817
875
|
const filtered = filterResumeSessions(state.resumePickerSessions, state.resumePickerFilter, state.serverModels);
|
|
876
|
+
const pickerWidth = Math.max(20, width - 2);
|
|
877
|
+
const divider = () => plainLine('─'.repeat(pickerWidth), { color: 'gray', dim: true });
|
|
818
878
|
lines.push(plainLine('Resume a previous session', { color: 'cyan', bold: true }));
|
|
819
879
|
lines.push(line(span('Search: ', { color: 'gray' }), span(state.resumePickerFilter, {}), span('█', { color: 'gray' })));
|
|
820
|
-
|
|
880
|
+
lines.push(divider());
|
|
881
|
+
const maxCards = Math.max(2, Math.min(filtered.length, Math.floor((height - 10) / 3)));
|
|
882
|
+
let start = 0;
|
|
883
|
+
if (filtered.length > maxCards) {
|
|
884
|
+
start = Math.min(Math.max(0, state.resumePickerIndex - Math.floor(maxCards / 2)), filtered.length - maxCards);
|
|
885
|
+
}
|
|
886
|
+
const visible = filtered.slice(start, start + maxCards);
|
|
887
|
+
if (start > 0) {
|
|
888
|
+
lines.push(plainLine(` … ${start} newer`, { color: 'gray', dim: true }));
|
|
889
|
+
}
|
|
890
|
+
for (const [offset, session] of visible.entries()) {
|
|
891
|
+
const index = start + offset;
|
|
821
892
|
const selected = index === state.resumePickerIndex;
|
|
822
|
-
const
|
|
823
|
-
|
|
824
|
-
|
|
893
|
+
const prompt = singleLinePreview(session.lastUserMessage, pickerWidth) ||
|
|
894
|
+
session.name ||
|
|
895
|
+
`Session ${session.id}`;
|
|
896
|
+
lines.push(line(span(selected ? '› ' : ' ', { color: 'cyan' }), span(fitLine(prompt, Math.max(10, pickerWidth - 2)), selected
|
|
897
|
+
? { color: 'cyan', bold: true }
|
|
898
|
+
: session.lastUserMessage
|
|
899
|
+
? {}
|
|
900
|
+
: { color: 'gray', dim: true })));
|
|
901
|
+
const meta = [
|
|
902
|
+
formatRelativeTime(session.updatedAt),
|
|
903
|
+
pickerModelLabel(session.modelId, state.serverModels),
|
|
904
|
+
session.branch ?? null,
|
|
905
|
+
]
|
|
906
|
+
.filter(Boolean)
|
|
907
|
+
.join(' · ');
|
|
908
|
+
lines.push(line(span(' '), span(fitLine(meta, Math.max(10, pickerWidth - 4)), {
|
|
909
|
+
color: selected ? 'cyan' : 'gray',
|
|
910
|
+
dim: !selected,
|
|
911
|
+
})));
|
|
912
|
+
if (offset < visible.length - 1)
|
|
913
|
+
lines.push(plainLine(''));
|
|
914
|
+
}
|
|
915
|
+
const remaining = filtered.length - (start + visible.length);
|
|
916
|
+
if (remaining > 0) {
|
|
917
|
+
lines.push(plainLine(` … ${remaining} older`, { color: 'gray', dim: true }));
|
|
825
918
|
}
|
|
826
919
|
if (filtered.length === 0) {
|
|
827
920
|
lines.push(plainLine('No sessions match.', { color: 'gray' }));
|
|
828
921
|
}
|
|
829
|
-
lines.push(
|
|
922
|
+
lines.push(divider());
|
|
923
|
+
lines.push(plainLine('↑/↓ move · enter resume · esc start new · ctrl+c quit', {
|
|
830
924
|
color: 'gray',
|
|
831
925
|
}));
|
|
832
926
|
}
|
|
@@ -873,7 +967,10 @@ export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds, n
|
|
|
873
967
|
const overlayActive = Boolean(state.approvalPrompt || state.sudoPrompt);
|
|
874
968
|
if (!state.resumePickerOpen && !state.modelPickerOpen && !state.jobsPickerOpen && !overlayActive) {
|
|
875
969
|
const composerLines = [];
|
|
876
|
-
if (state.
|
|
970
|
+
if (state.busy && state.status === 'Starting a new conversation...') {
|
|
971
|
+
composerLines.push(line(span('Starting a new conversation…', { color: 'gray', dim: true })));
|
|
972
|
+
}
|
|
973
|
+
else if (state.queuedMessage) {
|
|
877
974
|
const preview = truncate(state.queuedMessage.body.trim().replace(/\s+/g, ' '), 60);
|
|
878
975
|
const imageCount = state.queuedMessage.imageAttachments.length;
|
|
879
976
|
composerLines.push(line(span(`↳ Queued · "${preview}"`, { color: 'gray', dim: true }), ...(imageCount > 0
|
|
@@ -892,7 +989,7 @@ export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds, n
|
|
|
892
989
|
lines: [...composerLines, plainLine(''), ...composerFooterLines(state)],
|
|
893
990
|
});
|
|
894
991
|
}
|
|
895
|
-
const overlayLines = buildOverlayLines(state, contentWidth, nowMs);
|
|
992
|
+
const overlayLines = buildOverlayLines(state, contentWidth, rows, nowMs);
|
|
896
993
|
if (overlayLines.length > 0) {
|
|
897
994
|
sections.push({ kind: 'overlay', lines: overlayLines });
|
|
898
995
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { line, plainLine, span, wrapText } from './text.js';
|
|
2
|
-
const
|
|
3
|
-
|
|
1
|
+
import { displayWidth, line, padToWidth, plainLine, sliceToWidth, span, wrapText, wrapToWidth, } from './text.js';
|
|
2
|
+
const MIN_TABLE_COLUMN_WIDTH = 3;
|
|
3
|
+
function tableRowOverhead(columnCount) {
|
|
4
|
+
return 3 * columnCount + 1;
|
|
5
|
+
}
|
|
4
6
|
function parseInlineSegments(text) {
|
|
5
7
|
const source = String(text ?? '');
|
|
6
8
|
const segments = [];
|
|
@@ -110,45 +112,37 @@ function stripInlineFormattingForWidth(text) {
|
|
|
110
112
|
.replace(/\*\*([^*]+)\*\*/g, '$1');
|
|
111
113
|
}
|
|
112
114
|
function fitTableColumnWidths(columnWidths, maxWidth) {
|
|
113
|
-
const
|
|
114
|
-
if (
|
|
115
|
-
return
|
|
116
|
-
const
|
|
117
|
-
const budget = Math.
|
|
118
|
-
const total =
|
|
119
|
-
if (
|
|
120
|
-
return
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
for (let index = 1; index < widths.length; index++) {
|
|
130
|
-
if (widths[index] - scaled[index] > widths[targetIndex] - scaled[targetIndex]) {
|
|
131
|
-
targetIndex = index;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
scaled[targetIndex]++;
|
|
135
|
-
remaining--;
|
|
115
|
+
const columnCount = columnWidths.length;
|
|
116
|
+
if (columnCount === 0)
|
|
117
|
+
return [];
|
|
118
|
+
const natural = columnWidths.map((width) => Math.max(1, Math.floor(width)));
|
|
119
|
+
const budget = Math.floor(maxWidth) - tableRowOverhead(columnCount);
|
|
120
|
+
const total = natural.reduce((sum, width) => sum + width, 0);
|
|
121
|
+
if (budget >= total)
|
|
122
|
+
return natural;
|
|
123
|
+
const floorWidth = Math.max(1, Math.min(MIN_TABLE_COLUMN_WIDTH, Math.floor(budget / columnCount)));
|
|
124
|
+
const widths = natural.map((width) => Math.min(width, floorWidth));
|
|
125
|
+
let used = widths.reduce((sum, width) => sum + width, 0);
|
|
126
|
+
if (used > budget) {
|
|
127
|
+
const share = Math.max(1, Math.floor(budget / columnCount));
|
|
128
|
+
for (let index = 0; index < columnCount; index++)
|
|
129
|
+
widths[index] = share;
|
|
130
|
+
used = share * columnCount;
|
|
136
131
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
132
|
+
let remaining = budget - used;
|
|
133
|
+
while (remaining > 0) {
|
|
134
|
+
let grew = false;
|
|
135
|
+
for (let index = 0; index < columnCount && remaining > 0; index++) {
|
|
136
|
+
if (widths[index] >= natural[index])
|
|
141
137
|
continue;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
138
|
+
widths[index]++;
|
|
139
|
+
remaining--;
|
|
140
|
+
grew = true;
|
|
145
141
|
}
|
|
146
|
-
if (
|
|
142
|
+
if (!grew)
|
|
147
143
|
break;
|
|
148
|
-
scaled[targetIndex]--;
|
|
149
|
-
remaining++;
|
|
150
144
|
}
|
|
151
|
-
return
|
|
145
|
+
return widths;
|
|
152
146
|
}
|
|
153
147
|
function normalizeMarkdownTableCells(cells, columnCount) {
|
|
154
148
|
return Array.from({ length: columnCount }, (_, index) => cells[index] ?? '');
|
|
@@ -176,7 +170,7 @@ function parseMarkdownTableBlock(lines, startIndex) {
|
|
|
176
170
|
const normalizedHeaders = normalizeMarkdownTableCells(headers, columnCount);
|
|
177
171
|
const columnWidths = normalizedHeaders.map((header, columnIndex) => {
|
|
178
172
|
const values = [header, ...rows.map((row) => row[columnIndex] ?? '')];
|
|
179
|
-
return Math.max(
|
|
173
|
+
return Math.max(MIN_TABLE_COLUMN_WIDTH, ...values.map((value) => displayWidth(stripInlineFormattingForWidth(value))));
|
|
180
174
|
});
|
|
181
175
|
return {
|
|
182
176
|
nextIndex,
|
|
@@ -280,24 +274,33 @@ function wrapInlineToLines(text, width, bodyColor, prefix = '') {
|
|
|
280
274
|
rowWidth = 0;
|
|
281
275
|
};
|
|
282
276
|
const appendSpan = (part) => {
|
|
283
|
-
|
|
284
|
-
const limit = safeWidth - (indent ? prefix
|
|
277
|
+
let current = rows[rows.length - 1];
|
|
278
|
+
const limit = safeWidth - (rows.length === 1 && indent ? displayWidth(prefix) : 0);
|
|
285
279
|
let remaining = part.text;
|
|
286
280
|
while (remaining.length > 0) {
|
|
287
281
|
const room = limit - rowWidth;
|
|
288
282
|
if (room <= 0) {
|
|
289
283
|
startRow();
|
|
284
|
+
current = rows[rows.length - 1];
|
|
290
285
|
continue;
|
|
291
286
|
}
|
|
292
|
-
|
|
287
|
+
const width = displayWidth(remaining);
|
|
288
|
+
if (width <= room) {
|
|
293
289
|
current.push(span(remaining, styleFromSpan(part)));
|
|
294
|
-
rowWidth +=
|
|
290
|
+
rowWidth += width;
|
|
295
291
|
remaining = '';
|
|
296
292
|
break;
|
|
297
293
|
}
|
|
298
|
-
|
|
299
|
-
|
|
294
|
+
const head = sliceToWidth(remaining, room);
|
|
295
|
+
if (displayWidth(head) > room && current.length > 0) {
|
|
296
|
+
startRow();
|
|
297
|
+
current = rows[rows.length - 1];
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
current.push(span(head, styleFromSpan(part)));
|
|
301
|
+
remaining = remaining.slice(head.length);
|
|
300
302
|
startRow();
|
|
303
|
+
current = rows[rows.length - 1];
|
|
301
304
|
}
|
|
302
305
|
};
|
|
303
306
|
for (const segment of segments) {
|
|
@@ -316,36 +319,35 @@ function wrapInlineToLines(text, width, bodyColor, prefix = '') {
|
|
|
316
319
|
return line(...bodySpans);
|
|
317
320
|
});
|
|
318
321
|
}
|
|
319
|
-
function padCell(text, width) {
|
|
320
|
-
const plain = stripInlineFormattingForWidth(text);
|
|
321
|
-
if (plain.length >= width)
|
|
322
|
-
return plain.slice(0, width);
|
|
323
|
-
return plain + ' '.repeat(width - plain.length);
|
|
324
|
-
}
|
|
325
322
|
function renderTableLines(table, width) {
|
|
326
323
|
const columnWidths = fitTableColumnWidths(table.columnWidths, width);
|
|
324
|
+
const border = (left, joint, right) => line(span(left +
|
|
325
|
+
columnWidths.map((colWidth) => '─'.repeat(colWidth + 2)).join(joint) +
|
|
326
|
+
right, { color: 'cyan', dim: true }));
|
|
327
327
|
const renderRow = (cells, bold) => {
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
328
|
+
const wrapped = columnWidths.map((colWidth, index) => wrapToWidth(stripInlineFormattingForWidth(cells[index] ?? ''), colWidth));
|
|
329
|
+
const height = Math.max(1, ...wrapped.map((cellLines) => cellLines.length));
|
|
330
|
+
const rows = [];
|
|
331
|
+
for (let row = 0; row < height; row++) {
|
|
332
|
+
const spans = [span('│', { color: 'cyan' })];
|
|
333
|
+
for (let column = 0; column < columnWidths.length; column++) {
|
|
334
|
+
const text = wrapped[column]?.[row] ?? '';
|
|
335
|
+
spans.push(span(` ${padToWidth(text, columnWidths[column])} `, {
|
|
336
|
+
color: 'cyan',
|
|
337
|
+
bold,
|
|
338
|
+
}));
|
|
339
|
+
spans.push(span('│', { color: 'cyan' }));
|
|
340
|
+
}
|
|
341
|
+
rows.push(line(...spans));
|
|
342
|
+
}
|
|
343
|
+
return rows;
|
|
333
344
|
};
|
|
334
|
-
const separator = line(span('├' +
|
|
335
|
-
columnWidths.map((colWidth) => '─'.repeat(colWidth + 2)).join('┼') +
|
|
336
|
-
'┤', { color: 'cyan', dim: true }));
|
|
337
|
-
const top = line(span('┌' +
|
|
338
|
-
columnWidths.map((colWidth) => '─'.repeat(colWidth + 2)).join('┬') +
|
|
339
|
-
'┐', { color: 'cyan', dim: true }));
|
|
340
|
-
const bottom = line(span('└' +
|
|
341
|
-
columnWidths.map((colWidth) => '─'.repeat(colWidth + 2)).join('┴') +
|
|
342
|
-
'┘', { color: 'cyan', dim: true }));
|
|
343
345
|
return [
|
|
344
|
-
|
|
345
|
-
renderRow(table.headers, true),
|
|
346
|
-
|
|
347
|
-
...table.rows.
|
|
348
|
-
|
|
346
|
+
border('┌', '┬', '┐'),
|
|
347
|
+
...renderRow(table.headers, true),
|
|
348
|
+
border('├', '┼', '┤'),
|
|
349
|
+
...table.rows.flatMap((row) => renderRow(row, false)),
|
|
350
|
+
border('└', '┴', '┘'),
|
|
349
351
|
];
|
|
350
352
|
}
|
|
351
353
|
function getEntryColor(kind) {
|
|
@@ -410,10 +412,7 @@ export function renderFormattedBodyLines(body, width, kind) {
|
|
|
410
412
|
}
|
|
411
413
|
if (formattedLine.kind === 'table' && formattedLine.table) {
|
|
412
414
|
output.push(...renderTableLines(formattedLine.table, bodyWidth).map((tableLine) => {
|
|
413
|
-
const spans = tableLine.spans.map((part) =>
|
|
414
|
-
...part,
|
|
415
|
-
text: ` ${part.text}`,
|
|
416
|
-
}));
|
|
415
|
+
const spans = tableLine.spans.map((part, index) => index === 0 ? { ...part, text: ` ${part.text}` } : part);
|
|
417
416
|
return { spans };
|
|
418
417
|
}));
|
|
419
418
|
continue;
|