@thegitai/cli 1.0.0-preview.35 → 1.0.0-preview.37
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/background-jobs.js +2 -0
- package/dist/src/edit-journal.js +33 -2
- package/dist/src/help-text.js +5 -2
- package/dist/src/patcher.js +43 -4
- package/dist/src/tools/delete-file.js +5 -1
- package/dist/src/ui/repl.js +122 -40
- package/dist/src/ui/tui/build-frame.js +29 -7
- package/dist/src/ui/tui/shell-input.js +10 -2
- package/package.json +6 -6
|
@@ -265,6 +265,7 @@ export function getJobOutputTail(id, maxLines) {
|
|
|
265
265
|
if (!record || !belongsToSession(record) || maxLines <= 0)
|
|
266
266
|
return [];
|
|
267
267
|
const lines = sanitizeJobText(record, record.buffer)
|
|
268
|
+
.replace(/\r\n?/g, '\n')
|
|
268
269
|
.split('\n')
|
|
269
270
|
.filter((line) => line.trim().length > 0);
|
|
270
271
|
return lines.slice(-maxLines);
|
|
@@ -277,6 +278,7 @@ export function getJobOutputPreview(id, maxTailLines) {
|
|
|
277
278
|
? sanitizeJobText(record, record.firstOutputLine).trim()
|
|
278
279
|
: '';
|
|
279
280
|
return {
|
|
281
|
+
droppedChars: Math.max(record.totalCaptured - record.buffer.length, 0),
|
|
280
282
|
firstLine,
|
|
281
283
|
tailLines: getJobOutputTail(id, maxTailLines),
|
|
282
284
|
};
|
package/dist/src/edit-journal.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
-
import { existsSync, lstatSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { closeSync, existsSync, lstatSync, openSync, readFileSync, readSync, } from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { BINARY_ARTIFACT_EXTENSIONS } from './artifact-policy.js';
|
|
6
6
|
import { resolveProjectPath } from './patcher.js';
|
|
@@ -30,13 +30,33 @@ function snapshotEncoding(filePath, content) {
|
|
|
30
30
|
return 'base64';
|
|
31
31
|
return 'utf8';
|
|
32
32
|
}
|
|
33
|
+
const MAX_SNAPSHOT_READ_BYTES = MAX_STORED_CONTENT_CHARS;
|
|
34
|
+
const SNAPSHOT_HASH_CHUNK_BYTES = 1024 * 1024;
|
|
35
|
+
function hashFileInChunks(absPath) {
|
|
36
|
+
const hash = createHash('sha256');
|
|
37
|
+
const chunk = Buffer.allocUnsafe(SNAPSHOT_HASH_CHUNK_BYTES);
|
|
38
|
+
const fd = openSync(absPath, 'r');
|
|
39
|
+
try {
|
|
40
|
+
for (;;) {
|
|
41
|
+
const read = readSync(fd, chunk, 0, chunk.length, null);
|
|
42
|
+
if (read <= 0)
|
|
43
|
+
break;
|
|
44
|
+
hash.update(chunk.subarray(0, read));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
closeSync(fd);
|
|
49
|
+
}
|
|
50
|
+
return `sha256:${hash.digest('hex')}`;
|
|
51
|
+
}
|
|
33
52
|
export function readFileEditSnapshot(rootDir, filePath) {
|
|
34
53
|
try {
|
|
35
54
|
const absPath = resolveProjectPath(rootDir, filePath);
|
|
36
55
|
if (!existsSync(absPath)) {
|
|
37
56
|
return { exists: false, content: null, contentEncoding: 'utf8', hash: null };
|
|
38
57
|
}
|
|
39
|
-
|
|
58
|
+
const stat = lstatSync(absPath);
|
|
59
|
+
if (stat.isSymbolicLink()) {
|
|
40
60
|
return {
|
|
41
61
|
exists: true,
|
|
42
62
|
content: null,
|
|
@@ -45,6 +65,17 @@ export function readFileEditSnapshot(rootDir, filePath) {
|
|
|
45
65
|
error: `Refusing to snapshot symbolic link: ${filePath}`,
|
|
46
66
|
};
|
|
47
67
|
}
|
|
68
|
+
if (stat.isFile() && stat.size > MAX_SNAPSHOT_READ_BYTES) {
|
|
69
|
+
return {
|
|
70
|
+
exists: true,
|
|
71
|
+
content: null,
|
|
72
|
+
contentEncoding: BINARY_ARTIFACT_EXTENSIONS.has(path.extname(filePath).toLowerCase())
|
|
73
|
+
? 'base64'
|
|
74
|
+
: 'utf8',
|
|
75
|
+
hash: hashFileInChunks(absPath),
|
|
76
|
+
error: `File is too large to snapshot (${stat.size} bytes).`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
48
79
|
const content = readFileSync(absPath);
|
|
49
80
|
const encoding = snapshotEncoding(filePath, content);
|
|
50
81
|
return {
|
package/dist/src/help-text.js
CHANGED
|
@@ -92,6 +92,8 @@ const HELP_MARKDOWN = [
|
|
|
92
92
|
'- `/resume` — resume a saved session for this repo',
|
|
93
93
|
'- `/jobs` — manage long-running commands like dev servers and watchers:',
|
|
94
94
|
' browse them, press Enter to expand one and read its output, k to stop it',
|
|
95
|
+
'- Ctrl+B — open the same jobs picker, and the only way in while the agent',
|
|
96
|
+
' is working; press it again or Esc to close without touching the turn',
|
|
95
97
|
'- `/jobs output <id>` — print one job\'s full captured output',
|
|
96
98
|
'- `/jobs kill <id>` — stop one background job',
|
|
97
99
|
'- `/new` — start a new conversation; this session remains saved',
|
|
@@ -130,8 +132,9 @@ const HELP_MARKDOWN = [
|
|
|
130
132
|
' jobs after the same approval as any other command. Background output',
|
|
131
133
|
' stays quiet once the model has responded; the footer shows only a compact',
|
|
132
134
|
' shell-running indicator, `/jobs` lists, inspects, and kills jobs, and',
|
|
133
|
-
' killed jobs disappear immediately.
|
|
134
|
-
'
|
|
135
|
+
' killed jobs disappear immediately. While a turn is running the indicator',
|
|
136
|
+
' points at Ctrl+B instead, which opens the same picker without cancelling',
|
|
137
|
+
' or interrupting the turn. Every job is killed when the session ends.',
|
|
135
138
|
'- File and shell operations are confined to the target repo root.',
|
|
136
139
|
'- Sensitive directories (`.git`, `node_modules`, build output) are',
|
|
137
140
|
' excluded from search and listing.',
|
package/dist/src/patcher.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from './colors.js';
|
|
2
|
-
import { chmodSync, closeSync, constants, existsSync, fchmodSync, fstatSync, ftruncateSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, unlinkSync, writeFileSync, } from 'fs';
|
|
2
|
+
import { chmodSync, closeSync, constants, existsSync, fchmodSync, fstatSync, ftruncateSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, realpathSync, unlinkSync, writeFileSync, } from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { createInterface } from 'readline';
|
|
5
5
|
import { runCommand } from './executor.js';
|
|
@@ -271,22 +271,61 @@ export function writeProjectFileBuffer(rootDir, filePath, content) {
|
|
|
271
271
|
}
|
|
272
272
|
return { absPath, changed: true };
|
|
273
273
|
}
|
|
274
|
+
const MAX_DELETED_CONTENT_BYTES = 64 * 1024;
|
|
275
|
+
const DELETED_BINARY_SNIFF_BYTES = 4096;
|
|
276
|
+
function headHasNulByte(absPath) {
|
|
277
|
+
const chunk = Buffer.allocUnsafe(DELETED_BINARY_SNIFF_BYTES);
|
|
278
|
+
let fd = null;
|
|
279
|
+
try {
|
|
280
|
+
fd = openSync(absPath, 'r');
|
|
281
|
+
const read = readSync(fd, chunk, 0, chunk.length, 0);
|
|
282
|
+
return read > 0 && chunk.subarray(0, read).includes(0);
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
finally {
|
|
288
|
+
if (fd !== null)
|
|
289
|
+
closeSync(fd);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
274
292
|
export function deleteProjectFile(rootDir, filePath) {
|
|
275
293
|
const absPath = resolveProjectPath(rootDir, filePath);
|
|
276
294
|
if (!existsSync(absPath)) {
|
|
277
295
|
return { deleted: false, absPath };
|
|
278
296
|
}
|
|
297
|
+
let bytes;
|
|
279
298
|
let content;
|
|
299
|
+
let contentOmitted;
|
|
280
300
|
try {
|
|
281
|
-
|
|
282
|
-
|
|
301
|
+
const stat = lstatSync(absPath);
|
|
302
|
+
if (!stat.isSymbolicLink()) {
|
|
303
|
+
bytes = stat.size;
|
|
304
|
+
if (stat.size > MAX_DELETED_CONTENT_BYTES) {
|
|
305
|
+
contentOmitted = headHasNulByte(absPath) ? 'binary' : 'too_large';
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
const buffer = readFileSync(absPath);
|
|
309
|
+
if (buffer.includes(0))
|
|
310
|
+
contentOmitted = 'binary';
|
|
311
|
+
else
|
|
312
|
+
content = buffer.toString('utf-8');
|
|
313
|
+
}
|
|
283
314
|
}
|
|
284
315
|
}
|
|
285
316
|
catch {
|
|
317
|
+
bytes = undefined;
|
|
286
318
|
content = undefined;
|
|
319
|
+
contentOmitted = undefined;
|
|
287
320
|
}
|
|
288
321
|
unlinkSync(absPath);
|
|
289
|
-
return {
|
|
322
|
+
return {
|
|
323
|
+
deleted: true,
|
|
324
|
+
absPath,
|
|
325
|
+
...(bytes === undefined ? {} : { bytes }),
|
|
326
|
+
...(content === undefined ? {} : { content }),
|
|
327
|
+
...(contentOmitted === undefined ? {} : { contentOmitted }),
|
|
328
|
+
};
|
|
290
329
|
}
|
|
291
330
|
export function readProjectFile(rootDir, filePath) {
|
|
292
331
|
const absPath = resolveProjectPath(rootDir, filePath);
|
|
@@ -51,7 +51,11 @@ export async function deleteFile(context, args) {
|
|
|
51
51
|
changed: result.deleted,
|
|
52
52
|
deleted: result.deleted,
|
|
53
53
|
...(scratchPath ? { scratch: true } : {}),
|
|
54
|
-
|
|
54
|
+
...(result.bytes === undefined ? {} : { bytes: result.bytes }),
|
|
55
|
+
...(result.contentOmitted === undefined
|
|
56
|
+
? {}
|
|
57
|
+
: { contentOmitted: result.contentOmitted }),
|
|
58
|
+
...(result.content === undefined ? {} : { content: result.content }),
|
|
55
59
|
diagnostics: result.deleted && !scratchPath ? runShellDiagnostics(rootDir) : undefined,
|
|
56
60
|
};
|
|
57
61
|
}
|
package/dist/src/ui/repl.js
CHANGED
|
@@ -527,11 +527,43 @@ function buildWriteFileDiff(content) {
|
|
|
527
527
|
return '';
|
|
528
528
|
return `@@ -0,0 +1,${lines.length} @@\n${lines.map((line) => `+${line}`).join('\n')}`;
|
|
529
529
|
}
|
|
530
|
+
const MAX_DELETE_PREVIEW_LINES = 400;
|
|
530
531
|
function buildDeleteFileDiff(content) {
|
|
531
532
|
const lines = splitDiffLines(String(content ?? ''));
|
|
532
|
-
if (lines.length === 0)
|
|
533
|
+
if (lines.length === 0) {
|
|
534
|
+
return { removed: 0, text: '' };
|
|
535
|
+
}
|
|
536
|
+
const shown = lines.slice(0, MAX_DELETE_PREVIEW_LINES);
|
|
537
|
+
const rows = [
|
|
538
|
+
`@@ -1,${lines.length} +0,0 @@`,
|
|
539
|
+
...shown.map((line) => `-${line}`),
|
|
540
|
+
];
|
|
541
|
+
const omitted = lines.length - shown.length;
|
|
542
|
+
if (omitted > 0) {
|
|
543
|
+
rows.push(`@@ ${omitted} more removed line(s) not shown @@`);
|
|
544
|
+
}
|
|
545
|
+
return { removed: lines.length, text: rows.join('\n') };
|
|
546
|
+
}
|
|
547
|
+
function deletedFileSummary(result) {
|
|
548
|
+
const bytes = typeof result?.bytes === 'number' ? result.bytes : null;
|
|
549
|
+
if (bytes === null) {
|
|
533
550
|
return '';
|
|
534
|
-
|
|
551
|
+
}
|
|
552
|
+
const reason = result?.contentOmitted === 'binary' ? ', binary' : '';
|
|
553
|
+
return ` (${formatFileSize(bytes)}${reason})`;
|
|
554
|
+
}
|
|
555
|
+
function formatFileSize(bytes) {
|
|
556
|
+
if (bytes < 1024) {
|
|
557
|
+
return `${bytes} B`;
|
|
558
|
+
}
|
|
559
|
+
const units = ['KiB', 'MiB', 'GiB', 'TiB'];
|
|
560
|
+
let value = bytes / 1024;
|
|
561
|
+
let unit = 0;
|
|
562
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
563
|
+
value /= 1024;
|
|
564
|
+
unit += 1;
|
|
565
|
+
}
|
|
566
|
+
return `${value >= 10 ? Math.round(value) : value.toFixed(1)} ${units[unit]}`;
|
|
535
567
|
}
|
|
536
568
|
function isFileChangeTool(name) {
|
|
537
569
|
return (name === 'patch_file' ||
|
|
@@ -659,9 +691,11 @@ function buildFileChangeEntry(event) {
|
|
|
659
691
|
};
|
|
660
692
|
}
|
|
661
693
|
const content = typeof result?.content === 'string' ? result.content : '';
|
|
662
|
-
const
|
|
663
|
-
const preview =
|
|
664
|
-
const summary = preview
|
|
694
|
+
const diff = buildDeleteFileDiff(content);
|
|
695
|
+
const preview = diff.text ? parseDiffPreview(diff.text) : undefined;
|
|
696
|
+
const summary = preview
|
|
697
|
+
? ` (+0 -${diff.removed})`
|
|
698
|
+
: deletedFileSummary(result);
|
|
665
699
|
return {
|
|
666
700
|
body: '',
|
|
667
701
|
diffPreview: preview,
|
|
@@ -923,6 +957,23 @@ function createSessionTranscript(session) {
|
|
|
923
957
|
const transcript = buildTranscriptFromSessionHistory(session.history);
|
|
924
958
|
return transcript.length ? transcript : createInitialTranscript();
|
|
925
959
|
}
|
|
960
|
+
export function isJobsPickerCommand(input) {
|
|
961
|
+
return String(input ?? '').trim() === '/jobs';
|
|
962
|
+
}
|
|
963
|
+
export function statusBehindJobsPicker(state) {
|
|
964
|
+
return state.jobsPickerOpen
|
|
965
|
+
? (state.jobsPickerReturnStatus ?? 'Ready')
|
|
966
|
+
: state.status;
|
|
967
|
+
}
|
|
968
|
+
export function closedJobsPickerFields(state) {
|
|
969
|
+
if (!state.jobsPickerOpen)
|
|
970
|
+
return {};
|
|
971
|
+
return {
|
|
972
|
+
jobsPickerExpandedId: null,
|
|
973
|
+
jobsPickerOpen: false,
|
|
974
|
+
jobsPickerReturnStatus: null,
|
|
975
|
+
};
|
|
976
|
+
}
|
|
926
977
|
function createShellStore(initialState) {
|
|
927
978
|
let state = initialState;
|
|
928
979
|
let nextEntryId = 1;
|
|
@@ -1941,8 +1992,9 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1941
1992
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
1942
1993
|
store.update((current) => ({
|
|
1943
1994
|
...pauseBusyClock(current, Date.now()),
|
|
1995
|
+
...closedJobsPickerFields(current),
|
|
1944
1996
|
status: 'Waiting for your input',
|
|
1945
|
-
userInputPrompt: createUserInputPromptState(questions, current
|
|
1997
|
+
userInputPrompt: createUserInputPromptState(questions, statusBehindJobsPicker(current)),
|
|
1946
1998
|
}));
|
|
1947
1999
|
scheduleLiveFrameRemount();
|
|
1948
2000
|
});
|
|
@@ -1975,6 +2027,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1975
2027
|
resolvePermissionDecision = resolve;
|
|
1976
2028
|
store.update((current) => ({
|
|
1977
2029
|
...pauseBusyClock(current, Date.now()),
|
|
2030
|
+
...closedJobsPickerFields(current),
|
|
1978
2031
|
approvalCursor: getDefaultApprovalCursor(request.options.length),
|
|
1979
2032
|
approvalOpenedAt: Date.now(),
|
|
1980
2033
|
approvalScrollOffset: 0,
|
|
@@ -1986,7 +2039,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1986
2039
|
: undefined,
|
|
1987
2040
|
filePath: request.filePath,
|
|
1988
2041
|
options: request.options,
|
|
1989
|
-
returnStatus: current
|
|
2042
|
+
returnStatus: statusBehindJobsPicker(current),
|
|
1990
2043
|
},
|
|
1991
2044
|
status: request.title,
|
|
1992
2045
|
}));
|
|
@@ -2048,17 +2101,19 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2048
2101
|
});
|
|
2049
2102
|
};
|
|
2050
2103
|
const openJobsPicker = () => {
|
|
2104
|
+
if (store.getState().jobsPickerOpen)
|
|
2105
|
+
return;
|
|
2051
2106
|
syncBackgroundJobsState();
|
|
2052
2107
|
store.update((current) => ({
|
|
2053
2108
|
...current,
|
|
2054
2109
|
commandCursor: 0,
|
|
2055
|
-
cursor: 0,
|
|
2056
|
-
input: '',
|
|
2057
2110
|
jobsPickerExpandedId: null,
|
|
2058
2111
|
jobsPickerIndex: 0,
|
|
2059
2112
|
jobsPickerOpen: true,
|
|
2113
|
+
jobsPickerReturnStatus: current.status,
|
|
2060
2114
|
status: 'Background jobs',
|
|
2061
2115
|
}));
|
|
2116
|
+
scheduleLiveFrameRemount();
|
|
2062
2117
|
};
|
|
2063
2118
|
const appendJobOutputEntry = async (jobId) => {
|
|
2064
2119
|
const id = jobId.trim();
|
|
@@ -2089,30 +2144,51 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2089
2144
|
const index = Math.min(Math.max(current.jobsPickerIndex, 0), current.backgroundJobs.length - 1);
|
|
2090
2145
|
return current.backgroundJobs[index]?.id ?? null;
|
|
2091
2146
|
};
|
|
2147
|
+
const deferredJobMutationIds = new Set();
|
|
2148
|
+
const drainDeferredJobMutations = async () => {
|
|
2149
|
+
if (deferredJobMutationIds.size === 0)
|
|
2150
|
+
return;
|
|
2151
|
+
const pending = [...deferredJobMutationIds];
|
|
2152
|
+
deferredJobMutationIds.clear();
|
|
2153
|
+
for (const jobId of pending) {
|
|
2154
|
+
await collectBackgroundJobUiOutputMutations({ session, jobId });
|
|
2155
|
+
}
|
|
2156
|
+
syncBackgroundJobsState();
|
|
2157
|
+
};
|
|
2092
2158
|
const handleJobsPickerOutput = async () => {
|
|
2093
2159
|
const jobId = selectedJobsPickerId();
|
|
2094
2160
|
if (!jobId)
|
|
2095
2161
|
return;
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2162
|
+
if (store.getState().busy) {
|
|
2163
|
+
deferredJobMutationIds.add(jobId);
|
|
2164
|
+
}
|
|
2165
|
+
else {
|
|
2166
|
+
await collectBackgroundJobUiOutputMutations({
|
|
2167
|
+
session,
|
|
2168
|
+
jobId,
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
2101
2171
|
store.update((current) => ({
|
|
2102
2172
|
...current,
|
|
2103
2173
|
jobsPickerExpandedId: current.jobsPickerExpandedId === jobId ? null : jobId,
|
|
2104
2174
|
}));
|
|
2175
|
+
syncBackgroundJobsState();
|
|
2105
2176
|
};
|
|
2106
2177
|
const handleJobsPickerKill = async () => {
|
|
2107
2178
|
const jobId = selectedJobsPickerId();
|
|
2108
2179
|
if (!jobId)
|
|
2109
2180
|
return;
|
|
2110
2181
|
const killed = await killBackgroundJob(jobId);
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2182
|
+
if (store.getState().busy) {
|
|
2183
|
+
deferredJobMutationIds.add(jobId);
|
|
2184
|
+
}
|
|
2185
|
+
else {
|
|
2186
|
+
await collectBackgroundJobUiKillMutations({
|
|
2187
|
+
session,
|
|
2188
|
+
jobId,
|
|
2189
|
+
result: killed,
|
|
2190
|
+
});
|
|
2191
|
+
}
|
|
2116
2192
|
syncBackgroundJobsState();
|
|
2117
2193
|
if (!killed.ok) {
|
|
2118
2194
|
appendError(killed.error ?? 'Background job kill failed.');
|
|
@@ -2404,8 +2480,18 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2404
2480
|
if (!input || exiting)
|
|
2405
2481
|
return;
|
|
2406
2482
|
if (input.startsWith('/')) {
|
|
2483
|
+
if (isJobsPickerCommand(input)) {
|
|
2484
|
+
store.update((current) => ({
|
|
2485
|
+
...current,
|
|
2486
|
+
cursor: 0,
|
|
2487
|
+
input: '',
|
|
2488
|
+
pastedChunks: [],
|
|
2489
|
+
}));
|
|
2490
|
+
openJobsPicker();
|
|
2491
|
+
return;
|
|
2492
|
+
}
|
|
2407
2493
|
appendTurnAwareEntry({
|
|
2408
|
-
body: "Slash commands can't be queued while a turn is running.",
|
|
2494
|
+
body: "Slash commands can't be queued while a turn is running. Ctrl+B opens background jobs.",
|
|
2409
2495
|
kind: 'system',
|
|
2410
2496
|
title: 'Queued',
|
|
2411
2497
|
});
|
|
@@ -2845,6 +2931,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2845
2931
|
activeServerTurnId = null;
|
|
2846
2932
|
}
|
|
2847
2933
|
recoverUnacknowledgedMessages();
|
|
2934
|
+
await drainDeferredJobMutations();
|
|
2848
2935
|
}
|
|
2849
2936
|
};
|
|
2850
2937
|
session.onImageAnalysis = (activeImageCount) => {
|
|
@@ -2897,26 +2984,20 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2897
2984
|
}
|
|
2898
2985
|
store.appendWorkingTool(buildWorkingToolEntry(event));
|
|
2899
2986
|
};
|
|
2900
|
-
const jobDetailLines = (jobId) => {
|
|
2901
|
-
const buffered = getJobBufferedOutput(jobId);
|
|
2902
|
-
if (!buffered)
|
|
2903
|
-
return [];
|
|
2904
|
-
const lines = buffered.output
|
|
2905
|
-
.replace(/\r\n/g, '\n')
|
|
2906
|
-
.replace(/\r/g, '\n')
|
|
2907
|
-
.split('\n')
|
|
2908
|
-
.filter((line) => line.trim().length > 0);
|
|
2909
|
-
const visible = lines.slice(-20);
|
|
2910
|
-
return buffered.droppedChars > 0
|
|
2911
|
-
? [
|
|
2912
|
-
`... (${buffered.droppedChars} chars of older output dropped) ...`,
|
|
2913
|
-
...visible,
|
|
2914
|
-
]
|
|
2915
|
-
: visible;
|
|
2916
|
-
};
|
|
2917
2987
|
const syncBackgroundJobsState = () => {
|
|
2988
|
+
const expandedId = store.getState().jobsPickerExpandedId;
|
|
2918
2989
|
const jobsForDisplay = listBackgroundJobs().map((job) => {
|
|
2919
|
-
const
|
|
2990
|
+
const expanded = job.id === expandedId;
|
|
2991
|
+
const preview = getJobOutputPreview(job.id, expanded ? 20 : 3);
|
|
2992
|
+
const dropped = preview?.droppedChars ?? 0;
|
|
2993
|
+
const detailLines = !expanded
|
|
2994
|
+
? []
|
|
2995
|
+
: dropped > 0
|
|
2996
|
+
? [
|
|
2997
|
+
`... (${dropped} chars of older output dropped) ...`,
|
|
2998
|
+
...(preview?.tailLines ?? []),
|
|
2999
|
+
]
|
|
3000
|
+
: (preview?.tailLines ?? []);
|
|
2920
3001
|
return {
|
|
2921
3002
|
id: job.id,
|
|
2922
3003
|
command: job.command,
|
|
@@ -2925,8 +3006,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2925
3006
|
startedAt: job.startedAt,
|
|
2926
3007
|
endedAt: job.endedAt,
|
|
2927
3008
|
firstOutputLine: preview?.firstLine ?? '',
|
|
2928
|
-
tailLines: preview?.tailLines ?? [],
|
|
2929
|
-
detailLines
|
|
3009
|
+
tailLines: (preview?.tailLines ?? []).slice(-3),
|
|
3010
|
+
detailLines,
|
|
2930
3011
|
};
|
|
2931
3012
|
});
|
|
2932
3013
|
store.update((current) => ({
|
|
@@ -2996,6 +3077,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2996
3077
|
onResumeSession: handleInlineResumeSelection,
|
|
2997
3078
|
onSelectionCopy: handleAppSelectionCopy,
|
|
2998
3079
|
onSelectModel: handleInlineModelSelection,
|
|
3080
|
+
onOpenJobsPicker: openJobsPicker,
|
|
2999
3081
|
onJobsPickerOutput: handleJobsPickerOutput,
|
|
3000
3082
|
onJobsPickerKill: handleJobsPickerKill,
|
|
3001
3083
|
onSudoPasswordInput: handleSudoPasswordInput,
|
|
@@ -454,10 +454,11 @@ function backgroundJobIndicatorSpans(state) {
|
|
|
454
454
|
if (runningCount === 0)
|
|
455
455
|
return [];
|
|
456
456
|
const noun = runningCount === 1 ? 'shell' : 'shells';
|
|
457
|
+
const hint = state.busy ? ' · ctrl+b' : ' · /jobs';
|
|
457
458
|
return [
|
|
458
459
|
span(' ', { color: 'gray', dim: true }),
|
|
459
460
|
span(`● ${runningCount} ${noun} running`, { color: 'green', bold: true }),
|
|
460
|
-
span(
|
|
461
|
+
span(hint, { color: 'gray', dim: true }),
|
|
461
462
|
];
|
|
462
463
|
}
|
|
463
464
|
function todoIndicatorSpans(state) {
|
|
@@ -579,15 +580,29 @@ function jobStatusDescriptor(job, nowMs) {
|
|
|
579
580
|
? { glyph: '✓', color: 'green', text: `exited (0) · ran ${ran}` }
|
|
580
581
|
: { glyph: '✖', color: 'red', text: `exited (${job.exitCode ?? 1}) · ran ${ran}` };
|
|
581
582
|
}
|
|
582
|
-
|
|
583
|
+
const JOBS_PICKER_CHROME_ROWS = 2;
|
|
584
|
+
const JOBS_PICKER_MIN_ROWS = 8;
|
|
585
|
+
function buildJobsPickerLines(state, width, height, nowMs) {
|
|
583
586
|
const jobs = state.backgroundJobs ?? [];
|
|
587
|
+
const bodyBudget = Math.max(1, height - JOBS_PICKER_CHROME_ROWS);
|
|
584
588
|
const lines = [
|
|
585
589
|
plainLine('Background jobs', { color: 'cyan', bold: true }),
|
|
586
590
|
];
|
|
587
591
|
if (jobs.length === 0) {
|
|
588
592
|
lines.push(plainLine('No background jobs in this session.', { color: 'gray' }));
|
|
589
593
|
}
|
|
590
|
-
|
|
594
|
+
let start = 0;
|
|
595
|
+
let visible = jobs;
|
|
596
|
+
if (jobs.length > bodyBudget) {
|
|
597
|
+
start = Math.min(Math.max(0, state.jobsPickerIndex - Math.floor(bodyBudget / 2)), jobs.length - bodyBudget);
|
|
598
|
+
visible = jobs.slice(start, start + bodyBudget);
|
|
599
|
+
if (start > 0) {
|
|
600
|
+
lines.push(plainLine(` … ${start} newer`, { color: 'gray', dim: true }));
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
const detailBudget = Math.max(0, bodyBudget - visible.length - (start > 0 ? 1 : 0));
|
|
604
|
+
for (const [offset, job] of visible.entries()) {
|
|
605
|
+
const index = start + offset;
|
|
591
606
|
const selected = index === state.jobsPickerIndex;
|
|
592
607
|
const expanded = state.jobsPickerExpandedId === job.id;
|
|
593
608
|
const { glyph, color, text } = jobStatusDescriptor(job, nowMs);
|
|
@@ -596,10 +611,11 @@ function buildJobsPickerLines(state, width, nowMs) {
|
|
|
596
611
|
bold: selected,
|
|
597
612
|
}), span(` ${text}`, { color: 'gray' })));
|
|
598
613
|
if (expanded) {
|
|
599
|
-
const
|
|
614
|
+
const allDetail = (job.detailLines ?? []).length
|
|
600
615
|
? job.detailLines ?? []
|
|
601
616
|
: backgroundJobPreviewLines(job, 3);
|
|
602
|
-
|
|
617
|
+
const detailLines = allDetail.slice(-detailBudget);
|
|
618
|
+
if (detailBudget > 0 && detailLines.length === 0) {
|
|
603
619
|
lines.push(plainLine(' (no output captured)', { color: 'gray' }));
|
|
604
620
|
}
|
|
605
621
|
else {
|
|
@@ -612,6 +628,10 @@ function buildJobsPickerLines(state, width, nowMs) {
|
|
|
612
628
|
}
|
|
613
629
|
}
|
|
614
630
|
}
|
|
631
|
+
const remaining = jobs.length - (start + visible.length);
|
|
632
|
+
if (remaining > 0) {
|
|
633
|
+
lines.push(plainLine(` … ${remaining} older`, { color: 'gray', dim: true }));
|
|
634
|
+
}
|
|
615
635
|
lines.push(plainLine('↑/↓ move enter expand/collapse k kill esc close', {
|
|
616
636
|
color: 'gray',
|
|
617
637
|
}));
|
|
@@ -1119,7 +1139,7 @@ function buildOverlayLines(state, width, height, nowMs) {
|
|
|
1119
1139
|
lines.push(...buildModelPickerPanel(options, state.modelPickerIndex, width, height));
|
|
1120
1140
|
}
|
|
1121
1141
|
if (state.jobsPickerOpen) {
|
|
1122
|
-
lines.push(...buildJobsPickerLines(state, width, nowMs));
|
|
1142
|
+
lines.push(...buildJobsPickerLines(state, width, height, nowMs));
|
|
1123
1143
|
}
|
|
1124
1144
|
if (state.resumePickerOpen) {
|
|
1125
1145
|
const filtered = filterResumeSessions(state.resumePickerSessions, state.resumePickerFilter, state.serverModels);
|
|
@@ -1259,7 +1279,9 @@ export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds, n
|
|
|
1259
1279
|
}
|
|
1260
1280
|
const overlayHeight = state.userInputPrompt
|
|
1261
1281
|
? Math.max(0, rows - countSectionLines(sections))
|
|
1262
|
-
:
|
|
1282
|
+
: state.jobsPickerOpen
|
|
1283
|
+
? Math.max(Math.min(rows, JOBS_PICKER_MIN_ROWS), rows - countSectionLines(sections))
|
|
1284
|
+
: rows;
|
|
1263
1285
|
const overlayLines = buildOverlayLines(state, contentWidth, overlayHeight, nowMs);
|
|
1264
1286
|
if (overlayLines.length > 0) {
|
|
1265
1287
|
sections.push({ kind: 'overlay', lines: overlayLines });
|
|
@@ -320,12 +320,14 @@ export function handleShellKeyEvent(store, handlers, event) {
|
|
|
320
320
|
return;
|
|
321
321
|
}
|
|
322
322
|
if (state.jobsPickerOpen) {
|
|
323
|
-
if (key.escape) {
|
|
323
|
+
if (key.escape || (key.ctrl && key.input === 'b' && !key.meta && !key.shift)) {
|
|
324
|
+
handlers.onLiveFrameShapeChange();
|
|
324
325
|
store.update((current) => ({
|
|
325
326
|
...current,
|
|
326
327
|
jobsPickerExpandedId: null,
|
|
327
328
|
jobsPickerOpen: false,
|
|
328
|
-
|
|
329
|
+
jobsPickerReturnStatus: null,
|
|
330
|
+
status: current.jobsPickerReturnStatus ?? 'Ready',
|
|
329
331
|
}));
|
|
330
332
|
return;
|
|
331
333
|
}
|
|
@@ -413,6 +415,12 @@ export function handleShellKeyEvent(store, handlers, event) {
|
|
|
413
415
|
}
|
|
414
416
|
return;
|
|
415
417
|
}
|
|
418
|
+
if (key.ctrl && key.input === 'b' && !key.meta && !key.shift) {
|
|
419
|
+
if ((state.backgroundJobs ?? []).length > 0) {
|
|
420
|
+
handlers.onOpenJobsPicker();
|
|
421
|
+
}
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
416
424
|
if (key.escape) {
|
|
417
425
|
if (state.busy) {
|
|
418
426
|
if (state.queuedMessage) {
|
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.37",
|
|
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.37",
|
|
48
|
+
"@thegitai/tui-darwin-x64": "1.0.0-preview.37",
|
|
49
|
+
"@thegitai/tui-linux-arm64": "1.0.0-preview.37",
|
|
50
|
+
"@thegitai/tui-linux-x64": "1.0.0-preview.37",
|
|
51
|
+
"@thegitai/tui-win32-x64": "1.0.0-preview.37",
|
|
52
52
|
"@vscode/ripgrep": "1.18.0"
|
|
53
53
|
},
|
|
54
54
|
"repository": {
|