@thegitai/cli 1.0.0-preview.41 → 1.0.0-preview.43
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 +5 -4
- package/dist/src/agent-mode.js +24 -1
- package/dist/src/api/http.js +27 -3
- package/dist/src/help-text.js +18 -7
- package/dist/src/preferences.js +39 -0
- package/dist/src/quota-notice.js +35 -0
- package/dist/src/ui/repl.js +104 -7
- package/dist/src/ui/tui/attention.js +25 -0
- package/dist/src/ui/tui/bridge.js +31 -5
- package/dist/src/ui/tui/build-frame.js +166 -39
- package/dist/src/ui/tui/desktop-notification.js +118 -0
- package/dist/src/ui/tui/shell-input.js +22 -1
- package/dist/src/ui/tui/shortcuts.js +61 -0
- package/dist/src/usage.js +5 -2
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -53,8 +53,8 @@ is required. Tabs used by the agent join a TheGitAI tab group in an existing
|
|
|
53
53
|
window. The conversation stays in your terminal.
|
|
54
54
|
|
|
55
55
|
Default mode asks before using a website and can remember that site for the
|
|
56
|
-
session.
|
|
57
|
-
clicking, typing, or navigating. Use Stop/Resume in the extension popup to
|
|
56
|
+
session. Bypass all permissions allows browser actions; Plan mode reads pages
|
|
57
|
+
without clicking, typing, or navigating. Use Stop/Resume in the extension popup to
|
|
58
58
|
pause browser control, or Disable to disconnect it. Chrome's protected pages,
|
|
59
59
|
including browser settings and extension management, cannot be controlled.
|
|
60
60
|
|
|
@@ -76,8 +76,9 @@ Multi-select questions toggle choices. The last question has an explicit
|
|
|
76
76
|
the form; **Ctrl+C** cancels the whole turn.
|
|
77
77
|
|
|
78
78
|
Answers return to the same turn, and completed question/answer records remain
|
|
79
|
-
readable when the session is resumed. Default,
|
|
80
|
-
support the form;
|
|
79
|
+
readable when the session is resumed. Default, Bypass all permissions, and Plan
|
|
80
|
+
modes all support the form; Bypass all permissions does not choose answers for
|
|
81
|
+
you.
|
|
81
82
|
|
|
82
83
|
## Visible to-do list
|
|
83
84
|
|
package/dist/src/agent-mode.js
CHANGED
|
@@ -145,8 +145,31 @@ export function buildPlanModeToolBlockedResult(toolName, action = 'Plan mode is
|
|
|
145
145
|
}
|
|
146
146
|
export function agentModeLabel(mode) {
|
|
147
147
|
if (mode === 'auto-accept')
|
|
148
|
-
return '
|
|
148
|
+
return 'Bypass all permissions';
|
|
149
149
|
if (mode === 'plan')
|
|
150
150
|
return 'Plan · read-only';
|
|
151
|
+
return 'Default · asks to edit & run unless permitted';
|
|
152
|
+
}
|
|
153
|
+
export function agentModeLabelTiers(mode) {
|
|
154
|
+
if (mode === 'default') {
|
|
155
|
+
return [agentModeLabel(mode), 'Default · asks to edit & run'];
|
|
156
|
+
}
|
|
157
|
+
return [agentModeLabel(mode)];
|
|
158
|
+
}
|
|
159
|
+
export function agentModeShortLabel(mode) {
|
|
160
|
+
if (mode === 'auto-accept')
|
|
161
|
+
return 'Bypass';
|
|
162
|
+
if (mode === 'plan')
|
|
163
|
+
return 'Plan';
|
|
151
164
|
return 'Default';
|
|
152
165
|
}
|
|
166
|
+
export function agentModeColor(mode) {
|
|
167
|
+
if (mode === 'auto-accept')
|
|
168
|
+
return 'red';
|
|
169
|
+
if (mode === 'plan')
|
|
170
|
+
return 'yellow';
|
|
171
|
+
return 'cyan';
|
|
172
|
+
}
|
|
173
|
+
export function agentModeCycleSequence() {
|
|
174
|
+
return AGENT_MODES.map(agentModeShortLabel).join(' → ');
|
|
175
|
+
}
|
package/dist/src/api/http.js
CHANGED
|
@@ -7,12 +7,14 @@ export class ServerApiError extends Error {
|
|
|
7
7
|
status;
|
|
8
8
|
traceId;
|
|
9
9
|
code;
|
|
10
|
-
|
|
10
|
+
details;
|
|
11
|
+
constructor(message, status, traceId, code = '', details = {}) {
|
|
11
12
|
super(traceId ? `${message}\nTrace ID: ${traceId}` : message);
|
|
12
13
|
this.name = 'ServerApiError';
|
|
13
14
|
this.status = status;
|
|
14
15
|
this.traceId = traceId;
|
|
15
16
|
this.code = code;
|
|
17
|
+
this.details = details;
|
|
16
18
|
}
|
|
17
19
|
}
|
|
18
20
|
export function createTraceId() {
|
|
@@ -187,6 +189,8 @@ export function gatewayFailureCategory(error) {
|
|
|
187
189
|
return 'connection_error';
|
|
188
190
|
if (!(error instanceof ServerApiError))
|
|
189
191
|
return null;
|
|
192
|
+
if (isQuotaExceededError(error))
|
|
193
|
+
return 'quota_exceeded';
|
|
190
194
|
return error.status in GATEWAY_STATUS_MESSAGES ? 'gateway_error' : null;
|
|
191
195
|
}
|
|
192
196
|
export async function readJsonResponse(response) {
|
|
@@ -208,6 +212,26 @@ export function failureMessage(data, status) {
|
|
|
208
212
|
export function failureCode(data) {
|
|
209
213
|
return typeof data?.error?.code === 'string' ? data.error.code : '';
|
|
210
214
|
}
|
|
215
|
+
export function failureDetails(data) {
|
|
216
|
+
const error = data?.error;
|
|
217
|
+
if (!error || typeof error !== 'object')
|
|
218
|
+
return {};
|
|
219
|
+
return {
|
|
220
|
+
...(typeof error.window === 'string' ? { window: error.window } : {}),
|
|
221
|
+
...(typeof error.resetsAt === 'string' ? { resetsAt: error.resetsAt } : {}),
|
|
222
|
+
...(typeof error.percentLeft === 'number'
|
|
223
|
+
? { percentLeft: error.percentLeft }
|
|
224
|
+
: {}),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
export const QUOTA_EXCEEDED_CODE = 'QUOTA_EXCEEDED';
|
|
228
|
+
export function isQuotaExceededError(error) {
|
|
229
|
+
if (!(error instanceof ServerApiError))
|
|
230
|
+
return false;
|
|
231
|
+
if (error.code === QUOTA_EXCEEDED_CODE)
|
|
232
|
+
return true;
|
|
233
|
+
return error.status === 429 && !error.code;
|
|
234
|
+
}
|
|
211
235
|
export function isAuthenticationError(error) {
|
|
212
236
|
return error instanceof ServerApiError && error.status === 401;
|
|
213
237
|
}
|
|
@@ -218,7 +242,7 @@ export function authenticationErrorMessage(error) {
|
|
|
218
242
|
}
|
|
219
243
|
export async function readErrorResponse(response, traceId = response.headers.get(TRACE_ID_HEADER) ?? '') {
|
|
220
244
|
const data = await readJsonResponse(response);
|
|
221
|
-
return new ServerApiError(failureMessage(data, response.status), response.status, traceId, failureCode(data));
|
|
245
|
+
return new ServerApiError(failureMessage(data, response.status), response.status, traceId, failureCode(data), failureDetails(data));
|
|
222
246
|
}
|
|
223
247
|
export async function authorizedJson({ config, path, method = 'GET', body = null, headers = {}, fetchImpl = globalThis.fetch, timeoutMs = REQUEST_TIMEOUT_MS, }) {
|
|
224
248
|
const trace = createTraceContext();
|
|
@@ -235,7 +259,7 @@ export async function authorizedJson({ config, path, method = 'GET', body = null
|
|
|
235
259
|
});
|
|
236
260
|
const data = await readJsonResponse(response);
|
|
237
261
|
if (!response.ok) {
|
|
238
|
-
throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId, failureCode(data));
|
|
262
|
+
throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId, failureCode(data), failureDetails(data));
|
|
239
263
|
}
|
|
240
264
|
return data;
|
|
241
265
|
}
|
package/dist/src/help-text.js
CHANGED
|
@@ -45,24 +45,29 @@ const HELP_MARKDOWN = [
|
|
|
45
45
|
'',
|
|
46
46
|
'## Options',
|
|
47
47
|
'',
|
|
48
|
-
'- `-y, --yes` — start in
|
|
48
|
+
'- `-y, --yes` — start in Bypass all permissions mode',
|
|
49
49
|
'- `-h, --help` — show this help',
|
|
50
50
|
'- `-v, --version` — print the version and exit',
|
|
51
51
|
'',
|
|
52
52
|
'## Modes',
|
|
53
53
|
'',
|
|
54
|
-
'- Default
|
|
55
|
-
'
|
|
56
|
-
'
|
|
57
|
-
'
|
|
54
|
+
'- Default · asks to edit & run unless permitted — asks before creating,',
|
|
55
|
+
' editing or deleting files, before running commands, and before touching a',
|
|
56
|
+
' website. Each answer can be remembered for the rest of the session, and',
|
|
57
|
+
' once it is, Default stops asking for that action; see Safety & approvals.',
|
|
58
|
+
'- Bypass all permissions — approves every command, file edit, deletion and',
|
|
59
|
+
' browser action for the rest of the session, without asking. Shown in red',
|
|
60
|
+
' in the footer. Blocked commands stay blocked.',
|
|
58
61
|
'- Plan — read-only; the agent can inspect, ask typed questions, and plan,',
|
|
59
62
|
' with file-reading shell commands only. It will not edit files or run tests,',
|
|
60
63
|
' builds, diagnostics, package managers, project code, or network probes.',
|
|
61
|
-
'- In the TUI, Shift+Tab cycles
|
|
64
|
+
'- In the TUI, press **?** on an empty composer for the keyboard-and-mouse panel. Shift+Tab cycles Default → Bypass → Plan (refused while the agent is working), mouse drag copies transcript/footer text, left-click opens rendered links, right-click copies rendered links or pastes in the sticky composer/footer when no selection is active, double-click copies a word, and triple-click copies a row.',
|
|
62
65
|
'',
|
|
63
66
|
'## Keys & clipboard',
|
|
64
67
|
'',
|
|
65
|
-
'-
|
|
68
|
+
'- **?** on an empty composer opens the keyboard-and-mouse panel.',
|
|
69
|
+
'- **Enter** sends • **Shift+Tab** cycles Default → Bypass → Plan (only',
|
|
70
|
+
' between turns) • **Esc** cancels the turn •',
|
|
66
71
|
' **Ctrl+C** clears the composer or the queued message, and quits once there',
|
|
67
72
|
' is nothing left to clear. These are the same on macOS, Linux, and Windows.',
|
|
68
73
|
'- **While the agent is working**, Enter queues your message and locks the',
|
|
@@ -72,6 +77,11 @@ const HELP_MARKDOWN = [
|
|
|
72
77
|
' back for editing and **Esc**/**Ctrl+C** discards it; both unlock the',
|
|
73
78
|
' composer for the next message. **Ctrl+V** pastes a screenshot onto the',
|
|
74
79
|
' queued message, and it is delivered into the running turn with the text.',
|
|
80
|
+
'- **Ctrl+G** turns desktop notifications on or off. One is raised when a turn',
|
|
81
|
+
' ends and when a permission prompt or a question is waiting for you, but',
|
|
82
|
+
' only while the terminal is in the background — if you are looking at the',
|
|
83
|
+
' session you are told nothing. Coming back clears it. The choice is',
|
|
84
|
+
' remembered between sessions.',
|
|
75
85
|
`- **Paste** into the composer with your terminal's paste shortcut (\`${PASTE_SHORTCUT}\``,
|
|
76
86
|
' on this system) or by right-clicking the composer.',
|
|
77
87
|
'- **Copy** from the transcript by dragging to select; double-click copies a',
|
|
@@ -96,6 +106,7 @@ const HELP_MARKDOWN = [
|
|
|
96
106
|
' is working; press it again or Esc to close without touching the turn',
|
|
97
107
|
'- `/jobs output <id>` — print one job\'s full captured output',
|
|
98
108
|
'- `/jobs kill <id>` — stop one background job',
|
|
109
|
+
'- `/notify` — turn desktop notifications on or off (same as Ctrl+G)',
|
|
99
110
|
'- `/new` — start a new conversation; this session remains saved',
|
|
100
111
|
'- `/logout` — sign out and quit',
|
|
101
112
|
'- `/exit` — quit the session',
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { getClientStateDir } from './client-state.js';
|
|
4
|
+
export const DEFAULT_PREFERENCES = {
|
|
5
|
+
notifications: true,
|
|
6
|
+
};
|
|
7
|
+
export function getPreferencesPath(env = process.env) {
|
|
8
|
+
return path.join(getClientStateDir(env), 'prefs.json');
|
|
9
|
+
}
|
|
10
|
+
export function readPreferences(env = process.env) {
|
|
11
|
+
const filePath = getPreferencesPath(env);
|
|
12
|
+
if (!existsSync(filePath))
|
|
13
|
+
return { ...DEFAULT_PREFERENCES };
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
16
|
+
return {
|
|
17
|
+
notifications: parsed.notifications === false ? false : DEFAULT_PREFERENCES.notifications,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return { ...DEFAULT_PREFERENCES };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function writePreferences(preferences, env = process.env) {
|
|
25
|
+
const filePath = getPreferencesPath(env);
|
|
26
|
+
mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
27
|
+
const tempPath = `${filePath}.${process.pid}.tmp`;
|
|
28
|
+
try {
|
|
29
|
+
writeFileSync(tempPath, `${JSON.stringify(preferences, null, 2)}\n`, {
|
|
30
|
+
encoding: 'utf8',
|
|
31
|
+
mode: 0o600,
|
|
32
|
+
});
|
|
33
|
+
renameSync(tempPath, filePath);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
rmSync(tempPath, { force: true });
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { isQuotaExceededError, ServerApiError } from './api/http.js';
|
|
2
|
+
import { USAGE_WINDOWS, formatUtcMinute, usageWindowLabel } from './usage.js';
|
|
3
|
+
export function buildQuotaNotice(error) {
|
|
4
|
+
if (!(error instanceof ServerApiError) || !isQuotaExceededError(error)) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
const label = usageWindowLabel(String(error.details.window ?? ''));
|
|
8
|
+
const inSentence = label.toLowerCase();
|
|
9
|
+
const resetsAt = error.details.resetsAt ?? null;
|
|
10
|
+
const lines = [];
|
|
11
|
+
if (label) {
|
|
12
|
+
const reset = resetsAt ? formatUtcMinute(resetsAt) : '';
|
|
13
|
+
lines.push(reset && reset !== 'unknown'
|
|
14
|
+
? `You've used your ${inSentence} limit. It resets at ${reset}.`
|
|
15
|
+
: `You've used your ${inSentence} limit.`);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
lines.push(firstMessageLine(error));
|
|
19
|
+
}
|
|
20
|
+
lines.push(label
|
|
21
|
+
? `Run /usage to see your ${otherWindowNames(label)} windows.`
|
|
22
|
+
: 'Run /usage to see your remaining usage.');
|
|
23
|
+
return { title: 'Usage limit reached', body: lines.join('\n') };
|
|
24
|
+
}
|
|
25
|
+
function otherWindowNames(usedLabel) {
|
|
26
|
+
const names = USAGE_WINDOWS.map(([name]) => name)
|
|
27
|
+
.filter((name) => name !== usedLabel)
|
|
28
|
+
.map((name) => name.toLowerCase());
|
|
29
|
+
return names.length > 1
|
|
30
|
+
? `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}`
|
|
31
|
+
: (names[0] ?? '');
|
|
32
|
+
}
|
|
33
|
+
function firstMessageLine(error) {
|
|
34
|
+
return String(error.message ?? '').split('\n')[0]?.trim() || 'Usage limit reached.';
|
|
35
|
+
}
|
package/dist/src/ui/repl.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
1
2
|
import { createRatatuiBridge } from './tui/bridge.js';
|
|
2
3
|
import { bucketActionLabel, } from '../permissions.js';
|
|
3
|
-
import { approvalScrollLimit, buildTuiFrame, formatJobElapsed, formatTodoProgress, pickThinkingFallbackPhrase, renderTranscriptEntryLines, THINKING_FALLBACK_PHRASES, userInputViewportForFrame, } from './tui/build-frame.js';
|
|
4
|
+
import { AGENT_MODE_LOCKED_STATUS, approvalScrollLimit, buildTuiFrame, formatJobElapsed, formatTodoProgress, pickThinkingFallbackPhrase, renderTranscriptEntryLines, THINKING_FALLBACK_PHRASES, userInputViewportForFrame, } from './tui/build-frame.js';
|
|
5
|
+
import { createAttentionController, INITIAL_TERMINAL_FOCUS, } from './tui/attention.js';
|
|
6
|
+
import { backendFor, createNotificationQueue, } from './tui/desktop-notification.js';
|
|
7
|
+
import { readPreferences, writePreferences } from '../preferences.js';
|
|
4
8
|
import { createTerminalTitleController } from './tui/terminal-title.js';
|
|
5
9
|
import { captureTerminalWrites, releaseTerminalWrites, } from './tui/terminal-writes.js';
|
|
6
10
|
export { getSlashCommandSuggestions } from './tui/build-frame.js';
|
|
@@ -16,6 +20,7 @@ import { cancelActiveCommand } from '../executor.js';
|
|
|
16
20
|
import { isTurnFailureMarker } from '../turn-failure-marker.js';
|
|
17
21
|
import { clearCliAuthConfig, logoutFromServer, } from '../api/auth.js';
|
|
18
22
|
import { authenticationErrorMessage, isAuthenticationError, } from '../api/http.js';
|
|
23
|
+
import { buildQuotaNotice } from '../quota-notice.js';
|
|
19
24
|
import { createUserInputPromptState, formatUserInputTranscript, } from './tui/user-input.js';
|
|
20
25
|
import { setCommandOutputHook, withTuiMode } from '../runtime-mode.js';
|
|
21
26
|
import { collectBackgroundJobUiKillMutations, collectBackgroundJobUiOutputMutations, } from '../tool-executor.js';
|
|
@@ -76,7 +81,6 @@ const TRANSCRIPT_DIFF_PREVIEW_LINES = 24;
|
|
|
76
81
|
const WORKING_TOOL_PREVIEW_ITEMS = 4;
|
|
77
82
|
const THINKING_NOTE_PREVIEW_ROWS = 3;
|
|
78
83
|
const WORKING_TOOL_PREVIEW_ROWS = 3;
|
|
79
|
-
const AGENT_MODE_LABEL_WIDTH = 16;
|
|
80
84
|
export const REPORT_ISSUE_URL = 'https://thegit.ai/report';
|
|
81
85
|
export function buildReportTranscriptEntries(openedUrl) {
|
|
82
86
|
return openedUrl
|
|
@@ -135,6 +139,10 @@ export const SLASH_COMMANDS = [
|
|
|
135
139
|
command: '/jobs',
|
|
136
140
|
description: 'Background jobs: pick to view output or kill',
|
|
137
141
|
},
|
|
142
|
+
{
|
|
143
|
+
command: '/notify',
|
|
144
|
+
description: 'Toggle desktop notifications when a turn ends (Ctrl+G)',
|
|
145
|
+
},
|
|
138
146
|
{
|
|
139
147
|
command: '/new',
|
|
140
148
|
description: 'start a new conversation; this session remains saved',
|
|
@@ -1100,11 +1108,15 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
1100
1108
|
exiting: false,
|
|
1101
1109
|
input: '',
|
|
1102
1110
|
maxToolSteps: session.maxToolSteps,
|
|
1111
|
+
notificationsEnabled: readPreferences(session.env).notifications,
|
|
1112
|
+
terminalFocused: INITIAL_TERMINAL_FOCUS,
|
|
1103
1113
|
jobsPickerExpandedId: null,
|
|
1104
1114
|
jobsPickerIndex: 0,
|
|
1105
1115
|
jobsPickerOpen: false,
|
|
1106
1116
|
modelPickerIndex: getDefaultModelPickerIndex(session.modelId, serverModels.models),
|
|
1107
1117
|
modelPickerOpen: false,
|
|
1118
|
+
shortcutsOpen: false,
|
|
1119
|
+
transientStatus: null,
|
|
1108
1120
|
projectRoot: session.rootDir,
|
|
1109
1121
|
sessionId: session.sessionId,
|
|
1110
1122
|
showSessionId: debugUi.showSessionId,
|
|
@@ -1532,15 +1544,17 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1532
1544
|
void remountTui();
|
|
1533
1545
|
}, 0);
|
|
1534
1546
|
};
|
|
1535
|
-
const showTransientStatus = (
|
|
1547
|
+
const showTransientStatus = (transientStatus) => {
|
|
1536
1548
|
if (transientStatusTimer) {
|
|
1537
1549
|
clearTimeout(transientStatusTimer);
|
|
1538
1550
|
transientStatusTimer = null;
|
|
1539
1551
|
}
|
|
1540
|
-
store.update((current) => ({ ...current,
|
|
1552
|
+
store.update((current) => ({ ...current, transientStatus }));
|
|
1541
1553
|
transientStatusTimer = setTimeout(() => {
|
|
1542
1554
|
transientStatusTimer = null;
|
|
1543
|
-
store.update((current) => current.
|
|
1555
|
+
store.update((current) => current.transientStatus === transientStatus
|
|
1556
|
+
? { ...current, transientStatus: null }
|
|
1557
|
+
: current);
|
|
1544
1558
|
}, 2500);
|
|
1545
1559
|
};
|
|
1546
1560
|
const terminalTitle = createTerminalTitleController({
|
|
@@ -1553,11 +1567,42 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1553
1567
|
busy: state.busy,
|
|
1554
1568
|
});
|
|
1555
1569
|
};
|
|
1570
|
+
const notifier = backendFor();
|
|
1571
|
+
let notificationHandle = null;
|
|
1572
|
+
const projectLabel = basename(session.rootDir) || 'TheGitAI';
|
|
1573
|
+
const notifications = createNotificationQueue();
|
|
1574
|
+
const attention = createAttentionController({
|
|
1575
|
+
notify: (kind) => {
|
|
1576
|
+
notifications.run(async () => {
|
|
1577
|
+
notificationHandle = await notifier.post({
|
|
1578
|
+
title: projectLabel,
|
|
1579
|
+
body: kind === 'blocked'
|
|
1580
|
+
? 'Waiting for your answer'
|
|
1581
|
+
: 'Finished \u2014 your turn',
|
|
1582
|
+
}, notificationHandle);
|
|
1583
|
+
});
|
|
1584
|
+
},
|
|
1585
|
+
withdraw: () => {
|
|
1586
|
+
notifications.run(async () => {
|
|
1587
|
+
notifier.withdraw(notificationHandle);
|
|
1588
|
+
notificationHandle = null;
|
|
1589
|
+
});
|
|
1590
|
+
},
|
|
1591
|
+
});
|
|
1592
|
+
const syncAttention = () => {
|
|
1593
|
+
const state = store.getState();
|
|
1594
|
+
attention.sync({
|
|
1595
|
+
awaitingAnswer: Boolean(state.approvalPrompt || state.sudoPrompt || state.userInputPrompt),
|
|
1596
|
+
busy: state.busy,
|
|
1597
|
+
focused: state.terminalFocused,
|
|
1598
|
+
}, state.notificationsEnabled);
|
|
1599
|
+
};
|
|
1556
1600
|
const renderCurrentFrame = () => {
|
|
1557
1601
|
if (!tuiReady)
|
|
1558
1602
|
return;
|
|
1559
1603
|
const state = store.getState();
|
|
1560
1604
|
syncTerminalTitle();
|
|
1605
|
+
syncAttention();
|
|
1561
1606
|
const elapsedSeconds = busyElapsedSeconds(state, Date.now());
|
|
1562
1607
|
bridge.render(buildTuiFrame(state, terminalCols, terminalRows, spinnerFrame, elapsedSeconds, Date.now()));
|
|
1563
1608
|
};
|
|
@@ -1806,6 +1851,9 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1806
1851
|
setAgentMode(nextAgentMode(session.agentMode));
|
|
1807
1852
|
scheduleLiveFrameRemount();
|
|
1808
1853
|
};
|
|
1854
|
+
const reportAgentModeLocked = () => {
|
|
1855
|
+
showTransientStatus(AGENT_MODE_LOCKED_STATUS);
|
|
1856
|
+
};
|
|
1809
1857
|
const clearLiveFrameRemountTimer = () => {
|
|
1810
1858
|
if (!liveFrameRemountTimer)
|
|
1811
1859
|
return;
|
|
@@ -2100,6 +2148,16 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2100
2148
|
title: 'Model',
|
|
2101
2149
|
});
|
|
2102
2150
|
};
|
|
2151
|
+
const toggleNotifications = () => {
|
|
2152
|
+
const enabled = !store.getState().notificationsEnabled;
|
|
2153
|
+
store.update((current) => ({ ...current, notificationsEnabled: enabled }));
|
|
2154
|
+
try {
|
|
2155
|
+
writePreferences({ ...readPreferences(session.env), notifications: enabled }, session.env);
|
|
2156
|
+
}
|
|
2157
|
+
catch {
|
|
2158
|
+
}
|
|
2159
|
+
showTransientStatus(enabled ? 'Notifications on' : 'Notifications off');
|
|
2160
|
+
};
|
|
2103
2161
|
const openJobsPicker = () => {
|
|
2104
2162
|
if (store.getState().jobsPickerOpen)
|
|
2105
2163
|
return;
|
|
@@ -2506,8 +2564,19 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2506
2564
|
openJobsPicker();
|
|
2507
2565
|
return;
|
|
2508
2566
|
}
|
|
2567
|
+
if (input === '/notify') {
|
|
2568
|
+
store.update((current) => ({
|
|
2569
|
+
...current,
|
|
2570
|
+
cursor: 0,
|
|
2571
|
+
input: '',
|
|
2572
|
+
pastedChunks: [],
|
|
2573
|
+
}));
|
|
2574
|
+
toggleNotifications();
|
|
2575
|
+
return;
|
|
2576
|
+
}
|
|
2509
2577
|
appendTurnAwareEntry({
|
|
2510
|
-
body: "Slash commands can't be queued while a turn is running.
|
|
2578
|
+
body: "Slash commands can't be queued while a turn is running. " +
|
|
2579
|
+
'Ctrl+B opens background jobs, Ctrl+G toggles notifications.',
|
|
2511
2580
|
kind: 'system',
|
|
2512
2581
|
title: 'Queued',
|
|
2513
2582
|
});
|
|
@@ -2592,6 +2661,10 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2592
2661
|
appendStaticEntries(buildReportTranscriptEntries(opened));
|
|
2593
2662
|
return;
|
|
2594
2663
|
}
|
|
2664
|
+
if (input === '/notify') {
|
|
2665
|
+
toggleNotifications();
|
|
2666
|
+
return;
|
|
2667
|
+
}
|
|
2595
2668
|
if (input === '/jobs' || input.startsWith('/jobs ')) {
|
|
2596
2669
|
const jobsArgs = input.slice('/jobs'.length).trim();
|
|
2597
2670
|
if (!jobsArgs) {
|
|
@@ -2899,6 +2972,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2899
2972
|
return;
|
|
2900
2973
|
const failedTurnAttachments = activeTurnImageAttachments;
|
|
2901
2974
|
activeTurnImageAttachments = [];
|
|
2975
|
+
const quotaNotice = cancelled ? null : buildQuotaNotice(error);
|
|
2902
2976
|
store.update((current) => ({
|
|
2903
2977
|
...current,
|
|
2904
2978
|
activeTurnInput: '',
|
|
@@ -2908,7 +2982,11 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2908
2982
|
busySince: null,
|
|
2909
2983
|
commandLog: [],
|
|
2910
2984
|
imageAttachments: failedTurnAttachments,
|
|
2911
|
-
status: cancelled
|
|
2985
|
+
status: cancelled
|
|
2986
|
+
? 'Ready'
|
|
2987
|
+
: quotaNotice
|
|
2988
|
+
? quotaNotice.title
|
|
2989
|
+
: 'Turn failed',
|
|
2912
2990
|
thinkingTitle: '',
|
|
2913
2991
|
thinkingNotes: [],
|
|
2914
2992
|
tokenUsage: formatClientTokenUsage(Date.now() - turnStartedAt, latestUsageSummary),
|
|
@@ -2927,6 +3005,16 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2927
3005
|
if (!(await saveActiveSession()))
|
|
2928
3006
|
return;
|
|
2929
3007
|
}
|
|
3008
|
+
else if (quotaNotice) {
|
|
3009
|
+
appendStaticEntries([
|
|
3010
|
+
...turnEntries,
|
|
3011
|
+
{
|
|
3012
|
+
body: quotaNotice.body,
|
|
3013
|
+
kind: 'system',
|
|
3014
|
+
title: quotaNotice.title,
|
|
3015
|
+
},
|
|
3016
|
+
]);
|
|
3017
|
+
}
|
|
2930
3018
|
else {
|
|
2931
3019
|
appendStaticEntries(turnEntries);
|
|
2932
3020
|
appendError(error.message);
|
|
@@ -3090,6 +3178,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
3090
3178
|
},
|
|
3091
3179
|
onCancelTurn: cancelActiveTurn,
|
|
3092
3180
|
onCycleAgentMode: cycleAgentMode,
|
|
3181
|
+
onAgentModeChangeBlocked: reportAgentModeLocked,
|
|
3093
3182
|
onCtrlC: handleCtrlC,
|
|
3094
3183
|
onLinkCopy: handleLinkCopy,
|
|
3095
3184
|
onLinkOpen: handleLinkOpen,
|
|
@@ -3101,6 +3190,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
3101
3190
|
onSelectionCopy: handleAppSelectionCopy,
|
|
3102
3191
|
onSelectModel: handleInlineModelSelection,
|
|
3103
3192
|
onOpenJobsPicker: openJobsPicker,
|
|
3193
|
+
onToggleNotifications: toggleNotifications,
|
|
3104
3194
|
onJobsPickerOutput: handleJobsPickerOutput,
|
|
3105
3195
|
onJobsPickerKill: handleJobsPickerKill,
|
|
3106
3196
|
onSudoPasswordInput: handleSudoPasswordInput,
|
|
@@ -3144,6 +3234,13 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
3144
3234
|
return;
|
|
3145
3235
|
}
|
|
3146
3236
|
if (message.op === 'event') {
|
|
3237
|
+
if (message.kind === 'focus') {
|
|
3238
|
+
store.update((current) => current.terminalFocused === message.focused
|
|
3239
|
+
? current
|
|
3240
|
+
: { ...current, terminalFocused: message.focused });
|
|
3241
|
+
renderCurrentFrame();
|
|
3242
|
+
return;
|
|
3243
|
+
}
|
|
3147
3244
|
if (message.kind === 'resize') {
|
|
3148
3245
|
terminalCols = message.cols;
|
|
3149
3246
|
terminalRows = message.rows;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const INITIAL_TERMINAL_FOCUS = true;
|
|
2
|
+
export function createAttentionController(options) {
|
|
3
|
+
let previous = null;
|
|
4
|
+
let outstanding = false;
|
|
5
|
+
return {
|
|
6
|
+
sync(state, enabled) {
|
|
7
|
+
const last = previous;
|
|
8
|
+
previous = { ...state };
|
|
9
|
+
if (last && !last.focused && state.focused && outstanding) {
|
|
10
|
+
outstanding = false;
|
|
11
|
+
options.withdraw();
|
|
12
|
+
}
|
|
13
|
+
if (!last || !enabled)
|
|
14
|
+
return;
|
|
15
|
+
if (state.focused)
|
|
16
|
+
return;
|
|
17
|
+
const turnFinished = last.busy && !state.busy;
|
|
18
|
+
const nowBlocked = !last.awaitingAnswer && state.awaitingAnswer;
|
|
19
|
+
if (nowBlocked || turnFinished) {
|
|
20
|
+
outstanding = true;
|
|
21
|
+
options.notify(nowBlocked ? 'blocked' : 'finished');
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { createInterface } from 'node:readline';
|
|
3
|
-
import { existsSync } from 'node:fs';
|
|
3
|
+
import { existsSync, statSync } from 'node:fs';
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
@@ -51,6 +51,9 @@ function normalizeChildMessage(raw) {
|
|
|
51
51
|
url: String(raw.url ?? ''),
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
|
+
if (raw.kind === 'focus') {
|
|
55
|
+
return { op: 'event', kind: 'focus', focused: raw.focused === true };
|
|
56
|
+
}
|
|
54
57
|
if (raw.kind === 'contextMenu') {
|
|
55
58
|
return { op: 'event', kind: 'contextMenu' };
|
|
56
59
|
}
|
|
@@ -95,13 +98,18 @@ function normalizeChildMessage(raw) {
|
|
|
95
98
|
pageDown: bool(raw.pageDown ?? raw.page_down),
|
|
96
99
|
};
|
|
97
100
|
}
|
|
101
|
+
function isRepoCheckout(moduleDir) {
|
|
102
|
+
return [
|
|
103
|
+
path.join(moduleDir, '../../../../thegitai-tui/Cargo.toml'),
|
|
104
|
+
path.join(moduleDir, '../../../../../thegitai-tui/Cargo.toml'),
|
|
105
|
+
].some((candidate) => existsSync(candidate));
|
|
106
|
+
}
|
|
98
107
|
export function resolveTuiBinaryPath() {
|
|
99
108
|
const binaryName = process.platform === 'win32' ? 'thegitai-tui.exe' : 'thegitai-tui';
|
|
100
109
|
const platformPackage = `@thegitai/tui-${process.platform}-${process.arch}`;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
catch {
|
|
110
|
+
const override = String(process.env.THEGITAI_TUI_BINARY ?? '').trim();
|
|
111
|
+
if (override && existsSync(override)) {
|
|
112
|
+
return override;
|
|
105
113
|
}
|
|
106
114
|
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
107
115
|
const devCandidates = [
|
|
@@ -109,6 +117,24 @@ export function resolveTuiBinaryPath() {
|
|
|
109
117
|
path.join(moduleDir, '../../../../bin', binaryName),
|
|
110
118
|
];
|
|
111
119
|
const devBinary = devCandidates.find((candidate) => existsSync(candidate));
|
|
120
|
+
let publishedBinary = null;
|
|
121
|
+
try {
|
|
122
|
+
publishedBinary = requireFromHere.resolve(`${platformPackage}/${binaryName}`);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
}
|
|
126
|
+
if (publishedBinary && devBinary && isRepoCheckout(moduleDir)) {
|
|
127
|
+
try {
|
|
128
|
+
if (statSync(devBinary).mtimeMs > statSync(publishedBinary).mtimeMs) {
|
|
129
|
+
return devBinary;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (publishedBinary) {
|
|
136
|
+
return publishedBinary;
|
|
137
|
+
}
|
|
112
138
|
if (devBinary) {
|
|
113
139
|
return devBinary;
|
|
114
140
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { agentModeColor, agentModeLabelTiers, agentModeShortLabel, } from '../../agent-mode.js';
|
|
2
2
|
import { singleLinePreview, truncate } from '../../utils.js';
|
|
3
3
|
import { formatClientTokenUsage } from '../repl.js';
|
|
4
4
|
import { renderFormattedBodyLines, renderPreformattedBodyLines, } from './markdown-render.js';
|
|
5
5
|
import { displayWidth, line, padToWidth, plainLine, sliceToWidth, span, wrapText, } from './text.js';
|
|
6
6
|
import { isDarkTerminalBackground, mutedColor, mutedStyle } from './terminal-theme.js';
|
|
7
|
+
import { buildShortcutGroups, shortcutsPanelAvailable, } from './shortcuts.js';
|
|
7
8
|
import { buildUserInputOverlayLines, } from './user-input.js';
|
|
8
9
|
import { composerPromptLabel, frameContentWidth, splitComposerInput, } from './composer-layout.js';
|
|
9
10
|
const WORKING_CLOCK_ICON = '◷';
|
|
@@ -28,7 +29,20 @@ const APPROVAL_SCROLLBAR_THUMB = '█';
|
|
|
28
29
|
const APPROVAL_SCROLLBAR_TRACK = '░';
|
|
29
30
|
const THINKING_NOTE_PREVIEW_ROWS = 3;
|
|
30
31
|
const COMPOSER_INPUT_MAX_ROWS = 6;
|
|
31
|
-
const
|
|
32
|
+
const FOOTER_HINT_MIN_GAP = 2;
|
|
33
|
+
const FOOTER_HELP_HINT = '? for shortcuts';
|
|
34
|
+
const FOOTER_CANCEL_HINT = 'esc cancels turn';
|
|
35
|
+
const FOOTER_NOTIFICATIONS_ON = 'Notifications ON';
|
|
36
|
+
const FOOTER_NOTIFICATIONS_OFF = 'Notifications OFF';
|
|
37
|
+
export function footerNotificationHint(status) {
|
|
38
|
+
const text = String(status ?? '').trim();
|
|
39
|
+
if (text === 'Notifications on')
|
|
40
|
+
return FOOTER_NOTIFICATIONS_ON;
|
|
41
|
+
if (text === 'Notifications off')
|
|
42
|
+
return FOOTER_NOTIFICATIONS_OFF;
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
const FOOTER_CYCLE_HINT = ' (shift+tab to cycle)';
|
|
32
46
|
const OVERLAY_PANEL_MAX_WIDTH = 86;
|
|
33
47
|
const OVERLAY_PANEL_MARGIN_LINES = 2;
|
|
34
48
|
const OVERLAY_PANEL_MARGIN_MIN_ROWS = 30;
|
|
@@ -176,10 +190,12 @@ export function formatPromptSessionIdLabel(showSessionId, sessionId) {
|
|
|
176
190
|
}
|
|
177
191
|
const CLIENT_SLASH_COMMANDS = [
|
|
178
192
|
{ command: '/help', description: 'Show available chat commands' },
|
|
193
|
+
{ command: '/about', description: 'Show version and platform info' },
|
|
179
194
|
{ command: '/usage', description: 'Show account usage percentage and reset times' },
|
|
180
195
|
{ command: '/model', description: 'Switch the active model' },
|
|
181
196
|
{ command: '/resume', description: 'Open the session picker to resume a previous session' },
|
|
182
197
|
{ command: '/jobs', description: 'Background jobs: pick to view output or kill' },
|
|
198
|
+
{ command: '/notify', description: 'Toggle desktop notifications when a turn ends (Ctrl+G)' },
|
|
183
199
|
{ command: '/report', description: 'Report a bug privately on the TheGitAI website' },
|
|
184
200
|
{ command: '/new', description: 'start a new conversation; this session remains saved' },
|
|
185
201
|
{ command: '/logout', description: 'Sign out and quit' },
|
|
@@ -434,10 +450,14 @@ function tokenUsageLines(usage) {
|
|
|
434
450
|
}
|
|
435
451
|
return [line(...spans)];
|
|
436
452
|
}
|
|
453
|
+
export const AGENT_MODE_LOCKED_STATUS = 'Mode locked while the agent is working';
|
|
437
454
|
function footerTransientStatus(status) {
|
|
438
455
|
const text = String(status ?? '').trim();
|
|
439
456
|
if (!text)
|
|
440
457
|
return null;
|
|
458
|
+
if (text === AGENT_MODE_LOCKED_STATUS) {
|
|
459
|
+
return { color: 'yellow', text, icon: '!' };
|
|
460
|
+
}
|
|
441
461
|
if (text === 'Copied selection' || text === 'Copied link') {
|
|
442
462
|
return { color: 'green', text, icon: '✓' };
|
|
443
463
|
}
|
|
@@ -478,9 +498,47 @@ function todoIndicatorSpans(state) {
|
|
|
478
498
|
}),
|
|
479
499
|
];
|
|
480
500
|
}
|
|
481
|
-
function
|
|
501
|
+
function composerFooterRightHints(state) {
|
|
502
|
+
const cancelHint = state.busy && !state.queuedMessage && !state.userInputPrompt
|
|
503
|
+
? FOOTER_CANCEL_HINT
|
|
504
|
+
: '';
|
|
505
|
+
const helpHint = shortcutsPanelAvailable(state) ? FOOTER_HELP_HINT : '';
|
|
506
|
+
const notifyHint = footerNotificationHint(state.transientStatus);
|
|
507
|
+
const join = (...parts) => parts.filter(Boolean).join(' · ');
|
|
508
|
+
const tiers = [];
|
|
509
|
+
if (notifyHint) {
|
|
510
|
+
tiers.push(join(cancelHint, notifyHint, helpHint));
|
|
511
|
+
tiers.push(join(notifyHint, helpHint));
|
|
512
|
+
tiers.push(join(cancelHint, notifyHint));
|
|
513
|
+
tiers.push(notifyHint);
|
|
514
|
+
}
|
|
515
|
+
if (cancelHint && helpHint)
|
|
516
|
+
tiers.push(`${cancelHint} · ${helpHint}`);
|
|
517
|
+
if (cancelHint)
|
|
518
|
+
tiers.push(cancelHint);
|
|
519
|
+
if (helpHint)
|
|
520
|
+
tiers.push(helpHint);
|
|
521
|
+
tiers.push('');
|
|
522
|
+
return tiers.filter((tier, index) => tiers.indexOf(tier) === index);
|
|
523
|
+
}
|
|
524
|
+
export function fitComposerFooterRow({ modeLabels, modeLabelFallback, cycleHint, rightHints, leadWidth, tailWidth, contentWidth, }) {
|
|
525
|
+
const candidates = rightHints.flatMap((rightHint) => (cycleHint ? [cycleHint, ''] : ['']).flatMap((cycle) => modeLabels.map((modeLabel) => ({ modeLabel, cycleHint: cycle, rightHint }))));
|
|
526
|
+
const hasTail = (rightHint) => Boolean(rightHint) || tailWidth > 0;
|
|
527
|
+
const rowWidth = (choice, gap) => leadWidth +
|
|
528
|
+
displayWidth(choice.modeLabel) +
|
|
529
|
+
displayWidth(choice.cycleHint) +
|
|
530
|
+
(hasTail(choice.rightHint) ? gap : 0) +
|
|
531
|
+
displayWidth(choice.rightHint) +
|
|
532
|
+
tailWidth;
|
|
533
|
+
const chosen = candidates.find((choice) => rowWidth(choice, FOOTER_HINT_MIN_GAP) <= contentWidth) ?? { modeLabel: modeLabelFallback, cycleHint: '', rightHint: '' };
|
|
534
|
+
const gap = hasTail(chosen.rightHint)
|
|
535
|
+
? Math.max(FOOTER_HINT_MIN_GAP, contentWidth - rowWidth(chosen, 0))
|
|
536
|
+
: 0;
|
|
537
|
+
return { ...chosen, gap };
|
|
538
|
+
}
|
|
539
|
+
function composerFooterLines(state, contentWidth) {
|
|
482
540
|
const visibleSessionId = formatPromptSessionIdLabel(state.showSessionId, state.sessionId);
|
|
483
|
-
const transientStatus = footerTransientStatus(state.
|
|
541
|
+
const transientStatus = footerTransientStatus(state.transientStatus ?? null);
|
|
484
542
|
const lines = [
|
|
485
543
|
line(span('TheGitAI', { color: 'cyan', bold: true }), span(' • ', { color: 'gray', dim: true }), span(formatPromptDirectoryLabel(state.projectRoot), { color: 'cyan' }), ...(transientStatus
|
|
486
544
|
? [
|
|
@@ -498,11 +556,6 @@ function composerFooterLines(state) {
|
|
|
498
556
|
? [span(` ${visibleSessionId}`, { color: 'gray', dim: true })]
|
|
499
557
|
: []), ...backgroundJobIndicatorSpans(state), ...todoIndicatorSpans(state)),
|
|
500
558
|
plainLine(''),
|
|
501
|
-
plainLine(formatModelLabel(state.currentModelId, state.serverModels), {
|
|
502
|
-
color: 'cyan',
|
|
503
|
-
dim: true,
|
|
504
|
-
}),
|
|
505
|
-
plainLine(''),
|
|
506
559
|
];
|
|
507
560
|
if (state.exitConfirmUntil != null) {
|
|
508
561
|
lines.push(plainLine(state.status || 'Press Ctrl+C again to quit.', {
|
|
@@ -511,34 +564,36 @@ function composerFooterLines(state) {
|
|
|
511
564
|
}));
|
|
512
565
|
return lines;
|
|
513
566
|
}
|
|
514
|
-
const busyHelperText = state.queuedMessage
|
|
515
|
-
? 'Enter sends it to the agent • ↑ edit • Esc / Ctrl+C discard'
|
|
516
|
-
: state.input
|
|
517
|
-
? 'Enter queues • Esc cancels turn • Ctrl+C clears draft'
|
|
518
|
-
: 'Enter queues • Esc / Ctrl+C cancel turn';
|
|
519
|
-
const helperText = state.userInputPrompt
|
|
520
|
-
? 'Answering agent questions • Ctrl+C cancels turn'
|
|
521
|
-
: state.busy
|
|
522
|
-
? busyHelperText
|
|
523
|
-
: process.platform === 'win32'
|
|
524
|
-
? 'Enter sends • Shift+Tab mode • Alt+V image • Esc cancel turn • Ctrl+C clears / quits'
|
|
525
|
-
: process.platform === 'darwin'
|
|
526
|
-
? 'Enter sends • Shift+Tab mode • Ctrl+V image • Esc cancel turn • Ctrl+C clears / quits'
|
|
527
|
-
: 'Enter sends • Shift+Tab mode • Ctrl+V image • Esc cancel turn • Ctrl+C clears / quits';
|
|
528
|
-
const agentLabel = agentModeLabel(state.agentMode).padEnd(AGENT_MODE_LABEL_WIDTH);
|
|
529
567
|
const tokenUsageText = state.tokenUsage || formatClientTokenUsage(null);
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
];
|
|
539
|
-
lines.push(line(...footerSpans));
|
|
568
|
+
lines.push(composerFooterModeLine(state, contentWidth, formatModelLabel(state.currentModelId, state.serverModels), tokenUsageText
|
|
569
|
+
? displayWidth(tokenUsageText) + FOOTER_USAGE_SEPARATOR.length
|
|
570
|
+
: 0, tokenUsageText
|
|
571
|
+
? [
|
|
572
|
+
span(FOOTER_USAGE_SEPARATOR, mutedStyle()),
|
|
573
|
+
span(tokenUsageText, { color: 'cyan' }),
|
|
574
|
+
]
|
|
575
|
+
: []));
|
|
540
576
|
return lines;
|
|
541
577
|
}
|
|
578
|
+
function composerFooterModeLine(state, contentWidth, modelLabel, usageWidth, usageSpans) {
|
|
579
|
+
const lead = modelLabel ? `${modelLabel}${FOOTER_MODEL_SEPARATOR}` : '';
|
|
580
|
+
const row = fitComposerFooterRow({
|
|
581
|
+
modeLabels: agentModeLabelTiers(state.agentMode),
|
|
582
|
+
modeLabelFallback: agentModeShortLabel(state.agentMode),
|
|
583
|
+
cycleHint: state.busy ? '' : FOOTER_CYCLE_HINT,
|
|
584
|
+
rightHints: composerFooterRightHints(state),
|
|
585
|
+
leadWidth: displayWidth(lead),
|
|
586
|
+
tailWidth: usageWidth,
|
|
587
|
+
contentWidth,
|
|
588
|
+
});
|
|
589
|
+
const footerMuted = mutedStyle();
|
|
590
|
+
return line(...(lead ? [span(lead, { color: 'cyan', dim: true })] : []), span(row.modeLabel, {
|
|
591
|
+
color: agentModeColor(state.agentMode),
|
|
592
|
+
bold: state.agentMode === 'auto-accept',
|
|
593
|
+
}), ...(row.cycleHint ? [span(row.cycleHint, footerMuted)] : []), ...(row.gap ? [span(' '.repeat(row.gap), footerMuted)] : []), ...(row.rightHint ? [span(row.rightHint, footerMuted)] : []), ...usageSpans);
|
|
594
|
+
}
|
|
595
|
+
const FOOTER_USAGE_SEPARATOR = ' ';
|
|
596
|
+
const FOOTER_MODEL_SEPARATOR = ' ';
|
|
542
597
|
export function formatJobElapsed(elapsedMs) {
|
|
543
598
|
const totalSeconds = Math.max(0, Math.floor(elapsedMs / 1000));
|
|
544
599
|
if (totalSeconds < 60)
|
|
@@ -895,6 +950,71 @@ function buildModelPickerPanel(options, selectedIndex, width, availableHeight) {
|
|
|
895
950
|
body.push(modelPickerDivider(panelWidth), modelPickerPanelSideLine(plainLine(fitLine(hint, innerWidth), { color: 'gray' }), innerWidth), plainLine(`╰${'─'.repeat(panelWidth - 2)}╯`, { color: MODEL_PICKER_BORDER_COLOR }));
|
|
896
951
|
return [...margin, ...body, ...margin];
|
|
897
952
|
}
|
|
953
|
+
const SHORTCUTS_PANEL_MAX_WIDTH = 96;
|
|
954
|
+
const SHORTCUTS_KEY_COLUMN_MIN = 10;
|
|
955
|
+
function shortcutsTopBorder(panelWidth) {
|
|
956
|
+
const fullTitle = ' TheGitAI - Keyboard & mouse ';
|
|
957
|
+
const compactTitle = ' Shortcuts ';
|
|
958
|
+
const title = panelWidth >= fullTitle.length + 4 ? fullTitle : compactTitle;
|
|
959
|
+
const available = Math.max(0, panelWidth - 2 - [...title].length);
|
|
960
|
+
const left = Math.floor(available / 2);
|
|
961
|
+
const right = available - left;
|
|
962
|
+
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 }));
|
|
963
|
+
}
|
|
964
|
+
export function buildShortcutsPanel(groups, width, availableHeight) {
|
|
965
|
+
const panelWidth = Math.max(28, Math.min(width, SHORTCUTS_PANEL_MAX_WIDTH));
|
|
966
|
+
const innerWidth = Math.max(1, panelWidth - 4);
|
|
967
|
+
const keyWidth = Math.max(SHORTCUTS_KEY_COLUMN_MIN, ...groups.flatMap((group) => group.rows.map((row) => displayWidth(row.keys))));
|
|
968
|
+
const side = (row) => overlayPanelLine(row, innerWidth, MODEL_PICKER_BORDER_COLOR);
|
|
969
|
+
const groupLines = (group, spaced) => [
|
|
970
|
+
...(spaced ? [side(plainLine(''))] : []),
|
|
971
|
+
side(plainLine(fitLine(group.title, innerWidth), {
|
|
972
|
+
color: MODEL_PICKER_ACCENT_COLOR,
|
|
973
|
+
bold: true,
|
|
974
|
+
})),
|
|
975
|
+
...group.rows.map((row) => side(line(span(` ${row.keys}${' '.repeat(Math.max(0, keyWidth - displayWidth(row.keys)))} `, { bold: true }), span(fitLine(row.description, Math.max(1, innerWidth - keyWidth - 4)), { color: 'gray' })))),
|
|
976
|
+
];
|
|
977
|
+
const chromeRows = 4;
|
|
978
|
+
const budget = Math.max(1, availableHeight - chromeRows);
|
|
979
|
+
const spacedBody = groups.flatMap((group, index) => groupLines(group, index > 0));
|
|
980
|
+
const plainBody = groups.flatMap((group) => groupLines(group, false));
|
|
981
|
+
let body = spacedBody;
|
|
982
|
+
let hidden = 0;
|
|
983
|
+
if (spacedBody.length > budget) {
|
|
984
|
+
body = plainBody;
|
|
985
|
+
if (plainBody.length > budget) {
|
|
986
|
+
body = [];
|
|
987
|
+
hidden = groups.reduce((total, group) => total + group.rows.length, 0);
|
|
988
|
+
for (const group of groups) {
|
|
989
|
+
const whole = groupLines(group, false);
|
|
990
|
+
if (body.length + whole.length <= budget) {
|
|
991
|
+
body = [...body, ...whole];
|
|
992
|
+
hidden -= group.rows.length;
|
|
993
|
+
continue;
|
|
994
|
+
}
|
|
995
|
+
const room = budget - body.length - 1;
|
|
996
|
+
if (room >= 1) {
|
|
997
|
+
const kept = group.rows.slice(0, room);
|
|
998
|
+
body = [...body, ...groupLines({ ...group, rows: kept }, false)];
|
|
999
|
+
hidden -= kept.length;
|
|
1000
|
+
}
|
|
1001
|
+
break;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
const hint = hidden > 0 ? `esc close • ${hidden} more in /help` : 'esc close';
|
|
1006
|
+
return [
|
|
1007
|
+
shortcutsTopBorder(panelWidth),
|
|
1008
|
+
...body,
|
|
1009
|
+
plainLine(`├${'─'.repeat(panelWidth - 2)}┤`, {
|
|
1010
|
+
color: MODEL_PICKER_BORDER_COLOR,
|
|
1011
|
+
}),
|
|
1012
|
+
side(plainLine(fitLine(hint, innerWidth), { color: 'gray' })),
|
|
1013
|
+
plainLine(`╰${'─'.repeat(panelWidth - 2)}╯`, {
|
|
1014
|
+
color: MODEL_PICKER_BORDER_COLOR,
|
|
1015
|
+
}),
|
|
1016
|
+
];
|
|
1017
|
+
}
|
|
898
1018
|
function commandPaletteTopBorder(panelWidth) {
|
|
899
1019
|
const prefix = '╭─ Commands ';
|
|
900
1020
|
const suffix = '╮';
|
|
@@ -1134,6 +1254,9 @@ function buildOverlayLines(state, width, height, nowMs) {
|
|
|
1134
1254
|
}));
|
|
1135
1255
|
return buildOverlayPanel(lines, width, OVERLAY_BORDER_COLOR, height);
|
|
1136
1256
|
}
|
|
1257
|
+
if (state.shortcutsOpen) {
|
|
1258
|
+
lines.push(...buildShortcutsPanel(buildShortcutGroups(), width, height));
|
|
1259
|
+
}
|
|
1137
1260
|
if (state.modelPickerOpen) {
|
|
1138
1261
|
const options = buildModelPickerOptions(state.currentModelId, state.serverModels);
|
|
1139
1262
|
lines.push(...buildModelPickerPanel(options, state.modelPickerIndex, width, height));
|
|
@@ -1212,7 +1335,7 @@ function countSectionLines(sections) {
|
|
|
1212
1335
|
export function userInputViewportForFrame(state, cols, rows, spinnerFrame, elapsedSeconds, nowMs = 0) {
|
|
1213
1336
|
const contentWidth = frameContentWidth(cols);
|
|
1214
1337
|
const liveRows = buildLiveLines(state, contentWidth, spinnerFrame, elapsedSeconds, nowMs).length;
|
|
1215
|
-
const overlayHeight = Math.max(0, rows - liveRows - composerFooterLines(state).length);
|
|
1338
|
+
const overlayHeight = Math.max(0, rows - liveRows - composerFooterLines(state, contentWidth).length);
|
|
1216
1339
|
return {
|
|
1217
1340
|
width: approvalPanelInnerWidth(contentWidth),
|
|
1218
1341
|
maxRows: overlayPanelContentBudget(overlayHeight),
|
|
@@ -1248,7 +1371,11 @@ export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds, n
|
|
|
1248
1371
|
sections.push({ kind: 'live', lines: liveLines });
|
|
1249
1372
|
}
|
|
1250
1373
|
const overlayActive = Boolean(state.approvalPrompt || state.sudoPrompt || state.userInputPrompt);
|
|
1251
|
-
if (!state.resumePickerOpen &&
|
|
1374
|
+
if (!state.resumePickerOpen &&
|
|
1375
|
+
!state.modelPickerOpen &&
|
|
1376
|
+
!state.jobsPickerOpen &&
|
|
1377
|
+
!state.shortcutsOpen &&
|
|
1378
|
+
!overlayActive) {
|
|
1252
1379
|
const composerLines = [];
|
|
1253
1380
|
if (state.busy && state.status === 'Starting a new conversation...') {
|
|
1254
1381
|
composerLines.push(line(span('Starting a new conversation…', { color: 'gray', dim: true })));
|
|
@@ -1268,18 +1395,18 @@ export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds, n
|
|
|
1268
1395
|
}
|
|
1269
1396
|
sections.push({
|
|
1270
1397
|
kind: 'composer',
|
|
1271
|
-
lines: [...composerLines, plainLine(''), ...composerFooterLines(state)],
|
|
1398
|
+
lines: [...composerLines, plainLine(''), ...composerFooterLines(state, contentWidth)],
|
|
1272
1399
|
});
|
|
1273
1400
|
}
|
|
1274
1401
|
if (state.userInputPrompt) {
|
|
1275
1402
|
sections.push({
|
|
1276
1403
|
kind: 'busyFooter',
|
|
1277
|
-
lines: composerFooterLines(state),
|
|
1404
|
+
lines: composerFooterLines(state, contentWidth),
|
|
1278
1405
|
});
|
|
1279
1406
|
}
|
|
1280
1407
|
const overlayHeight = state.userInputPrompt
|
|
1281
1408
|
? Math.max(0, rows - countSectionLines(sections))
|
|
1282
|
-
: state.jobsPickerOpen
|
|
1409
|
+
: state.jobsPickerOpen || state.shortcutsOpen
|
|
1283
1410
|
? Math.max(Math.min(rows, JOBS_PICKER_MIN_ROWS), rows - countSectionLines(sections))
|
|
1284
1411
|
: rows;
|
|
1285
1412
|
const overlayLines = buildOverlayLines(state, contentWidth, overlayHeight, nowMs);
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
function run(command, args, { capture = false } = {}) {
|
|
3
|
+
return new Promise((resolve) => {
|
|
4
|
+
let child;
|
|
5
|
+
try {
|
|
6
|
+
child = spawn(command, args, {
|
|
7
|
+
stdio: ['ignore', capture ? 'pipe' : 'ignore', 'ignore'],
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
resolve('');
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
let out = '';
|
|
15
|
+
child.stdout?.on('data', (chunk) => {
|
|
16
|
+
out += chunk.toString('utf8');
|
|
17
|
+
});
|
|
18
|
+
child.on('error', () => resolve(''));
|
|
19
|
+
child.on('close', () => resolve(out.trim()));
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
const linuxBackend = {
|
|
23
|
+
canWithdraw: true,
|
|
24
|
+
async post(request, previous) {
|
|
25
|
+
const args = [
|
|
26
|
+
'--print-id',
|
|
27
|
+
'--app-name=TheGitAI',
|
|
28
|
+
'--icon=utilities-terminal',
|
|
29
|
+
'--urgency=normal',
|
|
30
|
+
];
|
|
31
|
+
if (previous)
|
|
32
|
+
args.push(`--replace-id=${previous}`);
|
|
33
|
+
args.push(request.title, request.body);
|
|
34
|
+
const id = await run('notify-send', args, { capture: true });
|
|
35
|
+
return /^\d+$/.test(id) ? id : null;
|
|
36
|
+
},
|
|
37
|
+
withdraw(handle) {
|
|
38
|
+
if (!handle)
|
|
39
|
+
return;
|
|
40
|
+
void run('gdbus', [
|
|
41
|
+
'call',
|
|
42
|
+
'--session',
|
|
43
|
+
'--dest',
|
|
44
|
+
'org.freedesktop.Notifications',
|
|
45
|
+
'--object-path',
|
|
46
|
+
'/org/freedesktop/Notifications',
|
|
47
|
+
'--method',
|
|
48
|
+
'org.freedesktop.Notifications.CloseNotification',
|
|
49
|
+
handle,
|
|
50
|
+
]);
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
function appleScriptString(value) {
|
|
54
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
55
|
+
}
|
|
56
|
+
const macBackend = {
|
|
57
|
+
canWithdraw: false,
|
|
58
|
+
async post(request) {
|
|
59
|
+
await run('osascript', [
|
|
60
|
+
'-e',
|
|
61
|
+
`display notification "${appleScriptString(request.body)}" with title "${appleScriptString(request.title)}"`,
|
|
62
|
+
]);
|
|
63
|
+
return null;
|
|
64
|
+
},
|
|
65
|
+
withdraw() {
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
function powerShellString(value) {
|
|
69
|
+
return value.replace(/'/g, "''");
|
|
70
|
+
}
|
|
71
|
+
const WINDOWS_AUMID = 'TheGitAI';
|
|
72
|
+
const WINDOWS_TAG = 'thegitai-attention';
|
|
73
|
+
const WINDOWS_GROUP = 'thegitai';
|
|
74
|
+
const windowsBackend = {
|
|
75
|
+
canWithdraw: true,
|
|
76
|
+
async post(request) {
|
|
77
|
+
const script = [
|
|
78
|
+
'[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType=WindowsRuntime] > $null',
|
|
79
|
+
'[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom, ContentType=WindowsRuntime] > $null',
|
|
80
|
+
'$t = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)',
|
|
81
|
+
'$n = $t.GetElementsByTagName("text")',
|
|
82
|
+
`$n.Item(0).AppendChild($t.CreateTextNode('${powerShellString(request.title)}')) > $null`,
|
|
83
|
+
`$n.Item(1).AppendChild($t.CreateTextNode('${powerShellString(request.body)}')) > $null`,
|
|
84
|
+
'$toast = [Windows.UI.Notifications.ToastNotification]::new($t)',
|
|
85
|
+
`$toast.Tag = '${WINDOWS_TAG}'`,
|
|
86
|
+
`$toast.Group = '${WINDOWS_GROUP}'`,
|
|
87
|
+
`[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('${WINDOWS_AUMID}').Show($toast)`,
|
|
88
|
+
].join('; ');
|
|
89
|
+
await run('powershell', ['-NoProfile', '-NonInteractive', '-Command', script]);
|
|
90
|
+
return WINDOWS_TAG;
|
|
91
|
+
},
|
|
92
|
+
withdraw(handle) {
|
|
93
|
+
if (!handle)
|
|
94
|
+
return;
|
|
95
|
+
const script = [
|
|
96
|
+
'[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType=WindowsRuntime] > $null',
|
|
97
|
+
`[Windows.UI.Notifications.ToastNotificationManager]::History.Remove('${powerShellString(handle)}', '${WINDOWS_GROUP}', '${WINDOWS_AUMID}')`,
|
|
98
|
+
].join('; ');
|
|
99
|
+
void run('powershell', ['-NoProfile', '-NonInteractive', '-Command', script]);
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
export function createNotificationQueue() {
|
|
103
|
+
let tail = Promise.resolve();
|
|
104
|
+
return {
|
|
105
|
+
idle: () => tail,
|
|
106
|
+
run(step) {
|
|
107
|
+
tail = tail.then(step).catch(() => {
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export function backendFor(platform = process.platform) {
|
|
113
|
+
if (platform === 'darwin')
|
|
114
|
+
return macBackend;
|
|
115
|
+
if (platform === 'win32')
|
|
116
|
+
return windowsBackend;
|
|
117
|
+
return linuxBackend;
|
|
118
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { shortcutsPanelAvailable } from './shortcuts.js';
|
|
1
2
|
import { moveComposerCursorVertically } from './composer-layout.js';
|
|
2
3
|
import { readClipboardImage, readClipboardText } from '../../core/clipboard.js';
|
|
3
4
|
import { MAX_IMAGES_PER_MESSAGE, MAX_TOTAL_IMAGE_BYTES_PER_MESSAGE, approximateBase64DecodedBytes, totalAttachmentBytes, } from '../../core/image-limits.js';
|
|
@@ -105,7 +106,7 @@ function pasteTextFromClipboard(store, handlers) {
|
|
|
105
106
|
if (!text) {
|
|
106
107
|
store.update((state) => ({
|
|
107
108
|
...state,
|
|
108
|
-
|
|
109
|
+
transientStatus: 'Clipboard has no text to paste',
|
|
109
110
|
}));
|
|
110
111
|
return;
|
|
111
112
|
}
|
|
@@ -415,12 +416,28 @@ export function handleShellKeyEvent(store, handlers, event) {
|
|
|
415
416
|
}
|
|
416
417
|
return;
|
|
417
418
|
}
|
|
419
|
+
if (state.shortcutsOpen) {
|
|
420
|
+
if (key.escape || key.returnKey || key.input === '?' || key.input === 'q') {
|
|
421
|
+
handlers.onLiveFrameShapeChange();
|
|
422
|
+
store.update((current) => ({ ...current, shortcutsOpen: false }));
|
|
423
|
+
}
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
418
426
|
if (key.ctrl && key.input === 'b' && !key.meta && !key.shift) {
|
|
419
427
|
if ((state.backgroundJobs ?? []).length > 0) {
|
|
420
428
|
handlers.onOpenJobsPicker();
|
|
421
429
|
}
|
|
422
430
|
return;
|
|
423
431
|
}
|
|
432
|
+
if (key.ctrl && key.input === 'g' && !key.meta && !key.shift) {
|
|
433
|
+
handlers.onToggleNotifications?.();
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (key.input === '?' && !key.ctrl && !key.meta && shortcutsPanelAvailable(state)) {
|
|
437
|
+
handlers.onLiveFrameShapeChange();
|
|
438
|
+
store.update((current) => ({ ...current, shortcutsOpen: true }));
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
424
441
|
if (key.escape) {
|
|
425
442
|
if (state.busy) {
|
|
426
443
|
if (state.queuedMessage) {
|
|
@@ -449,6 +466,10 @@ export function handleShellKeyEvent(store, handlers, event) {
|
|
|
449
466
|
return;
|
|
450
467
|
}
|
|
451
468
|
if (key.tab && key.shift) {
|
|
469
|
+
if (state.busy) {
|
|
470
|
+
handlers.onAgentModeChangeBlocked?.();
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
452
473
|
handlers.onCycleAgentMode();
|
|
453
474
|
return;
|
|
454
475
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { agentModeCycleSequence } from '../../agent-mode.js';
|
|
2
|
+
export function shortcutsPanelAvailable(state) {
|
|
3
|
+
return (!state.exiting &&
|
|
4
|
+
!state.approvalPrompt &&
|
|
5
|
+
!state.sudoPrompt &&
|
|
6
|
+
!state.userInputPrompt &&
|
|
7
|
+
!state.queuedMessage &&
|
|
8
|
+
!state.input &&
|
|
9
|
+
(state.cursor ?? 0) === 0 &&
|
|
10
|
+
(state.pastedChunks?.length ?? 0) === 0 &&
|
|
11
|
+
state.promptHistoryCursor == null);
|
|
12
|
+
}
|
|
13
|
+
export function buildShortcutGroups(platform = process.platform) {
|
|
14
|
+
const imagePaste = platform === 'win32' ? 'Alt+V' : 'Ctrl+V';
|
|
15
|
+
const textPaste = platform === 'darwin' ? 'Cmd+V' : 'Ctrl+Shift+V';
|
|
16
|
+
return [
|
|
17
|
+
{
|
|
18
|
+
title: 'Composer',
|
|
19
|
+
rows: [
|
|
20
|
+
{ keys: 'Enter', description: 'Send your message' },
|
|
21
|
+
{
|
|
22
|
+
keys: 'Shift+Tab',
|
|
23
|
+
description: `Cycle ${agentModeCycleSequence()} (between turns)`,
|
|
24
|
+
},
|
|
25
|
+
{ keys: '/', description: 'Slash commands — /help lists them all' },
|
|
26
|
+
{ keys: '↑ / ↓', description: 'Earlier prompts' },
|
|
27
|
+
{ keys: 'Esc', description: 'Clear the composer' },
|
|
28
|
+
{ keys: 'Ctrl+C', description: 'Clear the composer, or quit when empty' },
|
|
29
|
+
{ keys: 'Ctrl+G', description: 'Desktop notifications — toggle on or off' },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
title: 'While the agent is working',
|
|
34
|
+
rows: [
|
|
35
|
+
{ keys: 'Enter', description: 'Queue a message; Enter again sends it into the turn' },
|
|
36
|
+
{ keys: '↑', description: 'Bring a queued message back to edit it' },
|
|
37
|
+
{ keys: 'Esc', description: 'Discard the queued message, or cancel the turn' },
|
|
38
|
+
{ keys: 'Ctrl+C', description: 'Cancel the turn' },
|
|
39
|
+
{ keys: 'Ctrl+B', description: 'Background jobs' },
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
title: 'Clipboard & images',
|
|
44
|
+
rows: [
|
|
45
|
+
{ keys: imagePaste, description: 'Paste a screenshot into the message' },
|
|
46
|
+
{ keys: textPaste, description: 'Paste text (your terminal’s own shortcut)' },
|
|
47
|
+
{ keys: 'Right-click', description: 'Paste, or copy the link under the cursor' },
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
title: 'Transcript',
|
|
52
|
+
rows: [
|
|
53
|
+
{ keys: 'PageUp / PageDown', description: 'Scroll — the mouse wheel works too' },
|
|
54
|
+
{ keys: 'Drag', description: 'Select and copy' },
|
|
55
|
+
{ keys: 'Double-click', description: 'Copy a word' },
|
|
56
|
+
{ keys: 'Triple-click', description: 'Copy a whole row' },
|
|
57
|
+
{ keys: 'Left-click', description: 'Open a link' },
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
}
|
package/dist/src/usage.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
const USAGE_WINDOWS = [
|
|
1
|
+
export const USAGE_WINDOWS = [
|
|
2
2
|
['5-hour', '5-hourly'],
|
|
3
3
|
['Weekly', 'weekly'],
|
|
4
4
|
['Monthly', 'monthly'],
|
|
5
5
|
];
|
|
6
|
-
function
|
|
6
|
+
export function usageWindowLabel(key) {
|
|
7
|
+
return USAGE_WINDOWS.find(([, id]) => id === key)?.[0] ?? '';
|
|
8
|
+
}
|
|
9
|
+
export function formatUtcMinute(value) {
|
|
7
10
|
if (!value)
|
|
8
11
|
return 'no active usage';
|
|
9
12
|
const date = new Date(value);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thegitai/cli",
|
|
3
|
-
"version": "1.0.0-preview.
|
|
3
|
+
"version": "1.0.0-preview.43",
|
|
4
4
|
"description": "TheGitAI is an agentic AI coding tool for your terminal. It reads and searches your repository, writes and edits files, runs your tests, and verifies the change before handing it back.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agentic-ai",
|
|
@@ -44,11 +44,11 @@
|
|
|
44
44
|
"@lydell/node-pty-linux-x64": "1.1.0",
|
|
45
45
|
"@lydell/node-pty-win32-arm64": "1.1.0",
|
|
46
46
|
"@lydell/node-pty-win32-x64": "1.1.0",
|
|
47
|
-
"@thegitai/tui-darwin-arm64": "1.0.0-preview.
|
|
48
|
-
"@thegitai/tui-darwin-x64": "1.0.0-preview.
|
|
49
|
-
"@thegitai/tui-linux-arm64": "1.0.0-preview.
|
|
50
|
-
"@thegitai/tui-linux-x64": "1.0.0-preview.
|
|
51
|
-
"@thegitai/tui-win32-x64": "1.0.0-preview.
|
|
47
|
+
"@thegitai/tui-darwin-arm64": "1.0.0-preview.43",
|
|
48
|
+
"@thegitai/tui-darwin-x64": "1.0.0-preview.43",
|
|
49
|
+
"@thegitai/tui-linux-arm64": "1.0.0-preview.43",
|
|
50
|
+
"@thegitai/tui-linux-x64": "1.0.0-preview.43",
|
|
51
|
+
"@thegitai/tui-win32-x64": "1.0.0-preview.43",
|
|
52
52
|
"@vscode/ripgrep": "1.18.0"
|
|
53
53
|
},
|
|
54
54
|
"repository": {
|