@thegitai/cli 1.0.0-preview.51 → 1.0.0-preview.53
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 +2 -0
- package/dist/src/api/chat.js +4 -1
- package/dist/src/background-jobs.js +4 -0
- package/dist/src/job-follow-up.js +40 -0
- package/dist/src/ui/repl.js +55 -17
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -152,6 +152,8 @@ work against it live, and stop it when the task is done, all in one session.
|
|
|
152
152
|
read its output inline, **k** to stop it, **Esc** to close.
|
|
153
153
|
- `/jobs output <id>` prints a job's full output, and `/jobs kill <id>` stops one
|
|
154
154
|
by id.
|
|
155
|
+
- When waiting for a job is all that's left, the agent can hand control back to
|
|
156
|
+
you and continue automatically once the job finishes on its own.
|
|
155
157
|
- When you end the session, TheGitAI stops its background jobs for you.
|
|
156
158
|
|
|
157
159
|
Full documentation: <https://thegit.ai/docs>
|
package/dist/src/api/chat.js
CHANGED
|
@@ -731,7 +731,7 @@ async function runTurnStream({ openStream, config, session, input, request, reco
|
|
|
731
731
|
}
|
|
732
732
|
}
|
|
733
733
|
}
|
|
734
|
-
export async function sendServerUserMessage({ config, session, input, imageAttachments = [], fetchImpl = globalThis.fetch, signal, onTurnStart, onInterjectionDelivered, recoverySleep, }) {
|
|
734
|
+
export async function sendServerUserMessage({ config, session, input, jobFollowUp = false, imageAttachments = [], fetchImpl = globalThis.fetch, signal, onTurnStart, onInterjectionDelivered, recoverySleep, }) {
|
|
735
735
|
const autoAttach = autoAttachImages(input, session.rootDir, imageAttachments);
|
|
736
736
|
const requestImageAttachments = autoAttach.attachments.length > 0
|
|
737
737
|
? [...imageAttachments, ...autoAttach.attachments]
|
|
@@ -749,6 +749,8 @@ export async function sendServerUserMessage({ config, session, input, imageAttac
|
|
|
749
749
|
session: snapshotForServer(session),
|
|
750
750
|
input: requestInputBase,
|
|
751
751
|
backgroundJobUpdate: backgroundJobUpdate || undefined,
|
|
752
|
+
supportsJobFollowUp: true,
|
|
753
|
+
...(jobFollowUp ? { jobFollowUp: true } : {}),
|
|
752
754
|
clientEnvironment: collectClientEnvironment({ env: session.env }),
|
|
753
755
|
projectOrientation: collectProjectOrientation(session.rootDir) ?? undefined,
|
|
754
756
|
sessionImageStore: describeSessionImageStore(session.env) ?? undefined,
|
|
@@ -834,6 +836,7 @@ export async function sendServerUserMessage({ config, session, input, imageAttac
|
|
|
834
836
|
completed = true;
|
|
835
837
|
return {
|
|
836
838
|
text: outcome.result.text,
|
|
839
|
+
waitingForJobs: outcome.result.waitingForJobs,
|
|
837
840
|
waitingForApproval: outcome.result.waitingForApproval,
|
|
838
841
|
toolBudgetReached: outcome.result.toolBudgetReached,
|
|
839
842
|
usageSummary: outcome.result.usageSummary,
|
|
@@ -403,6 +403,10 @@ export function drainBackgroundJobNotifications({ sessionId } = {}) {
|
|
|
403
403
|
const hasReadableFinalOutput = pending.some((snapshot) => snapshot.status !== 'killed');
|
|
404
404
|
return `${lines.join(' ')}${hasReadableFinalOutput ? ' Use shell_job_output to read any final output.' : ''}`;
|
|
405
405
|
}
|
|
406
|
+
export function hasUnreportedBackgroundJobUpdate(id, { sessionId } = {}) {
|
|
407
|
+
const filter = sessionFilter(sessionId);
|
|
408
|
+
return pendingModelNotifications.some((snapshot) => snapshot.id === id && (!filter || snapshot.sessionId === filter));
|
|
409
|
+
}
|
|
406
410
|
function unknownJobError(id) {
|
|
407
411
|
const known = listBackgroundJobs().map((job) => job.id);
|
|
408
412
|
const hint = known.length
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { getBackgroundJob, hasUnreportedBackgroundJobUpdate } from './background-jobs.js';
|
|
2
|
+
export const MAX_CHAINED_JOB_FOLLOW_UPS = 3;
|
|
3
|
+
function exitedOnItsOwn(id, sessionId) {
|
|
4
|
+
const job = getBackgroundJob(id, { sessionId });
|
|
5
|
+
return job !== null && job.status !== 'running' && job.status !== 'killed';
|
|
6
|
+
}
|
|
7
|
+
export function decideJobFollowUp(jobIds, chainedFollowUps, sessionId) {
|
|
8
|
+
if (!jobIds.some((id) => exitedOnItsOwn(id, sessionId)))
|
|
9
|
+
return 'wait';
|
|
10
|
+
return chainedFollowUps >= MAX_CHAINED_JOB_FOLLOW_UPS ? 'stop' : 'start';
|
|
11
|
+
}
|
|
12
|
+
export function remainingJobWait(jobIds, sessionId) {
|
|
13
|
+
return jobIds.filter((id) => {
|
|
14
|
+
const job = getBackgroundJob(id, { sessionId });
|
|
15
|
+
if (job?.status === 'running')
|
|
16
|
+
return true;
|
|
17
|
+
return exitedOnItsOwn(id, sessionId) && hasUnreportedBackgroundJobUpdate(id, { sessionId });
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export function createJobWaitTracker(sessionId) {
|
|
21
|
+
let waiting = [];
|
|
22
|
+
let chained = 0;
|
|
23
|
+
return {
|
|
24
|
+
get waiting() {
|
|
25
|
+
return waiting;
|
|
26
|
+
},
|
|
27
|
+
turnStarted(jobFollowUp) {
|
|
28
|
+
chained = jobFollowUp ? chained + 1 : 0;
|
|
29
|
+
},
|
|
30
|
+
turnEnded(waitingForJobs) {
|
|
31
|
+
waiting = waitingForJobs ? [...waitingForJobs] : remainingJobWait(waiting, sessionId());
|
|
32
|
+
},
|
|
33
|
+
takeDecision() {
|
|
34
|
+
const decision = decideJobFollowUp(waiting, chained, sessionId());
|
|
35
|
+
if (decision !== 'wait')
|
|
36
|
+
waiting = [];
|
|
37
|
+
return decision;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
package/dist/src/ui/repl.js
CHANGED
|
@@ -14,6 +14,7 @@ import { agentModeLabel, nextAgentMode, } from '../agent-mode.js';
|
|
|
14
14
|
import { chat, models } from '../api/index.js';
|
|
15
15
|
import { isTurnCancelledError, isTurnOutcomeKnown } from '../api/chat.js';
|
|
16
16
|
import { formatTurnRecoveryStatus } from '../api/turn-recovery.js';
|
|
17
|
+
import { createJobWaitTracker } from '../job-follow-up.js';
|
|
17
18
|
import { getJobBufferedOutput, getJobOutputPreview, hasRunningBackgroundJobs, killAllBackgroundJobs, killBackgroundJob, listBackgroundJobs, setBackgroundJobSession, setBackgroundJobUpdateHook, } from '../background-jobs.js';
|
|
18
19
|
import { clearTodos, listTodos, setTodoSession } from '../todo-list.js';
|
|
19
20
|
import { setScratchSession } from '../scratch-dir.js';
|
|
@@ -2491,8 +2492,9 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2491
2492
|
rollback('turn ended — sending as the next prompt');
|
|
2492
2493
|
}
|
|
2493
2494
|
};
|
|
2494
|
-
const
|
|
2495
|
-
|
|
2495
|
+
const jobWait = createJobWaitTracker(() => session.sessionId);
|
|
2496
|
+
const handleSubmit = async (rawInput, { jobFollowUp = false } = {}) => {
|
|
2497
|
+
const chunks = jobFollowUp ? [] : store.getState().pastedChunks;
|
|
2496
2498
|
const expanded = chunks.length
|
|
2497
2499
|
? expandPastedChunks(String(rawInput ?? ''), chunks)
|
|
2498
2500
|
: String(rawInput ?? '');
|
|
@@ -2552,19 +2554,21 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2552
2554
|
const imageAttachments = store
|
|
2553
2555
|
.getState()
|
|
2554
2556
|
.imageAttachments.filter((attachment) => input.includes(`[Image #${attachment.index}]`));
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2557
|
+
if (!jobFollowUp) {
|
|
2558
|
+
scheduleLiveFrameRemount();
|
|
2559
|
+
store.update((current) => ({
|
|
2560
|
+
...current,
|
|
2561
|
+
cursor: 0,
|
|
2562
|
+
imageAttachments: [],
|
|
2563
|
+
input: '',
|
|
2564
|
+
pastedChunks: [],
|
|
2565
|
+
promptHistory: appendPromptToHistory(current.promptHistory, input),
|
|
2566
|
+
promptHistoryCursor: null,
|
|
2567
|
+
promptHistoryDraft: '',
|
|
2568
|
+
}));
|
|
2569
|
+
appendPromptHistory(input);
|
|
2570
|
+
}
|
|
2571
|
+
if ((!input && !jobFollowUp) || exiting) {
|
|
2568
2572
|
store.update((current) => ({ ...current, imageAttachments: [] }));
|
|
2569
2573
|
return;
|
|
2570
2574
|
}
|
|
@@ -2790,6 +2794,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2790
2794
|
await flushQueuedMessage();
|
|
2791
2795
|
return;
|
|
2792
2796
|
}
|
|
2797
|
+
jobWait.turnStarted(jobFollowUp);
|
|
2793
2798
|
latestUsageSummary = null;
|
|
2794
2799
|
disarmExitConfirm();
|
|
2795
2800
|
unacknowledged = new Map();
|
|
@@ -2799,7 +2804,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2799
2804
|
activeTurnAbort = turnAbort;
|
|
2800
2805
|
lastTurnStartedAt = turnStartedAt;
|
|
2801
2806
|
todosTouchedThisTurn = false;
|
|
2802
|
-
|
|
2807
|
+
if (!jobFollowUp)
|
|
2808
|
+
clearTodos();
|
|
2803
2809
|
syncTodosState();
|
|
2804
2810
|
const userEntry = {
|
|
2805
2811
|
body: input,
|
|
@@ -2808,7 +2814,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2808
2814
|
title: 'You',
|
|
2809
2815
|
};
|
|
2810
2816
|
pendingTurnEntries = [];
|
|
2811
|
-
|
|
2817
|
+
if (!jobFollowUp)
|
|
2818
|
+
queueTurnEntry(userEntry);
|
|
2812
2819
|
resetThinkingPacer();
|
|
2813
2820
|
activeTurnImageAttachments = imageAttachments;
|
|
2814
2821
|
store.update((current) => ({
|
|
@@ -2836,6 +2843,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2836
2843
|
config: authConfig,
|
|
2837
2844
|
session,
|
|
2838
2845
|
input,
|
|
2846
|
+
jobFollowUp,
|
|
2839
2847
|
imageAttachments,
|
|
2840
2848
|
signal: turnAbort.signal,
|
|
2841
2849
|
onTurnStart: (serverTurnId) => {
|
|
@@ -2907,6 +2915,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2907
2915
|
status: result.waitingForApproval ? 'Awaiting approval' : 'Ready',
|
|
2908
2916
|
tokenUsage: formatClientTokenUsage(Date.now() - turnStartedAt, latestUsageSummary),
|
|
2909
2917
|
}));
|
|
2918
|
+
jobWait.turnEnded(result.waitingForJobs);
|
|
2910
2919
|
recoverUnacknowledgedMessages();
|
|
2911
2920
|
await flushQueuedMessage();
|
|
2912
2921
|
}
|
|
@@ -2991,6 +3000,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2991
3000
|
}
|
|
2992
3001
|
recoverUnacknowledgedMessages();
|
|
2993
3002
|
await drainDeferredJobMutations();
|
|
3003
|
+
if (turnGeneration === activeTurnGeneration)
|
|
3004
|
+
scheduleJobFollowUp();
|
|
2994
3005
|
}
|
|
2995
3006
|
};
|
|
2996
3007
|
session.onImageAnalysis = (activeImageCount) => {
|
|
@@ -3096,10 +3107,35 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
3096
3107
|
: null,
|
|
3097
3108
|
}));
|
|
3098
3109
|
};
|
|
3110
|
+
const scheduleJobFollowUp = () => {
|
|
3111
|
+
if (jobWait.waiting.length === 0)
|
|
3112
|
+
return;
|
|
3113
|
+
setTimeout(async () => {
|
|
3114
|
+
const state = store.getState();
|
|
3115
|
+
if (exiting || state.busy || state.modelPickerOpen || state.resumePickerOpen)
|
|
3116
|
+
return;
|
|
3117
|
+
const decision = jobWait.takeDecision();
|
|
3118
|
+
if (decision === 'wait')
|
|
3119
|
+
return;
|
|
3120
|
+
if (decision === 'stop') {
|
|
3121
|
+
appendStaticEntry({
|
|
3122
|
+
body: 'A background job finished. Send a message to continue.',
|
|
3123
|
+
kind: 'system',
|
|
3124
|
+
title: 'Background job',
|
|
3125
|
+
});
|
|
3126
|
+
return;
|
|
3127
|
+
}
|
|
3128
|
+
await drainDeferredJobMutations();
|
|
3129
|
+
if (exiting || store.getState().busy)
|
|
3130
|
+
return;
|
|
3131
|
+
void handleSubmit('', { jobFollowUp: true });
|
|
3132
|
+
}, 0);
|
|
3133
|
+
};
|
|
3099
3134
|
setBackgroundJobUpdateHook((snapshot) => {
|
|
3100
3135
|
syncBackgroundJobsState();
|
|
3101
3136
|
if (snapshot.status !== 'running') {
|
|
3102
3137
|
appendTurnAwareEntry(buildBackgroundJobNoticeEntry(snapshot));
|
|
3138
|
+
scheduleJobFollowUp();
|
|
3103
3139
|
}
|
|
3104
3140
|
});
|
|
3105
3141
|
session.requestSudoPassword = async ({ command, prompt, signal }) => openSudoPasswordPrompt(command, prompt, signal);
|
|
@@ -3173,6 +3209,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
3173
3209
|
};
|
|
3174
3210
|
const unsubscribe = store.subscribe(() => {
|
|
3175
3211
|
renderCurrentFrame();
|
|
3212
|
+
if (jobWait.waiting.length > 0 && !store.getState().busy)
|
|
3213
|
+
scheduleJobFollowUp();
|
|
3176
3214
|
});
|
|
3177
3215
|
const spinnerTimer = setInterval(() => {
|
|
3178
3216
|
spinnerFrame = (spinnerFrame + 1) % 6;
|
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.53",
|
|
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",
|
|
@@ -45,11 +45,11 @@
|
|
|
45
45
|
"@lydell/node-pty-linux-x64": "1.1.0",
|
|
46
46
|
"@lydell/node-pty-win32-arm64": "1.1.0",
|
|
47
47
|
"@lydell/node-pty-win32-x64": "1.1.0",
|
|
48
|
-
"@thegitai/tui-darwin-arm64": "1.0.0-preview.
|
|
49
|
-
"@thegitai/tui-darwin-x64": "1.0.0-preview.
|
|
50
|
-
"@thegitai/tui-linux-arm64": "1.0.0-preview.
|
|
51
|
-
"@thegitai/tui-linux-x64": "1.0.0-preview.
|
|
52
|
-
"@thegitai/tui-win32-x64": "1.0.0-preview.
|
|
48
|
+
"@thegitai/tui-darwin-arm64": "1.0.0-preview.53",
|
|
49
|
+
"@thegitai/tui-darwin-x64": "1.0.0-preview.53",
|
|
50
|
+
"@thegitai/tui-linux-arm64": "1.0.0-preview.53",
|
|
51
|
+
"@thegitai/tui-linux-x64": "1.0.0-preview.53",
|
|
52
|
+
"@thegitai/tui-win32-x64": "1.0.0-preview.53",
|
|
53
53
|
"@vscode/ripgrep": "1.18.0"
|
|
54
54
|
},
|
|
55
55
|
"repository": {
|