@thegitai/cli 1.0.0-preview.34 → 1.0.0-preview.36
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/api/chat.js +2 -0
- package/dist/src/background-jobs.js +2 -0
- package/dist/src/core/session-image-store.js +47 -0
- package/dist/src/help-text.js +5 -2
- package/dist/src/ui/repl.js +103 -37
- package/dist/src/ui/tui/build-frame.js +80 -14
- package/dist/src/ui/tui/shell-input.js +10 -2
- package/package.json +6 -6
package/dist/src/api/chat.js
CHANGED
|
@@ -7,6 +7,7 @@ import { isUserInputQuestionArray } from './contracts.js';
|
|
|
7
7
|
import { createTraceContext, gatewayFailureCategory, normalizeServerUrl, readErrorResponse, } from './http.js';
|
|
8
8
|
import { collectClientEnvironment } from '../client-environment.js';
|
|
9
9
|
import { collectProjectOrientation } from '../project-orientation.js';
|
|
10
|
+
import { describeSessionImageStore } from '../core/session-image-store.js';
|
|
10
11
|
import { autoAttachImages } from '../core/image-path-extractor.js';
|
|
11
12
|
import { formatTurnFailureMarker } from '../turn-failure-marker.js';
|
|
12
13
|
export class TurnCancelledError extends Error {
|
|
@@ -546,6 +547,7 @@ export async function sendServerUserMessage({ config, session, input, imageAttac
|
|
|
546
547
|
backgroundJobUpdate: backgroundJobUpdate || undefined,
|
|
547
548
|
clientEnvironment: collectClientEnvironment({ env: session.env }),
|
|
548
549
|
projectOrientation: collectProjectOrientation(session.rootDir) ?? undefined,
|
|
550
|
+
sessionImageStore: describeSessionImageStore(session.env) ?? undefined,
|
|
549
551
|
imageAttachments: imageAttachmentsForServer(requestImageAttachments),
|
|
550
552
|
maxToolSteps: session.maxToolSteps,
|
|
551
553
|
autoYes: session.autoYes,
|
|
@@ -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
|
};
|
|
@@ -111,6 +111,53 @@ export function readSessionImageByIndex(index, env = process.env) {
|
|
|
111
111
|
}
|
|
112
112
|
return null;
|
|
113
113
|
}
|
|
114
|
+
export function describeSessionImageStore(env = process.env) {
|
|
115
|
+
if (!activeSessionId)
|
|
116
|
+
return null;
|
|
117
|
+
const dir = getSessionImageDir(activeSessionId, env);
|
|
118
|
+
if (!existsSync(dir))
|
|
119
|
+
return null;
|
|
120
|
+
const indices = [];
|
|
121
|
+
try {
|
|
122
|
+
for (const name of readdirSync(dir)) {
|
|
123
|
+
const parsed = Number.parseInt(path.basename(name, path.extname(name)), 10);
|
|
124
|
+
if (Number.isInteger(parsed) && parsed >= 1)
|
|
125
|
+
indices.push(parsed);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
if (indices.length === 0)
|
|
132
|
+
return null;
|
|
133
|
+
return { dir, indices: indices.sort((a, b) => a - b) };
|
|
134
|
+
}
|
|
135
|
+
const MAX_STORE_DIR_CHARS = 512;
|
|
136
|
+
const MAX_STORE_INDICES = 200;
|
|
137
|
+
const STORE_CONTROL_CHARS = /[\u0000-\u001f\u007f]/g;
|
|
138
|
+
export function normalizeSessionImageStore(value) {
|
|
139
|
+
if (!value || typeof value !== 'object')
|
|
140
|
+
return null;
|
|
141
|
+
const record = value;
|
|
142
|
+
const dir = String(record.dir ?? '')
|
|
143
|
+
.replace(STORE_CONTROL_CHARS, ' ')
|
|
144
|
+
.trim()
|
|
145
|
+
.slice(0, MAX_STORE_DIR_CHARS);
|
|
146
|
+
if (!dir)
|
|
147
|
+
return null;
|
|
148
|
+
const seen = new Set();
|
|
149
|
+
for (const raw of Array.isArray(record.indices) ? record.indices : []) {
|
|
150
|
+
const parsed = Number(raw);
|
|
151
|
+
if (!Number.isInteger(parsed) || parsed < 1)
|
|
152
|
+
continue;
|
|
153
|
+
seen.add(parsed);
|
|
154
|
+
if (seen.size >= MAX_STORE_INDICES)
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
if (seen.size === 0)
|
|
158
|
+
return null;
|
|
159
|
+
return { dir, indices: [...seen].sort((a, b) => a - b) };
|
|
160
|
+
}
|
|
114
161
|
export class SessionImageError extends Error {
|
|
115
162
|
code;
|
|
116
163
|
constructor(message, code) {
|
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/ui/repl.js
CHANGED
|
@@ -148,6 +148,17 @@ export const SLASH_COMMANDS = [
|
|
|
148
148
|
description: 'Quit the current session',
|
|
149
149
|
},
|
|
150
150
|
];
|
|
151
|
+
export function composerAttachmentsAfterCancel(queuedAttachments, inFlightAttachments) {
|
|
152
|
+
const seen = new Set();
|
|
153
|
+
const kept = [];
|
|
154
|
+
for (const attachment of [...queuedAttachments, ...inFlightAttachments]) {
|
|
155
|
+
if (seen.has(attachment.index))
|
|
156
|
+
continue;
|
|
157
|
+
seen.add(attachment.index);
|
|
158
|
+
kept.push(attachment);
|
|
159
|
+
}
|
|
160
|
+
return kept;
|
|
161
|
+
}
|
|
151
162
|
function getShellWidth(columns) {
|
|
152
163
|
const safeColumns = Math.max(columns, 20);
|
|
153
164
|
const targetWidth = Math.floor(safeColumns * TUI_WIDTH_RATIO);
|
|
@@ -912,6 +923,23 @@ function createSessionTranscript(session) {
|
|
|
912
923
|
const transcript = buildTranscriptFromSessionHistory(session.history);
|
|
913
924
|
return transcript.length ? transcript : createInitialTranscript();
|
|
914
925
|
}
|
|
926
|
+
export function isJobsPickerCommand(input) {
|
|
927
|
+
return String(input ?? '').trim() === '/jobs';
|
|
928
|
+
}
|
|
929
|
+
export function statusBehindJobsPicker(state) {
|
|
930
|
+
return state.jobsPickerOpen
|
|
931
|
+
? (state.jobsPickerReturnStatus ?? 'Ready')
|
|
932
|
+
: state.status;
|
|
933
|
+
}
|
|
934
|
+
export function closedJobsPickerFields(state) {
|
|
935
|
+
if (!state.jobsPickerOpen)
|
|
936
|
+
return {};
|
|
937
|
+
return {
|
|
938
|
+
jobsPickerExpandedId: null,
|
|
939
|
+
jobsPickerOpen: false,
|
|
940
|
+
jobsPickerReturnStatus: null,
|
|
941
|
+
};
|
|
942
|
+
}
|
|
915
943
|
function createShellStore(initialState) {
|
|
916
944
|
let state = initialState;
|
|
917
945
|
let nextEntryId = 1;
|
|
@@ -1414,6 +1442,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1414
1442
|
let latestUsageSummary = null;
|
|
1415
1443
|
let pendingTurnEntries = [];
|
|
1416
1444
|
let activeTurnAbort = null;
|
|
1445
|
+
let activeTurnImageAttachments = [];
|
|
1417
1446
|
let activeServerTurnId = null;
|
|
1418
1447
|
let unacknowledged = new Map();
|
|
1419
1448
|
const blockedRows = new WeakMap();
|
|
@@ -1675,6 +1704,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1675
1704
|
activeTurnAbort = null;
|
|
1676
1705
|
cancelActiveCommand();
|
|
1677
1706
|
const queued = store.getState().queuedMessage;
|
|
1707
|
+
const restoredAttachments = composerAttachmentsAfterCancel(queued ? queued.imageAttachments : [], activeTurnImageAttachments);
|
|
1708
|
+
activeTurnImageAttachments = [];
|
|
1678
1709
|
const cancelledEntries = [
|
|
1679
1710
|
...takePendingTurnEntries(),
|
|
1680
1711
|
{
|
|
@@ -1692,7 +1723,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1692
1723
|
busySince: null,
|
|
1693
1724
|
commandLog: [],
|
|
1694
1725
|
cursor: queued ? queued.body.length : current.cursor,
|
|
1695
|
-
imageAttachments:
|
|
1726
|
+
imageAttachments: restoredAttachments,
|
|
1696
1727
|
input: queued ? queued.body : current.input,
|
|
1697
1728
|
pastedChunks: queued ? queued.pastedChunks : current.pastedChunks,
|
|
1698
1729
|
promptHistoryCursor: queued ? null : current.promptHistoryCursor,
|
|
@@ -1927,8 +1958,9 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1927
1958
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
1928
1959
|
store.update((current) => ({
|
|
1929
1960
|
...pauseBusyClock(current, Date.now()),
|
|
1961
|
+
...closedJobsPickerFields(current),
|
|
1930
1962
|
status: 'Waiting for your input',
|
|
1931
|
-
userInputPrompt: createUserInputPromptState(questions, current
|
|
1963
|
+
userInputPrompt: createUserInputPromptState(questions, statusBehindJobsPicker(current)),
|
|
1932
1964
|
}));
|
|
1933
1965
|
scheduleLiveFrameRemount();
|
|
1934
1966
|
});
|
|
@@ -1961,6 +1993,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1961
1993
|
resolvePermissionDecision = resolve;
|
|
1962
1994
|
store.update((current) => ({
|
|
1963
1995
|
...pauseBusyClock(current, Date.now()),
|
|
1996
|
+
...closedJobsPickerFields(current),
|
|
1964
1997
|
approvalCursor: getDefaultApprovalCursor(request.options.length),
|
|
1965
1998
|
approvalOpenedAt: Date.now(),
|
|
1966
1999
|
approvalScrollOffset: 0,
|
|
@@ -1972,7 +2005,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1972
2005
|
: undefined,
|
|
1973
2006
|
filePath: request.filePath,
|
|
1974
2007
|
options: request.options,
|
|
1975
|
-
returnStatus: current
|
|
2008
|
+
returnStatus: statusBehindJobsPicker(current),
|
|
1976
2009
|
},
|
|
1977
2010
|
status: request.title,
|
|
1978
2011
|
}));
|
|
@@ -2034,17 +2067,19 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2034
2067
|
});
|
|
2035
2068
|
};
|
|
2036
2069
|
const openJobsPicker = () => {
|
|
2070
|
+
if (store.getState().jobsPickerOpen)
|
|
2071
|
+
return;
|
|
2037
2072
|
syncBackgroundJobsState();
|
|
2038
2073
|
store.update((current) => ({
|
|
2039
2074
|
...current,
|
|
2040
2075
|
commandCursor: 0,
|
|
2041
|
-
cursor: 0,
|
|
2042
|
-
input: '',
|
|
2043
2076
|
jobsPickerExpandedId: null,
|
|
2044
2077
|
jobsPickerIndex: 0,
|
|
2045
2078
|
jobsPickerOpen: true,
|
|
2079
|
+
jobsPickerReturnStatus: current.status,
|
|
2046
2080
|
status: 'Background jobs',
|
|
2047
2081
|
}));
|
|
2082
|
+
scheduleLiveFrameRemount();
|
|
2048
2083
|
};
|
|
2049
2084
|
const appendJobOutputEntry = async (jobId) => {
|
|
2050
2085
|
const id = jobId.trim();
|
|
@@ -2075,30 +2110,51 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2075
2110
|
const index = Math.min(Math.max(current.jobsPickerIndex, 0), current.backgroundJobs.length - 1);
|
|
2076
2111
|
return current.backgroundJobs[index]?.id ?? null;
|
|
2077
2112
|
};
|
|
2113
|
+
const deferredJobMutationIds = new Set();
|
|
2114
|
+
const drainDeferredJobMutations = async () => {
|
|
2115
|
+
if (deferredJobMutationIds.size === 0)
|
|
2116
|
+
return;
|
|
2117
|
+
const pending = [...deferredJobMutationIds];
|
|
2118
|
+
deferredJobMutationIds.clear();
|
|
2119
|
+
for (const jobId of pending) {
|
|
2120
|
+
await collectBackgroundJobUiOutputMutations({ session, jobId });
|
|
2121
|
+
}
|
|
2122
|
+
syncBackgroundJobsState();
|
|
2123
|
+
};
|
|
2078
2124
|
const handleJobsPickerOutput = async () => {
|
|
2079
2125
|
const jobId = selectedJobsPickerId();
|
|
2080
2126
|
if (!jobId)
|
|
2081
2127
|
return;
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2128
|
+
if (store.getState().busy) {
|
|
2129
|
+
deferredJobMutationIds.add(jobId);
|
|
2130
|
+
}
|
|
2131
|
+
else {
|
|
2132
|
+
await collectBackgroundJobUiOutputMutations({
|
|
2133
|
+
session,
|
|
2134
|
+
jobId,
|
|
2135
|
+
});
|
|
2136
|
+
}
|
|
2087
2137
|
store.update((current) => ({
|
|
2088
2138
|
...current,
|
|
2089
2139
|
jobsPickerExpandedId: current.jobsPickerExpandedId === jobId ? null : jobId,
|
|
2090
2140
|
}));
|
|
2141
|
+
syncBackgroundJobsState();
|
|
2091
2142
|
};
|
|
2092
2143
|
const handleJobsPickerKill = async () => {
|
|
2093
2144
|
const jobId = selectedJobsPickerId();
|
|
2094
2145
|
if (!jobId)
|
|
2095
2146
|
return;
|
|
2096
2147
|
const killed = await killBackgroundJob(jobId);
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2148
|
+
if (store.getState().busy) {
|
|
2149
|
+
deferredJobMutationIds.add(jobId);
|
|
2150
|
+
}
|
|
2151
|
+
else {
|
|
2152
|
+
await collectBackgroundJobUiKillMutations({
|
|
2153
|
+
session,
|
|
2154
|
+
jobId,
|
|
2155
|
+
result: killed,
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2102
2158
|
syncBackgroundJobsState();
|
|
2103
2159
|
if (!killed.ok) {
|
|
2104
2160
|
appendError(killed.error ?? 'Background job kill failed.');
|
|
@@ -2390,8 +2446,18 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2390
2446
|
if (!input || exiting)
|
|
2391
2447
|
return;
|
|
2392
2448
|
if (input.startsWith('/')) {
|
|
2449
|
+
if (isJobsPickerCommand(input)) {
|
|
2450
|
+
store.update((current) => ({
|
|
2451
|
+
...current,
|
|
2452
|
+
cursor: 0,
|
|
2453
|
+
input: '',
|
|
2454
|
+
pastedChunks: [],
|
|
2455
|
+
}));
|
|
2456
|
+
openJobsPicker();
|
|
2457
|
+
return;
|
|
2458
|
+
}
|
|
2393
2459
|
appendTurnAwareEntry({
|
|
2394
|
-
body: "Slash commands can't be queued while a turn is running.",
|
|
2460
|
+
body: "Slash commands can't be queued while a turn is running. Ctrl+B opens background jobs.",
|
|
2395
2461
|
kind: 'system',
|
|
2396
2462
|
title: 'Queued',
|
|
2397
2463
|
});
|
|
@@ -2673,6 +2739,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2673
2739
|
pendingTurnEntries = [];
|
|
2674
2740
|
queueTurnEntry(userEntry);
|
|
2675
2741
|
resetThinkingPacer();
|
|
2742
|
+
activeTurnImageAttachments = imageAttachments;
|
|
2676
2743
|
store.update((current) => ({
|
|
2677
2744
|
...current,
|
|
2678
2745
|
activeTurnInput: input,
|
|
@@ -2726,6 +2793,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2726
2793
|
if (!(await saveActiveSession()))
|
|
2727
2794
|
return;
|
|
2728
2795
|
syncShellStateFromSession();
|
|
2796
|
+
activeTurnImageAttachments = [];
|
|
2729
2797
|
store.update((current) => ({
|
|
2730
2798
|
...current,
|
|
2731
2799
|
activeTurnInput: '',
|
|
@@ -2779,6 +2847,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2779
2847
|
const cancelled = isTurnCancelledError(error);
|
|
2780
2848
|
if (exitForAuthenticationError(error))
|
|
2781
2849
|
return;
|
|
2850
|
+
const failedTurnAttachments = activeTurnImageAttachments;
|
|
2851
|
+
activeTurnImageAttachments = [];
|
|
2782
2852
|
store.update((current) => ({
|
|
2783
2853
|
...current,
|
|
2784
2854
|
activeTurnInput: '',
|
|
@@ -2787,7 +2857,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2787
2857
|
busyPausedAt: null,
|
|
2788
2858
|
busySince: null,
|
|
2789
2859
|
commandLog: [],
|
|
2790
|
-
imageAttachments:
|
|
2860
|
+
imageAttachments: failedTurnAttachments,
|
|
2791
2861
|
status: cancelled ? 'Ready' : 'Turn failed',
|
|
2792
2862
|
thinkingTitle: '',
|
|
2793
2863
|
thinkingNotes: [],
|
|
@@ -2827,6 +2897,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2827
2897
|
activeServerTurnId = null;
|
|
2828
2898
|
}
|
|
2829
2899
|
recoverUnacknowledgedMessages();
|
|
2900
|
+
await drainDeferredJobMutations();
|
|
2830
2901
|
}
|
|
2831
2902
|
};
|
|
2832
2903
|
session.onImageAnalysis = (activeImageCount) => {
|
|
@@ -2879,26 +2950,20 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2879
2950
|
}
|
|
2880
2951
|
store.appendWorkingTool(buildWorkingToolEntry(event));
|
|
2881
2952
|
};
|
|
2882
|
-
const jobDetailLines = (jobId) => {
|
|
2883
|
-
const buffered = getJobBufferedOutput(jobId);
|
|
2884
|
-
if (!buffered)
|
|
2885
|
-
return [];
|
|
2886
|
-
const lines = buffered.output
|
|
2887
|
-
.replace(/\r\n/g, '\n')
|
|
2888
|
-
.replace(/\r/g, '\n')
|
|
2889
|
-
.split('\n')
|
|
2890
|
-
.filter((line) => line.trim().length > 0);
|
|
2891
|
-
const visible = lines.slice(-20);
|
|
2892
|
-
return buffered.droppedChars > 0
|
|
2893
|
-
? [
|
|
2894
|
-
`... (${buffered.droppedChars} chars of older output dropped) ...`,
|
|
2895
|
-
...visible,
|
|
2896
|
-
]
|
|
2897
|
-
: visible;
|
|
2898
|
-
};
|
|
2899
2953
|
const syncBackgroundJobsState = () => {
|
|
2954
|
+
const expandedId = store.getState().jobsPickerExpandedId;
|
|
2900
2955
|
const jobsForDisplay = listBackgroundJobs().map((job) => {
|
|
2901
|
-
const
|
|
2956
|
+
const expanded = job.id === expandedId;
|
|
2957
|
+
const preview = getJobOutputPreview(job.id, expanded ? 20 : 3);
|
|
2958
|
+
const dropped = preview?.droppedChars ?? 0;
|
|
2959
|
+
const detailLines = !expanded
|
|
2960
|
+
? []
|
|
2961
|
+
: dropped > 0
|
|
2962
|
+
? [
|
|
2963
|
+
`... (${dropped} chars of older output dropped) ...`,
|
|
2964
|
+
...(preview?.tailLines ?? []),
|
|
2965
|
+
]
|
|
2966
|
+
: (preview?.tailLines ?? []);
|
|
2902
2967
|
return {
|
|
2903
2968
|
id: job.id,
|
|
2904
2969
|
command: job.command,
|
|
@@ -2907,8 +2972,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2907
2972
|
startedAt: job.startedAt,
|
|
2908
2973
|
endedAt: job.endedAt,
|
|
2909
2974
|
firstOutputLine: preview?.firstLine ?? '',
|
|
2910
|
-
tailLines: preview?.tailLines ?? [],
|
|
2911
|
-
detailLines
|
|
2975
|
+
tailLines: (preview?.tailLines ?? []).slice(-3),
|
|
2976
|
+
detailLines,
|
|
2912
2977
|
};
|
|
2913
2978
|
});
|
|
2914
2979
|
store.update((current) => ({
|
|
@@ -2978,6 +3043,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2978
3043
|
onResumeSession: handleInlineResumeSelection,
|
|
2979
3044
|
onSelectionCopy: handleAppSelectionCopy,
|
|
2980
3045
|
onSelectModel: handleInlineModelSelection,
|
|
3046
|
+
onOpenJobsPicker: openJobsPicker,
|
|
2981
3047
|
onJobsPickerOutput: handleJobsPickerOutput,
|
|
2982
3048
|
onJobsPickerKill: handleJobsPickerKill,
|
|
2983
3049
|
onSudoPasswordInput: handleSudoPasswordInput,
|
|
@@ -41,6 +41,48 @@ const MODEL_PICKER_BORDER_COLOR = 'cyan';
|
|
|
41
41
|
const MODEL_PICKER_ACCENT_COLOR = 'cyan';
|
|
42
42
|
const MODEL_PICKER_HIGHLIGHT_BG = 'ansi256(87)';
|
|
43
43
|
const MODEL_PICKER_META_INDENT = ' ';
|
|
44
|
+
const COMPOSER_MARKER_PATTERN = /\[Image #\d+\]|\[Pasted Text: [^\]\n]*\]/g;
|
|
45
|
+
function composerMarkerMask(text) {
|
|
46
|
+
const mask = new Array(text.length).fill(false);
|
|
47
|
+
for (const match of text.matchAll(COMPOSER_MARKER_PATTERN)) {
|
|
48
|
+
const start = match.index ?? 0;
|
|
49
|
+
for (let offset = start; offset < start + match[0].length; offset += 1) {
|
|
50
|
+
mask[offset] = true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return mask;
|
|
54
|
+
}
|
|
55
|
+
function composerRowSpans(text, mask, cursorCol) {
|
|
56
|
+
const spans = [];
|
|
57
|
+
const push = (value, marker, cursor) => {
|
|
58
|
+
if (!value)
|
|
59
|
+
return;
|
|
60
|
+
spans.push(span(value, {
|
|
61
|
+
...(marker ? { color: 'cyan' } : {}),
|
|
62
|
+
...(cursor ? { inverse: true } : {}),
|
|
63
|
+
}));
|
|
64
|
+
};
|
|
65
|
+
let cut = 0;
|
|
66
|
+
const flushTo = (end) => {
|
|
67
|
+
while (cut < end) {
|
|
68
|
+
const marker = mask[cut] ?? false;
|
|
69
|
+
let runEnd = cut + 1;
|
|
70
|
+
while (runEnd < end && (mask[runEnd] ?? false) === marker)
|
|
71
|
+
runEnd += 1;
|
|
72
|
+
push(text.slice(cut, runEnd), marker, false);
|
|
73
|
+
cut = runEnd;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
if (cursorCol === null) {
|
|
77
|
+
flushTo(text.length);
|
|
78
|
+
return spans;
|
|
79
|
+
}
|
|
80
|
+
flushTo(Math.min(cursorCol, text.length));
|
|
81
|
+
push(text[cursorCol] ?? ' ', mask[cursorCol] ?? false, true);
|
|
82
|
+
cut = Math.min(cursorCol + 1, text.length);
|
|
83
|
+
flushTo(text.length);
|
|
84
|
+
return spans;
|
|
85
|
+
}
|
|
44
86
|
function buildComposerInputLines(input, cursor, promptLabel, placeholder, width) {
|
|
45
87
|
if (!input) {
|
|
46
88
|
return [
|
|
@@ -51,20 +93,22 @@ function buildComposerInputLines(input, cursor, promptLabel, placeholder, width)
|
|
|
51
93
|
const inputWidth = Math.max(1, width - labelWidth);
|
|
52
94
|
const { cursorCol, cursorRow, rows } = splitComposerInput(input, cursor, inputWidth);
|
|
53
95
|
const firstRow = Math.min(Math.max(cursorRow - COMPOSER_INPUT_MAX_ROWS + 1, 0), Math.max(rows.length - COMPOSER_INPUT_MAX_ROWS, 0));
|
|
96
|
+
const rowOffsets = [];
|
|
97
|
+
let consumed = 0;
|
|
98
|
+
for (const row of rows) {
|
|
99
|
+
rowOffsets.push(consumed);
|
|
100
|
+
consumed += row.length;
|
|
101
|
+
}
|
|
102
|
+
const mask = composerMarkerMask(rows.join(''));
|
|
54
103
|
return rows
|
|
55
104
|
.slice(firstRow, firstRow + COMPOSER_INPUT_MAX_ROWS)
|
|
56
105
|
.map((text, index) => {
|
|
57
106
|
const absoluteRow = firstRow + index;
|
|
107
|
+
const rowStart = rowOffsets[absoluteRow] ?? 0;
|
|
58
108
|
const label = index === 0
|
|
59
109
|
? span(promptLabel, { color: 'cyan' })
|
|
60
110
|
: span(' '.repeat(labelWidth));
|
|
61
|
-
|
|
62
|
-
return line(label, span(text));
|
|
63
|
-
}
|
|
64
|
-
const before = text.slice(0, cursorCol);
|
|
65
|
-
const cursorChar = text[cursorCol] ?? ' ';
|
|
66
|
-
const after = text.slice(cursorCol + 1);
|
|
67
|
-
return line(label, span(before), span(cursorChar, { inverse: true }), span(after));
|
|
111
|
+
return line(label, ...composerRowSpans(text, mask.slice(rowStart, rowStart + text.length), absoluteRow === cursorRow ? cursorCol : null));
|
|
68
112
|
});
|
|
69
113
|
}
|
|
70
114
|
function getEntryColor(kind) {
|
|
@@ -410,10 +454,11 @@ function backgroundJobIndicatorSpans(state) {
|
|
|
410
454
|
if (runningCount === 0)
|
|
411
455
|
return [];
|
|
412
456
|
const noun = runningCount === 1 ? 'shell' : 'shells';
|
|
457
|
+
const hint = state.busy ? ' · ctrl+b' : ' · /jobs';
|
|
413
458
|
return [
|
|
414
459
|
span(' ', { color: 'gray', dim: true }),
|
|
415
460
|
span(`● ${runningCount} ${noun} running`, { color: 'green', bold: true }),
|
|
416
|
-
span(
|
|
461
|
+
span(hint, { color: 'gray', dim: true }),
|
|
417
462
|
];
|
|
418
463
|
}
|
|
419
464
|
function todoIndicatorSpans(state) {
|
|
@@ -535,15 +580,29 @@ function jobStatusDescriptor(job, nowMs) {
|
|
|
535
580
|
? { glyph: '✓', color: 'green', text: `exited (0) · ran ${ran}` }
|
|
536
581
|
: { glyph: '✖', color: 'red', text: `exited (${job.exitCode ?? 1}) · ran ${ran}` };
|
|
537
582
|
}
|
|
538
|
-
|
|
583
|
+
const JOBS_PICKER_CHROME_ROWS = 2;
|
|
584
|
+
const JOBS_PICKER_MIN_ROWS = 8;
|
|
585
|
+
function buildJobsPickerLines(state, width, height, nowMs) {
|
|
539
586
|
const jobs = state.backgroundJobs ?? [];
|
|
587
|
+
const bodyBudget = Math.max(1, height - JOBS_PICKER_CHROME_ROWS);
|
|
540
588
|
const lines = [
|
|
541
589
|
plainLine('Background jobs', { color: 'cyan', bold: true }),
|
|
542
590
|
];
|
|
543
591
|
if (jobs.length === 0) {
|
|
544
592
|
lines.push(plainLine('No background jobs in this session.', { color: 'gray' }));
|
|
545
593
|
}
|
|
546
|
-
|
|
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;
|
|
547
606
|
const selected = index === state.jobsPickerIndex;
|
|
548
607
|
const expanded = state.jobsPickerExpandedId === job.id;
|
|
549
608
|
const { glyph, color, text } = jobStatusDescriptor(job, nowMs);
|
|
@@ -552,10 +611,11 @@ function buildJobsPickerLines(state, width, nowMs) {
|
|
|
552
611
|
bold: selected,
|
|
553
612
|
}), span(` ${text}`, { color: 'gray' })));
|
|
554
613
|
if (expanded) {
|
|
555
|
-
const
|
|
614
|
+
const allDetail = (job.detailLines ?? []).length
|
|
556
615
|
? job.detailLines ?? []
|
|
557
616
|
: backgroundJobPreviewLines(job, 3);
|
|
558
|
-
|
|
617
|
+
const detailLines = allDetail.slice(-detailBudget);
|
|
618
|
+
if (detailBudget > 0 && detailLines.length === 0) {
|
|
559
619
|
lines.push(plainLine(' (no output captured)', { color: 'gray' }));
|
|
560
620
|
}
|
|
561
621
|
else {
|
|
@@ -568,6 +628,10 @@ function buildJobsPickerLines(state, width, nowMs) {
|
|
|
568
628
|
}
|
|
569
629
|
}
|
|
570
630
|
}
|
|
631
|
+
const remaining = jobs.length - (start + visible.length);
|
|
632
|
+
if (remaining > 0) {
|
|
633
|
+
lines.push(plainLine(` … ${remaining} older`, { color: 'gray', dim: true }));
|
|
634
|
+
}
|
|
571
635
|
lines.push(plainLine('↑/↓ move enter expand/collapse k kill esc close', {
|
|
572
636
|
color: 'gray',
|
|
573
637
|
}));
|
|
@@ -1075,7 +1139,7 @@ function buildOverlayLines(state, width, height, nowMs) {
|
|
|
1075
1139
|
lines.push(...buildModelPickerPanel(options, state.modelPickerIndex, width, height));
|
|
1076
1140
|
}
|
|
1077
1141
|
if (state.jobsPickerOpen) {
|
|
1078
|
-
lines.push(...buildJobsPickerLines(state, width, nowMs));
|
|
1142
|
+
lines.push(...buildJobsPickerLines(state, width, height, nowMs));
|
|
1079
1143
|
}
|
|
1080
1144
|
if (state.resumePickerOpen) {
|
|
1081
1145
|
const filtered = filterResumeSessions(state.resumePickerSessions, state.resumePickerFilter, state.serverModels);
|
|
@@ -1215,7 +1279,9 @@ export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds, n
|
|
|
1215
1279
|
}
|
|
1216
1280
|
const overlayHeight = state.userInputPrompt
|
|
1217
1281
|
? Math.max(0, rows - countSectionLines(sections))
|
|
1218
|
-
:
|
|
1282
|
+
: state.jobsPickerOpen
|
|
1283
|
+
? Math.max(Math.min(rows, JOBS_PICKER_MIN_ROWS), rows - countSectionLines(sections))
|
|
1284
|
+
: rows;
|
|
1219
1285
|
const overlayLines = buildOverlayLines(state, contentWidth, overlayHeight, nowMs);
|
|
1220
1286
|
if (overlayLines.length > 0) {
|
|
1221
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.36",
|
|
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.36",
|
|
48
|
+
"@thegitai/tui-darwin-x64": "1.0.0-preview.36",
|
|
49
|
+
"@thegitai/tui-linux-arm64": "1.0.0-preview.36",
|
|
50
|
+
"@thegitai/tui-linux-x64": "1.0.0-preview.36",
|
|
51
|
+
"@thegitai/tui-win32-x64": "1.0.0-preview.36",
|
|
52
52
|
"@vscode/ripgrep": "1.18.0"
|
|
53
53
|
},
|
|
54
54
|
"repository": {
|