@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 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>
@@ -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
+ }
@@ -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 handleSubmit = async (rawInput) => {
2495
- const chunks = store.getState().pastedChunks;
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
- scheduleLiveFrameRemount();
2556
- store.update((current) => ({
2557
- ...current,
2558
- cursor: 0,
2559
- imageAttachments: [],
2560
- input: '',
2561
- pastedChunks: [],
2562
- promptHistory: appendPromptToHistory(current.promptHistory, input),
2563
- promptHistoryCursor: null,
2564
- promptHistoryDraft: '',
2565
- }));
2566
- appendPromptHistory(input);
2567
- if (!input || exiting) {
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
- clearTodos();
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
- queueTurnEntry(userEntry);
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.51",
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.51",
49
- "@thegitai/tui-darwin-x64": "1.0.0-preview.51",
50
- "@thegitai/tui-linux-arm64": "1.0.0-preview.51",
51
- "@thegitai/tui-linux-x64": "1.0.0-preview.51",
52
- "@thegitai/tui-win32-x64": "1.0.0-preview.51",
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": {