@thegitai/cli 1.0.0-preview.42 → 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/dist/src/help-text.js +6 -0
- package/dist/src/preferences.js +39 -0
- package/dist/src/ui/repl.js +75 -1
- 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 +21 -1
- package/dist/src/ui/tui/desktop-notification.js +118 -0
- package/dist/src/ui/tui/shell-input.js +4 -0
- package/dist/src/ui/tui/shortcuts.js +1 -0
- package/package.json +6 -6
package/dist/src/help-text.js
CHANGED
|
@@ -77,6 +77,11 @@ const HELP_MARKDOWN = [
|
|
|
77
77
|
' back for editing and **Esc**/**Ctrl+C** discards it; both unlock the',
|
|
78
78
|
' composer for the next message. **Ctrl+V** pastes a screenshot onto the',
|
|
79
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.',
|
|
80
85
|
`- **Paste** into the composer with your terminal's paste shortcut (\`${PASTE_SHORTCUT}\``,
|
|
81
86
|
' on this system) or by right-clicking the composer.',
|
|
82
87
|
'- **Copy** from the transcript by dragging to select; double-click copies a',
|
|
@@ -101,6 +106,7 @@ const HELP_MARKDOWN = [
|
|
|
101
106
|
' is working; press it again or Esc to close without touching the turn',
|
|
102
107
|
'- `/jobs output <id>` — print one job\'s full captured output',
|
|
103
108
|
'- `/jobs kill <id>` — stop one background job',
|
|
109
|
+
'- `/notify` — turn desktop notifications on or off (same as Ctrl+G)',
|
|
104
110
|
'- `/new` — start a new conversation; this session remains saved',
|
|
105
111
|
'- `/logout` — sign out and quit',
|
|
106
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
|
+
}
|
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
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';
|
|
@@ -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,6 +1108,8 @@ 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,
|
|
@@ -1557,11 +1567,42 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1557
1567
|
busy: state.busy,
|
|
1558
1568
|
});
|
|
1559
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
|
+
};
|
|
1560
1600
|
const renderCurrentFrame = () => {
|
|
1561
1601
|
if (!tuiReady)
|
|
1562
1602
|
return;
|
|
1563
1603
|
const state = store.getState();
|
|
1564
1604
|
syncTerminalTitle();
|
|
1605
|
+
syncAttention();
|
|
1565
1606
|
const elapsedSeconds = busyElapsedSeconds(state, Date.now());
|
|
1566
1607
|
bridge.render(buildTuiFrame(state, terminalCols, terminalRows, spinnerFrame, elapsedSeconds, Date.now()));
|
|
1567
1608
|
};
|
|
@@ -2107,6 +2148,16 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2107
2148
|
title: 'Model',
|
|
2108
2149
|
});
|
|
2109
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
|
+
};
|
|
2110
2161
|
const openJobsPicker = () => {
|
|
2111
2162
|
if (store.getState().jobsPickerOpen)
|
|
2112
2163
|
return;
|
|
@@ -2513,8 +2564,19 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2513
2564
|
openJobsPicker();
|
|
2514
2565
|
return;
|
|
2515
2566
|
}
|
|
2567
|
+
if (input === '/notify') {
|
|
2568
|
+
store.update((current) => ({
|
|
2569
|
+
...current,
|
|
2570
|
+
cursor: 0,
|
|
2571
|
+
input: '',
|
|
2572
|
+
pastedChunks: [],
|
|
2573
|
+
}));
|
|
2574
|
+
toggleNotifications();
|
|
2575
|
+
return;
|
|
2576
|
+
}
|
|
2516
2577
|
appendTurnAwareEntry({
|
|
2517
|
-
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.',
|
|
2518
2580
|
kind: 'system',
|
|
2519
2581
|
title: 'Queued',
|
|
2520
2582
|
});
|
|
@@ -2599,6 +2661,10 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2599
2661
|
appendStaticEntries(buildReportTranscriptEntries(opened));
|
|
2600
2662
|
return;
|
|
2601
2663
|
}
|
|
2664
|
+
if (input === '/notify') {
|
|
2665
|
+
toggleNotifications();
|
|
2666
|
+
return;
|
|
2667
|
+
}
|
|
2602
2668
|
if (input === '/jobs' || input.startsWith('/jobs ')) {
|
|
2603
2669
|
const jobsArgs = input.slice('/jobs'.length).trim();
|
|
2604
2670
|
if (!jobsArgs) {
|
|
@@ -3124,6 +3190,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
3124
3190
|
onSelectionCopy: handleAppSelectionCopy,
|
|
3125
3191
|
onSelectModel: handleInlineModelSelection,
|
|
3126
3192
|
onOpenJobsPicker: openJobsPicker,
|
|
3193
|
+
onToggleNotifications: toggleNotifications,
|
|
3127
3194
|
onJobsPickerOutput: handleJobsPickerOutput,
|
|
3128
3195
|
onJobsPickerKill: handleJobsPickerKill,
|
|
3129
3196
|
onSudoPasswordInput: handleSudoPasswordInput,
|
|
@@ -3167,6 +3234,13 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
3167
3234
|
return;
|
|
3168
3235
|
}
|
|
3169
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
|
+
}
|
|
3170
3244
|
if (message.kind === 'resize') {
|
|
3171
3245
|
terminalCols = message.cols;
|
|
3172
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
|
}
|
|
@@ -32,6 +32,16 @@ const COMPOSER_INPUT_MAX_ROWS = 6;
|
|
|
32
32
|
const FOOTER_HINT_MIN_GAP = 2;
|
|
33
33
|
const FOOTER_HELP_HINT = '? for shortcuts';
|
|
34
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
|
+
}
|
|
35
45
|
const FOOTER_CYCLE_HINT = ' (shift+tab to cycle)';
|
|
36
46
|
const OVERLAY_PANEL_MAX_WIDTH = 86;
|
|
37
47
|
const OVERLAY_PANEL_MARGIN_LINES = 2;
|
|
@@ -180,10 +190,12 @@ export function formatPromptSessionIdLabel(showSessionId, sessionId) {
|
|
|
180
190
|
}
|
|
181
191
|
const CLIENT_SLASH_COMMANDS = [
|
|
182
192
|
{ command: '/help', description: 'Show available chat commands' },
|
|
193
|
+
{ command: '/about', description: 'Show version and platform info' },
|
|
183
194
|
{ command: '/usage', description: 'Show account usage percentage and reset times' },
|
|
184
195
|
{ command: '/model', description: 'Switch the active model' },
|
|
185
196
|
{ command: '/resume', description: 'Open the session picker to resume a previous session' },
|
|
186
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)' },
|
|
187
199
|
{ command: '/report', description: 'Report a bug privately on the TheGitAI website' },
|
|
188
200
|
{ command: '/new', description: 'start a new conversation; this session remains saved' },
|
|
189
201
|
{ command: '/logout', description: 'Sign out and quit' },
|
|
@@ -491,7 +503,15 @@ function composerFooterRightHints(state) {
|
|
|
491
503
|
? FOOTER_CANCEL_HINT
|
|
492
504
|
: '';
|
|
493
505
|
const helpHint = shortcutsPanelAvailable(state) ? FOOTER_HELP_HINT : '';
|
|
506
|
+
const notifyHint = footerNotificationHint(state.transientStatus);
|
|
507
|
+
const join = (...parts) => parts.filter(Boolean).join(' · ');
|
|
494
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
|
+
}
|
|
495
515
|
if (cancelHint && helpHint)
|
|
496
516
|
tiers.push(`${cancelHint} · ${helpHint}`);
|
|
497
517
|
if (cancelHint)
|
|
@@ -499,7 +519,7 @@ function composerFooterRightHints(state) {
|
|
|
499
519
|
if (helpHint)
|
|
500
520
|
tiers.push(helpHint);
|
|
501
521
|
tiers.push('');
|
|
502
|
-
return tiers;
|
|
522
|
+
return tiers.filter((tier, index) => tiers.indexOf(tier) === index);
|
|
503
523
|
}
|
|
504
524
|
export function fitComposerFooterRow({ modeLabels, modeLabelFallback, cycleHint, rightHints, leadWidth, tailWidth, contentWidth, }) {
|
|
505
525
|
const candidates = rightHints.flatMap((rightHint) => (cycleHint ? [cycleHint, ''] : ['']).flatMap((cycle) => modeLabels.map((modeLabel) => ({ modeLabel, cycleHint: cycle, rightHint }))));
|
|
@@ -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
|
+
}
|
|
@@ -429,6 +429,10 @@ export function handleShellKeyEvent(store, handlers, event) {
|
|
|
429
429
|
}
|
|
430
430
|
return;
|
|
431
431
|
}
|
|
432
|
+
if (key.ctrl && key.input === 'g' && !key.meta && !key.shift) {
|
|
433
|
+
handlers.onToggleNotifications?.();
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
432
436
|
if (key.input === '?' && !key.ctrl && !key.meta && shortcutsPanelAvailable(state)) {
|
|
433
437
|
handlers.onLiveFrameShapeChange();
|
|
434
438
|
store.update((current) => ({ ...current, shortcutsOpen: true }));
|
|
@@ -26,6 +26,7 @@ export function buildShortcutGroups(platform = process.platform) {
|
|
|
26
26
|
{ keys: '↑ / ↓', description: 'Earlier prompts' },
|
|
27
27
|
{ keys: 'Esc', description: 'Clear the composer' },
|
|
28
28
|
{ keys: 'Ctrl+C', description: 'Clear the composer, or quit when empty' },
|
|
29
|
+
{ keys: 'Ctrl+G', description: 'Desktop notifications — toggle on or off' },
|
|
29
30
|
],
|
|
30
31
|
},
|
|
31
32
|
{
|
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": {
|