codeep 2.14.0 → 2.16.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/README.md +47 -27
- package/dist/acp/commands.js +22 -1
- package/dist/acp/server.js +13 -2
- package/dist/acp/session.js +22 -1
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.js +2 -2
- package/dist/config/providers.js +35 -24
- package/dist/renderer/App.d.ts +77 -30
- package/dist/renderer/App.js +429 -659
- package/dist/renderer/agentExecution.d.ts +1 -0
- package/dist/renderer/agentExecution.js +3 -2
- package/dist/renderer/commands/helpers.d.ts +251 -0
- package/dist/renderer/commands/helpers.js +450 -0
- package/dist/renderer/commands/registry.js +7 -1
- package/dist/renderer/commands.d.ts +4 -0
- package/dist/renderer/commands.js +363 -318
- package/dist/renderer/components/ActionFormatting.d.ts +17 -0
- package/dist/renderer/components/ActionFormatting.js +67 -0
- package/dist/renderer/components/Autocomplete.d.ts +58 -0
- package/dist/renderer/components/Autocomplete.js +75 -0
- package/dist/renderer/components/Intro.d.ts +9 -0
- package/dist/renderer/components/Intro.js +5 -15
- package/dist/renderer/components/MessageFormatter.d.ts +96 -0
- package/dist/renderer/components/MessageFormatter.js +375 -0
- package/dist/renderer/components/Permission.d.ts +4 -0
- package/dist/renderer/components/Permission.js +1 -1
- package/dist/renderer/components/Status.d.ts +4 -0
- package/dist/renderer/components/Status.js +2 -3
- package/dist/renderer/components/WelcomeFormatter.d.ts +19 -0
- package/dist/renderer/components/WelcomeFormatter.js +79 -0
- package/dist/renderer/components/uiConstants.d.ts +8 -0
- package/dist/renderer/components/uiConstants.js +24 -0
- package/dist/renderer/inputParsing.d.ts +22 -0
- package/dist/renderer/inputParsing.js +28 -0
- package/dist/renderer/layout.d.ts +219 -0
- package/dist/renderer/layout.js +338 -0
- package/dist/renderer/main.d.ts +2 -1
- package/dist/renderer/main.js +79 -11
- package/dist/renderer/ollamaHint.d.ts +12 -0
- package/dist/renderer/ollamaHint.js +29 -0
- package/dist/utils/agentChat.js +23 -1
- package/dist/utils/codeepCloud.d.ts +54 -0
- package/dist/utils/codeepCloud.js +95 -0
- package/dist/utils/diffPreview.d.ts +31 -0
- package/dist/utils/diffPreview.js +102 -0
- package/dist/utils/export.d.ts +12 -0
- package/dist/utils/export.js +3 -3
- package/dist/utils/git.d.ts +28 -0
- package/dist/utils/git.js +111 -1
- package/dist/utils/hooks.d.ts +26 -0
- package/dist/utils/hooks.js +69 -1
- package/dist/utils/keychain.js +45 -29
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.js +1 -1
- package/dist/utils/mcpConfig.d.ts +26 -0
- package/dist/utils/mcpConfig.js +109 -4
- package/dist/utils/mentions.d.ts +195 -0
- package/dist/utils/mentions.js +672 -0
- package/dist/utils/skillBundles.d.ts +14 -0
- package/dist/utils/skillBundles.js +3 -3
- package/dist/utils/skillBundlesCloud.d.ts +7 -0
- package/dist/utils/skillBundlesCloud.js +1 -1
- package/dist/utils/tokenTracker.js +21 -5
- package/dist/utils/toolParsing.d.ts +11 -0
- package/dist/utils/toolParsing.js +6 -0
- package/dist/utils/webFetch.d.ts +101 -0
- package/dist/utils/webFetch.js +375 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure layout helpers extracted from App.ts.
|
|
3
|
+
*
|
|
4
|
+
* These functions take a *snapshot* of the App's UI state (which panels are
|
|
5
|
+
* open, how many items they hold) and return geometric values (heights,
|
|
6
|
+
* offsets) without touching `this`. Keeping them pure means they can be
|
|
7
|
+
* unit-tested directly — the layout math is the part of the renderer most
|
|
8
|
+
* prone to off-by-one regressions, and it was previously untestable because
|
|
9
|
+
* it was inlined in `renderChat` with `this.*` access on every line.
|
|
10
|
+
*
|
|
11
|
+
* Convention: every field on `LayoutSnapshot` is `readonly` so callers
|
|
12
|
+
* cannot mutate the App's real state through the snapshot.
|
|
13
|
+
*/
|
|
14
|
+
/** Read-only snapshot of the fields `bottomPanelHeight` consults. */
|
|
15
|
+
export interface LayoutSnapshot {
|
|
16
|
+
readonly height: number;
|
|
17
|
+
readonly pasteInfoOpen: boolean;
|
|
18
|
+
readonly pasteInfoPreviewLines: number;
|
|
19
|
+
readonly isAgentRunning: boolean;
|
|
20
|
+
readonly confirmOpen: boolean;
|
|
21
|
+
readonly permissionOpen: boolean;
|
|
22
|
+
readonly sessionPickerOpen: boolean;
|
|
23
|
+
readonly sessionPickerItemCount: number;
|
|
24
|
+
readonly confirmMessageCount: number;
|
|
25
|
+
readonly statusOpen: boolean;
|
|
26
|
+
readonly helpOpen: boolean;
|
|
27
|
+
readonly searchOpen: boolean;
|
|
28
|
+
readonly searchResultCount: number;
|
|
29
|
+
readonly exportOpen: boolean;
|
|
30
|
+
readonly logoutOpen: boolean;
|
|
31
|
+
readonly logoutProviderCount: number;
|
|
32
|
+
readonly loginOpen: boolean;
|
|
33
|
+
readonly loginStep: 'provider' | 'apikey';
|
|
34
|
+
readonly loginProviderCount: number;
|
|
35
|
+
readonly menuOpen: boolean;
|
|
36
|
+
readonly menuItemCount: number;
|
|
37
|
+
readonly settingsOpen: boolean;
|
|
38
|
+
readonly settingsCount: number;
|
|
39
|
+
readonly showAutocomplete: boolean;
|
|
40
|
+
readonly autocompleteItemCount: number;
|
|
41
|
+
readonly hunkPickerOpen: boolean;
|
|
42
|
+
readonly mentionPickerOpen: boolean;
|
|
43
|
+
readonly mentionItemCount: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Compute how many terminal rows the bottom panel (paste info, agent box,
|
|
47
|
+
* permission/session/confirm/search/export/login/logout/menu/settings
|
|
48
|
+
* dialogs, autocomplete) occupies in the current frame.
|
|
49
|
+
*
|
|
50
|
+
* Mirrors the if/else chain that used to live inline in `renderChat`.
|
|
51
|
+
* Returns 0 when no panel is open.
|
|
52
|
+
*/
|
|
53
|
+
export declare function bottomPanelHeight(s: LayoutSnapshot): number;
|
|
54
|
+
/**
|
|
55
|
+
* Split the available terminal height into the main chat area and the
|
|
56
|
+
* bottom panel. Returns the y-coordinates the renderer paints into.
|
|
57
|
+
*
|
|
58
|
+
* - `messagesStart` is always 0 (top of the screen).
|
|
59
|
+
* - `messagesEnd` is the last row the message list may use.
|
|
60
|
+
* - `separatorLine`, `inputLine`, `statusLine` are the three reserved
|
|
61
|
+
* rows at the bottom of the main area, in order.
|
|
62
|
+
*/
|
|
63
|
+
export interface ChatLayout {
|
|
64
|
+
messagesStart: number;
|
|
65
|
+
messagesEnd: number;
|
|
66
|
+
separatorLine: number;
|
|
67
|
+
inputLine: number;
|
|
68
|
+
statusLine: number;
|
|
69
|
+
mainHeight: number;
|
|
70
|
+
}
|
|
71
|
+
export declare function chatLayout(height: number, panelHeight: number): ChatLayout;
|
|
72
|
+
/**
|
|
73
|
+
* Count how many terminal rows a single chat message will occupy once
|
|
74
|
+
* word-wrapped to `maxWidth` columns. Used by `scrollToMessage` to find
|
|
75
|
+
* the right scroll offset.
|
|
76
|
+
*
|
|
77
|
+
* Every message renders as: 1 header row + 1 blank row + one or more
|
|
78
|
+
* wrapped content rows, followed by 1 blank spacing row.
|
|
79
|
+
*/
|
|
80
|
+
export declare function messageLineCount(content: string, maxWidth: number): number;
|
|
81
|
+
/**
|
|
82
|
+
* Sum `messageLineCount` across a list of messages and return both the
|
|
83
|
+
* running total and the line offset where `targetIndex` begins. This is
|
|
84
|
+
* the pure core of the old `scrollToMessage` method.
|
|
85
|
+
*/
|
|
86
|
+
export declare function messageOffsets(contents: string[], maxWidth: number, targetIndex: number): {
|
|
87
|
+
totalLines: number;
|
|
88
|
+
targetStartLine: number;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Compute the scroll offset that places the target message roughly in
|
|
92
|
+
* the middle of the visible window.
|
|
93
|
+
*/
|
|
94
|
+
export declare function scrollOffsetForTarget(totalLines: number, targetStartLine: number, visibleLines: number): number;
|
|
95
|
+
/**
|
|
96
|
+
* Compute the visible window of a chat transcript given the current scroll
|
|
97
|
+
* offset. Returns the [startIndex, endIndex) slice into the all-lines array
|
|
98
|
+
* and, as a side-effect contract, the clamped scroll offset the caller
|
|
99
|
+
* should store (the renderer overwrites `this.scrollOffset` with this).
|
|
100
|
+
*
|
|
101
|
+
* Extracted from `getVisibleMessages` so the off-by-one-prone scroll math
|
|
102
|
+
* has direct unit tests.
|
|
103
|
+
*/
|
|
104
|
+
export declare function scrollWindow(args: {
|
|
105
|
+
totalLines: number;
|
|
106
|
+
height: number;
|
|
107
|
+
scrollOffset: number;
|
|
108
|
+
}): {
|
|
109
|
+
startIndex: number;
|
|
110
|
+
endIndex: number;
|
|
111
|
+
clampedScrollOffset: number;
|
|
112
|
+
};
|
|
113
|
+
/** Render a gradient progress bar of the given width for the given ratio. */
|
|
114
|
+
export declare function agentProgressBar(iteration: number, maxIterations: number, barWidth: number): string;
|
|
115
|
+
/** Truncate `text` to `maxLen` columns, appending an ellipsis if it doesn’t fit. */
|
|
116
|
+
export declare function truncateNotification(text: string, maxLen: number): string;
|
|
117
|
+
export interface PasteInfo {
|
|
118
|
+
chars: number;
|
|
119
|
+
lines: number;
|
|
120
|
+
preview: string;
|
|
121
|
+
fullText: string;
|
|
122
|
+
}
|
|
123
|
+
/** Threshold above which a paste is considered "large" and shows a dialog. */
|
|
124
|
+
export declare const PASTE_DIALOG_THRESHOLD: {
|
|
125
|
+
chars: number;
|
|
126
|
+
lines: number;
|
|
127
|
+
};
|
|
128
|
+
/** True when the paste is large enough to warrant the confirm dialog. */
|
|
129
|
+
export declare function shouldShowPasteDialog(text: string): boolean;
|
|
130
|
+
/** Build the PasteInfo struct for a large paste (preview truncated to 200 chars). */
|
|
131
|
+
export declare function buildPasteInfo(text: string): PasteInfo;
|
|
132
|
+
/**
|
|
133
|
+
* Compact a raw token count into the short string shown in the status bar
|
|
134
|
+
* ("123", "1.2K", "12.3K"). Returns an empty string when tokens is 0 so
|
|
135
|
+
* the caller can omit the segment entirely.
|
|
136
|
+
*/
|
|
137
|
+
export declare function formatTokenCount(tokens: number): string;
|
|
138
|
+
/**
|
|
139
|
+
* Pick the context-sensitive hint shown at the right edge of the status
|
|
140
|
+
* bar. The "new messages below" badge takes priority when the user has
|
|
141
|
+
* scrolled up — otherwise the hint depends on whether work is in flight.
|
|
142
|
+
*/
|
|
143
|
+
export declare function statusBarRightHint(args: {
|
|
144
|
+
scrollOffset: number;
|
|
145
|
+
unseenWhileScrolled: number;
|
|
146
|
+
isStreaming: boolean;
|
|
147
|
+
isLoading: boolean;
|
|
148
|
+
}): string;
|
|
149
|
+
/** The panel that currently owns keyboard focus, in priority order. */
|
|
150
|
+
export type ActivePanel = 'pasteInfo' | 'permission' | 'sessionPicker' | 'confirm' | 'status' | 'help' | 'settings' | 'search' | 'export' | 'logout' | 'login' | 'menu' | 'autocomplete' | 'hunkPicker' | 'chat';
|
|
151
|
+
export interface PanelState {
|
|
152
|
+
readonly pasteInfoOpen: boolean;
|
|
153
|
+
readonly permissionOpen: boolean;
|
|
154
|
+
readonly sessionPickerOpen: boolean;
|
|
155
|
+
readonly confirmOpen: boolean;
|
|
156
|
+
readonly statusOpen: boolean;
|
|
157
|
+
readonly helpOpen: boolean;
|
|
158
|
+
readonly settingsOpen: boolean;
|
|
159
|
+
readonly searchOpen: boolean;
|
|
160
|
+
readonly exportOpen: boolean;
|
|
161
|
+
readonly logoutOpen: boolean;
|
|
162
|
+
readonly loginOpen: boolean;
|
|
163
|
+
readonly menuOpen: boolean;
|
|
164
|
+
readonly showAutocomplete: boolean;
|
|
165
|
+
readonly hunkPickerOpen: boolean;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Return the highest-priority open panel. `chat` is the fallback when no
|
|
169
|
+
* panel is open. The order matches the if/else chain that used to live in
|
|
170
|
+
* `handleChatKey`.
|
|
171
|
+
*/
|
|
172
|
+
export declare function activePanel(s: PanelState): ActivePanel;
|
|
173
|
+
export interface InputDisplayOptions {
|
|
174
|
+
/** Full editor value (may contain newlines). */
|
|
175
|
+
value: string;
|
|
176
|
+
/** Character offset of the cursor within `value`. */
|
|
177
|
+
cursorPos: number;
|
|
178
|
+
/** Available width (terminal columns). */
|
|
179
|
+
width: number;
|
|
180
|
+
/** Whether multi-line (❯❯) mode is active. */
|
|
181
|
+
isMultilineMode: boolean;
|
|
182
|
+
}
|
|
183
|
+
export interface InputDisplay {
|
|
184
|
+
/** Prompt symbol shown before the text ("❯ ", "❯❯ ", "[3] ❯ "). */
|
|
185
|
+
promptSymbol: string;
|
|
186
|
+
/** Visible slice of the input (already truncated / ellipsised). */
|
|
187
|
+
displayValue: string;
|
|
188
|
+
/** Column position for the cursor (absolute, 0-based from screen left). */
|
|
189
|
+
cursorX: number;
|
|
190
|
+
/** Placeholder text to show when the editor is empty. */
|
|
191
|
+
placeholder: string;
|
|
192
|
+
/** True when the editor value is empty. */
|
|
193
|
+
isEmpty: boolean;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Compute the prompt symbol for the current state. Multi-line content
|
|
197
|
+
* shows a `[n] ❯ ` prefix with the line count; otherwise `❯❯ ` in
|
|
198
|
+
* multi-line mode, or the plain `❯ `.
|
|
199
|
+
*/
|
|
200
|
+
export declare function inputPromptSymbol(value: string, isMultilineMode: boolean): string;
|
|
201
|
+
/**
|
|
202
|
+
* Compute the visible slice of a long input line, plus the cursor column
|
|
203
|
+
* it maps to. Mirrors the inline logic that used to live in `renderInput`:
|
|
204
|
+
* when the line fits, show it whole; otherwise anchor the cursor at 70%
|
|
205
|
+
* of the available width and slide the viewport.
|
|
206
|
+
*/
|
|
207
|
+
export declare function inputViewport(args: {
|
|
208
|
+
line: string;
|
|
209
|
+
cursorInLine: number;
|
|
210
|
+
maxInputWidth: number;
|
|
211
|
+
}): {
|
|
212
|
+
displayValue: string;
|
|
213
|
+
cursorOffset: number;
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* Top-level entry point used by `renderInput`. Produces the prompt symbol,
|
|
217
|
+
* the visible text, the absolute cursor X, and the placeholder.
|
|
218
|
+
*/
|
|
219
|
+
export declare function computeInputDisplay(opts: InputDisplayOptions): InputDisplay;
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure layout helpers extracted from App.ts.
|
|
3
|
+
*
|
|
4
|
+
* These functions take a *snapshot* of the App's UI state (which panels are
|
|
5
|
+
* open, how many items they hold) and return geometric values (heights,
|
|
6
|
+
* offsets) without touching `this`. Keeping them pure means they can be
|
|
7
|
+
* unit-tested directly — the layout math is the part of the renderer most
|
|
8
|
+
* prone to off-by-one regressions, and it was previously untestable because
|
|
9
|
+
* it was inlined in `renderChat` with `this.*` access on every line.
|
|
10
|
+
*
|
|
11
|
+
* Convention: every field on `LayoutSnapshot` is `readonly` so callers
|
|
12
|
+
* cannot mutate the App's real state through the snapshot.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Compute how many terminal rows the bottom panel (paste info, agent box,
|
|
16
|
+
* permission/session/confirm/search/export/login/logout/menu/settings
|
|
17
|
+
* dialogs, autocomplete) occupies in the current frame.
|
|
18
|
+
*
|
|
19
|
+
* Mirrors the if/else chain that used to live inline in `renderChat`.
|
|
20
|
+
* Returns 0 when no panel is open.
|
|
21
|
+
*/
|
|
22
|
+
export function bottomPanelHeight(s) {
|
|
23
|
+
if (s.pasteInfoOpen) {
|
|
24
|
+
const previewLines = Math.min(s.pasteInfoPreviewLines, 5);
|
|
25
|
+
return previewLines + 6; // title + preview + extra line indicator + options
|
|
26
|
+
}
|
|
27
|
+
if (s.isAgentRunning && !s.confirmOpen) {
|
|
28
|
+
return 9; // Agent progress box: top + 5 log lines + stats + bottom + 1 margin
|
|
29
|
+
}
|
|
30
|
+
if (s.permissionOpen) {
|
|
31
|
+
return 10; // Permission dialog
|
|
32
|
+
}
|
|
33
|
+
if (s.sessionPickerOpen) {
|
|
34
|
+
return Math.min(s.sessionPickerItemCount + 6, 14); // Session picker
|
|
35
|
+
}
|
|
36
|
+
if (s.confirmOpen) {
|
|
37
|
+
return s.confirmMessageCount + 5; // title + messages + buttons + padding
|
|
38
|
+
}
|
|
39
|
+
if (s.hunkPickerOpen) {
|
|
40
|
+
// Title + progress + path + header + up to 12 diff lines + more marker + legend.
|
|
41
|
+
return 18;
|
|
42
|
+
}
|
|
43
|
+
if (s.statusOpen) {
|
|
44
|
+
return 16; // Status info panel
|
|
45
|
+
}
|
|
46
|
+
if (s.helpOpen) {
|
|
47
|
+
return Math.min(s.height - 6, 20); // Help takes more space
|
|
48
|
+
}
|
|
49
|
+
if (s.searchOpen) {
|
|
50
|
+
return Math.min(s.searchResultCount * 3 + 6, 18); // Search results
|
|
51
|
+
}
|
|
52
|
+
if (s.exportOpen) {
|
|
53
|
+
return 10; // Export dialog
|
|
54
|
+
}
|
|
55
|
+
if (s.logoutOpen) {
|
|
56
|
+
return Math.min(s.logoutProviderCount + 6, 12); // Logout picker
|
|
57
|
+
}
|
|
58
|
+
if (s.loginOpen) {
|
|
59
|
+
return s.loginStep === 'provider'
|
|
60
|
+
? Math.min(s.loginProviderCount + 5, 14)
|
|
61
|
+
: 8; // Login dialog
|
|
62
|
+
}
|
|
63
|
+
if (s.menuOpen) {
|
|
64
|
+
return Math.min(s.menuItemCount + 4, 14);
|
|
65
|
+
}
|
|
66
|
+
if (s.settingsOpen) {
|
|
67
|
+
return Math.min(s.settingsCount + 4, 16);
|
|
68
|
+
}
|
|
69
|
+
if (s.showAutocomplete && s.autocompleteItemCount > 0) {
|
|
70
|
+
return Math.min(s.autocompleteItemCount + 3, 12);
|
|
71
|
+
}
|
|
72
|
+
// `@`-mention picker: separator + title + up to 8 rows + footer. Without
|
|
73
|
+
// this branch the panel measured 0 while the picker still painted its rows,
|
|
74
|
+
// so it drew straight over the bottom of the chat transcript.
|
|
75
|
+
if (s.mentionPickerOpen && s.mentionItemCount > 0) {
|
|
76
|
+
return Math.min(s.mentionItemCount, 8) + 3;
|
|
77
|
+
}
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
export function chatLayout(height, panelHeight) {
|
|
81
|
+
const mainHeight = Math.max(1, height - panelHeight);
|
|
82
|
+
const messagesEnd = Math.max(0, mainHeight - 4);
|
|
83
|
+
const separatorLine = Math.max(0, mainHeight - 3);
|
|
84
|
+
const inputLine = Math.max(0, mainHeight - 2);
|
|
85
|
+
const statusLine = Math.max(0, mainHeight - 1);
|
|
86
|
+
return { messagesStart: 0, messagesEnd, separatorLine, inputLine, statusLine, mainHeight };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Count how many terminal rows a single chat message will occupy once
|
|
90
|
+
* word-wrapped to `maxWidth` columns. Used by `scrollToMessage` to find
|
|
91
|
+
* the right scroll offset.
|
|
92
|
+
*
|
|
93
|
+
* Every message renders as: 1 header row + 1 blank row + one or more
|
|
94
|
+
* wrapped content rows, followed by 1 blank spacing row.
|
|
95
|
+
*/
|
|
96
|
+
export function messageLineCount(content, maxWidth) {
|
|
97
|
+
const contentLines = content.split('\n');
|
|
98
|
+
let lines = 2; // Header + empty line after
|
|
99
|
+
for (const line of contentLines) {
|
|
100
|
+
lines += Math.ceil(Math.max(1, line.length) / maxWidth);
|
|
101
|
+
}
|
|
102
|
+
return lines + 1; // +1 for spacing between messages
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Sum `messageLineCount` across a list of messages and return both the
|
|
106
|
+
* running total and the line offset where `targetIndex` begins. This is
|
|
107
|
+
* the pure core of the old `scrollToMessage` method.
|
|
108
|
+
*/
|
|
109
|
+
export function messageOffsets(contents, maxWidth, targetIndex) {
|
|
110
|
+
let totalLines = 0;
|
|
111
|
+
let targetStartLine = 0;
|
|
112
|
+
for (let i = 0; i < contents.length; i++) {
|
|
113
|
+
if (i === targetIndex)
|
|
114
|
+
targetStartLine = totalLines;
|
|
115
|
+
totalLines += messageLineCount(contents[i], maxWidth);
|
|
116
|
+
}
|
|
117
|
+
return { totalLines, targetStartLine };
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Compute the scroll offset that places the target message roughly in
|
|
121
|
+
* the middle of the visible window.
|
|
122
|
+
*/
|
|
123
|
+
export function scrollOffsetForTarget(totalLines, targetStartLine, visibleLines) {
|
|
124
|
+
return Math.max(0, totalLines - targetStartLine - Math.floor(visibleLines / 2));
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Compute the visible window of a chat transcript given the current scroll
|
|
128
|
+
* offset. Returns the [startIndex, endIndex) slice into the all-lines array
|
|
129
|
+
* and, as a side-effect contract, the clamped scroll offset the caller
|
|
130
|
+
* should store (the renderer overwrites `this.scrollOffset` with this).
|
|
131
|
+
*
|
|
132
|
+
* Extracted from `getVisibleMessages` so the off-by-one-prone scroll math
|
|
133
|
+
* has direct unit tests.
|
|
134
|
+
*/
|
|
135
|
+
export function scrollWindow(args) {
|
|
136
|
+
const maxScroll = Math.max(0, args.totalLines - args.height);
|
|
137
|
+
const clampedScrollOffset = Math.min(args.scrollOffset, maxScroll);
|
|
138
|
+
const endIndex = args.totalLines - clampedScrollOffset;
|
|
139
|
+
const startIndex = Math.max(0, endIndex - args.height);
|
|
140
|
+
return { startIndex, endIndex, clampedScrollOffset };
|
|
141
|
+
}
|
|
142
|
+
// ─── Agent progress bar ───────────────────────────────────────────────────────
|
|
143
|
+
//
|
|
144
|
+
// `renderInlineAgentProgress` builds a gradient progress bar from block
|
|
145
|
+
// characters (░▒▓█) when the agent has a known iteration budget. The bar
|
|
146
|
+
// construction is pure string math; extracting it makes the gradient
|
|
147
|
+
// thresholds testable without a Screen mock.
|
|
148
|
+
/** Render a gradient progress bar of the given width for the given ratio. */
|
|
149
|
+
export function agentProgressBar(iteration, maxIterations, barWidth) {
|
|
150
|
+
// Avoid Infinity when maxIterations is 0 — show an empty bar instead.
|
|
151
|
+
const progress = maxIterations > 0 ? Math.min(iteration / maxIterations, 1) : 0;
|
|
152
|
+
const filled = Math.round(progress * barWidth);
|
|
153
|
+
let bar = '';
|
|
154
|
+
for (let i = 0; i < barWidth; i++) {
|
|
155
|
+
if (i < filled - 1)
|
|
156
|
+
bar += '█';
|
|
157
|
+
else if (i === filled - 1)
|
|
158
|
+
bar += '▓';
|
|
159
|
+
else if (i === filled)
|
|
160
|
+
bar += '▒';
|
|
161
|
+
else
|
|
162
|
+
bar += '░';
|
|
163
|
+
}
|
|
164
|
+
return bar;
|
|
165
|
+
}
|
|
166
|
+
// ─── Notification truncation ──────────────────────────────────────────────────
|
|
167
|
+
//
|
|
168
|
+
// `renderStatusBar` truncates the notification string to fit the terminal
|
|
169
|
+
// width with an ellipsis. The truncation rule is pure.
|
|
170
|
+
/** Truncate `text` to `maxLen` columns, appending an ellipsis if it doesn’t fit. */
|
|
171
|
+
export function truncateNotification(text, maxLen) {
|
|
172
|
+
return text.length > maxLen ? text.slice(0, maxLen - 1) + '…' : text;
|
|
173
|
+
}
|
|
174
|
+
/** Threshold above which a paste is considered "large" and shows a dialog. */
|
|
175
|
+
export const PASTE_DIALOG_THRESHOLD = { chars: 100, lines: 3 };
|
|
176
|
+
/** True when the paste is large enough to warrant the confirm dialog. */
|
|
177
|
+
export function shouldShowPasteDialog(text) {
|
|
178
|
+
const chars = text.length;
|
|
179
|
+
const lines = text.split('\n').length;
|
|
180
|
+
return chars >= PASTE_DIALOG_THRESHOLD.chars || lines > PASTE_DIALOG_THRESHOLD.lines;
|
|
181
|
+
}
|
|
182
|
+
/** Build the PasteInfo struct for a large paste (preview truncated to 200 chars). */
|
|
183
|
+
export function buildPasteInfo(text) {
|
|
184
|
+
const preview = text.length > 200 ? text.slice(0, 197) + '...' : text;
|
|
185
|
+
return {
|
|
186
|
+
chars: text.length,
|
|
187
|
+
lines: text.split('\n').length,
|
|
188
|
+
preview,
|
|
189
|
+
fullText: text,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
// ─── Status-bar formatting ────────────────────────────────────────────────────
|
|
193
|
+
//
|
|
194
|
+
// Small pure helpers used by `renderStatusBar`. Extracted so the
|
|
195
|
+
// formatting rules (token compacting, context-sensitive right hint) are
|
|
196
|
+
// unit-testable instead of buried in a 60-line screen-painting method.
|
|
197
|
+
/**
|
|
198
|
+
* Compact a raw token count into the short string shown in the status bar
|
|
199
|
+
* ("123", "1.2K", "12.3K"). Returns an empty string when tokens is 0 so
|
|
200
|
+
* the caller can omit the segment entirely.
|
|
201
|
+
*/
|
|
202
|
+
export function formatTokenCount(tokens) {
|
|
203
|
+
if (tokens <= 0)
|
|
204
|
+
return '';
|
|
205
|
+
if (tokens < 1000)
|
|
206
|
+
return String(tokens);
|
|
207
|
+
return (tokens / 1000).toFixed(1) + 'K';
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Pick the context-sensitive hint shown at the right edge of the status
|
|
211
|
+
* bar. The "new messages below" badge takes priority when the user has
|
|
212
|
+
* scrolled up — otherwise the hint depends on whether work is in flight.
|
|
213
|
+
*/
|
|
214
|
+
export function statusBarRightHint(args) {
|
|
215
|
+
if (args.scrollOffset > 0 && args.unseenWhileScrolled > 0) {
|
|
216
|
+
return `↓ ${args.unseenWhileScrolled} new · PgDn `;
|
|
217
|
+
}
|
|
218
|
+
return args.isStreaming || args.isLoading ? 'Esc to stop ' : '/help · ↑↓ history ';
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Return the highest-priority open panel. `chat` is the fallback when no
|
|
222
|
+
* panel is open. The order matches the if/else chain that used to live in
|
|
223
|
+
* `handleChatKey`.
|
|
224
|
+
*/
|
|
225
|
+
export function activePanel(s) {
|
|
226
|
+
if (s.pasteInfoOpen)
|
|
227
|
+
return 'pasteInfo';
|
|
228
|
+
if (s.permissionOpen)
|
|
229
|
+
return 'permission';
|
|
230
|
+
if (s.sessionPickerOpen)
|
|
231
|
+
return 'sessionPicker';
|
|
232
|
+
if (s.confirmOpen)
|
|
233
|
+
return 'confirm';
|
|
234
|
+
if (s.statusOpen)
|
|
235
|
+
return 'status';
|
|
236
|
+
if (s.helpOpen)
|
|
237
|
+
return 'help';
|
|
238
|
+
if (s.settingsOpen)
|
|
239
|
+
return 'settings';
|
|
240
|
+
if (s.searchOpen)
|
|
241
|
+
return 'search';
|
|
242
|
+
if (s.exportOpen)
|
|
243
|
+
return 'export';
|
|
244
|
+
if (s.logoutOpen)
|
|
245
|
+
return 'logout';
|
|
246
|
+
if (s.loginOpen)
|
|
247
|
+
return 'login';
|
|
248
|
+
if (s.menuOpen)
|
|
249
|
+
return 'menu';
|
|
250
|
+
if (s.hunkPickerOpen)
|
|
251
|
+
return 'hunkPicker';
|
|
252
|
+
if (s.showAutocomplete)
|
|
253
|
+
return 'autocomplete';
|
|
254
|
+
return 'chat';
|
|
255
|
+
}
|
|
256
|
+
// ─── Input-line display ───────────────────────────────────────────────────────
|
|
257
|
+
//
|
|
258
|
+
// `renderInput` builds the text and cursor position for the bottom input
|
|
259
|
+
// row. The geometry (which slice of a long line to show, where the cursor
|
|
260
|
+
// lands within that slice, how the prompt symbol scales with multi-line
|
|
261
|
+
// mode) is pure and was previously inlined alongside screen-write calls.
|
|
262
|
+
// Extracting it makes the truncation/scroll behaviour unit-testable.
|
|
263
|
+
/** Multiplier that controls how far from the left edge the cursor sits. */
|
|
264
|
+
const INPUT_CURSOR_ANCHOR = 0.7;
|
|
265
|
+
/**
|
|
266
|
+
* Compute the prompt symbol for the current state. Multi-line content
|
|
267
|
+
* shows a `[n] ❯ ` prefix with the line count; otherwise `❯❯ ` in
|
|
268
|
+
* multi-line mode, or the plain `❯ `.
|
|
269
|
+
*/
|
|
270
|
+
export function inputPromptSymbol(value, isMultilineMode) {
|
|
271
|
+
const lineCount = value.split('\n').length;
|
|
272
|
+
if (lineCount > 1)
|
|
273
|
+
return `[${lineCount}] ❯ `;
|
|
274
|
+
return isMultilineMode ? '❯❯ ' : '❯ ';
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Compute the visible slice of a long input line, plus the cursor column
|
|
278
|
+
* it maps to. Mirrors the inline logic that used to live in `renderInput`:
|
|
279
|
+
* when the line fits, show it whole; otherwise anchor the cursor at 70%
|
|
280
|
+
* of the available width and slide the viewport.
|
|
281
|
+
*/
|
|
282
|
+
export function inputViewport(args) {
|
|
283
|
+
const { line, cursorInLine, maxInputWidth } = args;
|
|
284
|
+
if (line.length <= maxInputWidth) {
|
|
285
|
+
return { displayValue: line, cursorOffset: Math.max(0, cursorInLine) };
|
|
286
|
+
}
|
|
287
|
+
const effectiveCursor = Math.max(0, cursorInLine);
|
|
288
|
+
const visibleStart = Math.max(0, effectiveCursor - Math.floor(maxInputWidth * INPUT_CURSOR_ANCHOR));
|
|
289
|
+
const visibleEnd = visibleStart + maxInputWidth;
|
|
290
|
+
let displayValue;
|
|
291
|
+
if (visibleStart > 0) {
|
|
292
|
+
displayValue = '…' + line.slice(visibleStart + 1, visibleEnd);
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
displayValue = line.slice(0, maxInputWidth);
|
|
296
|
+
}
|
|
297
|
+
return { displayValue, cursorOffset: effectiveCursor - visibleStart };
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Top-level entry point used by `renderInput`. Produces the prompt symbol,
|
|
301
|
+
* the visible text, the absolute cursor X, and the placeholder.
|
|
302
|
+
*/
|
|
303
|
+
export function computeInputDisplay(opts) {
|
|
304
|
+
const promptSymbol = inputPromptSymbol(opts.value, opts.isMultilineMode);
|
|
305
|
+
const maxInputWidth = opts.width - promptSymbol.length - 1;
|
|
306
|
+
const isEmpty = opts.value.length === 0;
|
|
307
|
+
const placeholder = opts.isMultilineMode
|
|
308
|
+
? 'Multi-line mode Enter=newline · Esc=send'
|
|
309
|
+
: 'Message or /command';
|
|
310
|
+
if (isEmpty) {
|
|
311
|
+
return {
|
|
312
|
+
promptSymbol,
|
|
313
|
+
displayValue: '',
|
|
314
|
+
cursorX: promptSymbol.length,
|
|
315
|
+
placeholder,
|
|
316
|
+
isEmpty,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
// For multi-line content, show the last line being edited.
|
|
320
|
+
const lines = opts.value.split('\n');
|
|
321
|
+
const lineCount = lines.length;
|
|
322
|
+
const lastLine = lines[lines.length - 1];
|
|
323
|
+
const displayInput = lineCount > 1 ? lastLine : opts.value;
|
|
324
|
+
const charsBeforeLastLine = lineCount > 1 ? opts.value.lastIndexOf('\n') + 1 : 0;
|
|
325
|
+
const cursorInLine = opts.cursorPos - charsBeforeLastLine;
|
|
326
|
+
const { displayValue, cursorOffset } = inputViewport({
|
|
327
|
+
line: displayInput,
|
|
328
|
+
cursorInLine,
|
|
329
|
+
maxInputWidth,
|
|
330
|
+
});
|
|
331
|
+
return {
|
|
332
|
+
promptSymbol,
|
|
333
|
+
displayValue,
|
|
334
|
+
cursorX: promptSymbol.length + cursorOffset,
|
|
335
|
+
placeholder,
|
|
336
|
+
isEmpty,
|
|
337
|
+
};
|
|
338
|
+
}
|
package/dist/renderer/main.d.ts
CHANGED
|
@@ -5,4 +5,5 @@
|
|
|
5
5
|
* This file contains only startup/init logic. Command dispatch lives in
|
|
6
6
|
* commands.ts and agent execution in agentExecution.ts.
|
|
7
7
|
*/
|
|
8
|
-
|
|
8
|
+
/** Derive a short display name from a user message (first ~5 words, max 48 chars). */
|
|
9
|
+
export declare function deriveSessionName(message: string): string;
|