@thegitai/cli 1.0.0-preview.50 → 1.0.0-preview.52

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,
@@ -0,0 +1,11 @@
1
+ import { getBackgroundJob } from './background-jobs.js';
2
+ export const MAX_CHAINED_JOB_FOLLOW_UPS = 3;
3
+ export function decideJobFollowUp(jobIds, chainedFollowUps, sessionId) {
4
+ const finished = jobIds.some((id) => {
5
+ const job = getBackgroundJob(id, { sessionId });
6
+ return job !== null && job.status !== 'running' && job.status !== 'killed';
7
+ });
8
+ if (!finished)
9
+ return 'wait';
10
+ return chainedFollowUps >= MAX_CHAINED_JOB_FOLLOW_UPS ? 'stop' : 'start';
11
+ }
@@ -138,12 +138,25 @@ export async function browserTabs(context, args) {
138
138
  return bridgeFailure(error);
139
139
  }
140
140
  }
141
- export async function browserTabNew(context) {
141
+ export async function browserTabNew(context, args = {}) {
142
142
  const guard = await notConnected(context);
143
143
  if (guard.ok === false)
144
144
  return guard;
145
+ const url = String(args.url ?? '').trim();
145
146
  try {
146
- const result = await call(context, 'tab.create', {});
147
+ if (url) {
148
+ const denied = await ensureSiteAllowed(context, {
149
+ url,
150
+ tool: 'browser_tab_new',
151
+ summary: `Open ${url} in a new tab in TheGitAI's tab group.`,
152
+ });
153
+ if (denied)
154
+ return denied;
155
+ }
156
+ const created = await call(context, 'tab.create', {});
157
+ if (!url)
158
+ return attachEvents({ ok: true, ...created });
159
+ const result = await call(context, 'navigate', { tabId: Number(created.tabId), url }, NAVIGATE_TIMEOUT_MS);
147
160
  return attachEvents({ ok: true, ...result });
148
161
  }
149
162
  catch (error) {
@@ -174,14 +187,6 @@ export async function browserNavigate(context, args) {
174
187
  return missing('url', 'browser_navigate');
175
188
  let tabId = Number(args.tabId ?? args.tab_id);
176
189
  try {
177
- if (!Number.isInteger(tabId)) {
178
- const context_ = await call(context, 'context');
179
- tabId = Number(context_?.tabs?.find((tab) => tab.inSession && tab.available)?.tabId);
180
- if (!Number.isInteger(tabId)) {
181
- const created = await call(context, 'tab.create', {});
182
- tabId = Number(created.tabId);
183
- }
184
- }
185
190
  if (url !== 'back' && url !== 'forward') {
186
191
  const denied = await ensureSiteAllowed(context, {
187
192
  url,
@@ -191,6 +196,14 @@ export async function browserNavigate(context, args) {
191
196
  if (denied)
192
197
  return denied;
193
198
  }
199
+ if (!Number.isInteger(tabId)) {
200
+ const context_ = await call(context, 'context');
201
+ tabId = Number(context_?.tabs?.find((tab) => tab.inSession && tab.available)?.tabId);
202
+ if (!Number.isInteger(tabId)) {
203
+ const created = await call(context, 'tab.create', {});
204
+ tabId = Number(created.tabId);
205
+ }
206
+ }
194
207
  const result = await call(context, 'navigate', { tabId, url }, NAVIGATE_TIMEOUT_MS);
195
208
  return attachEvents({ ok: true, ...result });
196
209
  }
@@ -46,7 +46,7 @@ export const TOOL_MAP = {
46
46
  analyze_image: (context, args) => readImageFile(context, args),
47
47
  browser: (context) => browserGateway(context),
48
48
  browser_tabs: browserTabs,
49
- browser_tab_new: (context) => browserTabNew(context),
49
+ browser_tab_new: (context, args) => browserTabNew(context, args),
50
50
  browser_tab_close: browserTabClose,
51
51
  browser_navigate: browserNavigate,
52
52
  browser_read_page: browserReadPage,
@@ -7,6 +7,8 @@ import { isWithinSessionScratchDir } from '../scratch-dir.js';
7
7
  import { repairFilePath } from './path-suggest.js';
8
8
  import { dotenvFitsRedactionBudget, getCurrentFileHash, recordReadCoverage, redactContentWithStableTokens, redactDotenvWithStableTokens, } from '../session-safety.js';
9
9
  import { readFileRange, truncate } from '../utils.js';
10
+ import { IMAGE_MIME_BY_EXT } from '../core/image-limits.js';
11
+ import { readImageFile } from './read-image-file.js';
10
12
  const MAX_FILE_READ_CHARS = 12000;
11
13
  const DOCUMENT_EXTENSIONS = new Set(['.pdf', '.xlsx', '.docx']);
12
14
  export async function readFile(context, args) {
@@ -38,6 +40,9 @@ export async function readFile(context, args) {
38
40
  failureCategory: 'invalid_argument',
39
41
  };
40
42
  }
43
+ if (IMAGE_MIME_BY_EXT[documentExt] && typeof context !== 'string') {
44
+ return readImageFile(context, { path: filePath });
45
+ }
41
46
  let content;
42
47
  if (projectPath || scratchPath) {
43
48
  try {
@@ -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 { decideJobFollowUp } 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,10 @@ 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
+ let waitingJobIds = [];
2496
+ let chainedJobFollowUps = 0;
2497
+ const handleSubmit = async (rawInput, { jobFollowUp = false } = {}) => {
2498
+ const chunks = jobFollowUp ? [] : store.getState().pastedChunks;
2496
2499
  const expanded = chunks.length
2497
2500
  ? expandPastedChunks(String(rawInput ?? ''), chunks)
2498
2501
  : String(rawInput ?? '');
@@ -2552,19 +2555,21 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2552
2555
  const imageAttachments = store
2553
2556
  .getState()
2554
2557
  .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) {
2558
+ if (!jobFollowUp) {
2559
+ scheduleLiveFrameRemount();
2560
+ store.update((current) => ({
2561
+ ...current,
2562
+ cursor: 0,
2563
+ imageAttachments: [],
2564
+ input: '',
2565
+ pastedChunks: [],
2566
+ promptHistory: appendPromptToHistory(current.promptHistory, input),
2567
+ promptHistoryCursor: null,
2568
+ promptHistoryDraft: '',
2569
+ }));
2570
+ appendPromptHistory(input);
2571
+ }
2572
+ if ((!input && !jobFollowUp) || exiting) {
2568
2573
  store.update((current) => ({ ...current, imageAttachments: [] }));
2569
2574
  return;
2570
2575
  }
@@ -2790,6 +2795,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2790
2795
  await flushQueuedMessage();
2791
2796
  return;
2792
2797
  }
2798
+ waitingJobIds = [];
2799
+ chainedJobFollowUps = jobFollowUp ? chainedJobFollowUps + 1 : 0;
2793
2800
  latestUsageSummary = null;
2794
2801
  disarmExitConfirm();
2795
2802
  unacknowledged = new Map();
@@ -2799,7 +2806,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2799
2806
  activeTurnAbort = turnAbort;
2800
2807
  lastTurnStartedAt = turnStartedAt;
2801
2808
  todosTouchedThisTurn = false;
2802
- clearTodos();
2809
+ if (!jobFollowUp)
2810
+ clearTodos();
2803
2811
  syncTodosState();
2804
2812
  const userEntry = {
2805
2813
  body: input,
@@ -2808,7 +2816,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2808
2816
  title: 'You',
2809
2817
  };
2810
2818
  pendingTurnEntries = [];
2811
- queueTurnEntry(userEntry);
2819
+ if (!jobFollowUp)
2820
+ queueTurnEntry(userEntry);
2812
2821
  resetThinkingPacer();
2813
2822
  activeTurnImageAttachments = imageAttachments;
2814
2823
  store.update((current) => ({
@@ -2836,6 +2845,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2836
2845
  config: authConfig,
2837
2846
  session,
2838
2847
  input,
2848
+ jobFollowUp,
2839
2849
  imageAttachments,
2840
2850
  signal: turnAbort.signal,
2841
2851
  onTurnStart: (serverTurnId) => {
@@ -2907,6 +2917,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2907
2917
  status: result.waitingForApproval ? 'Awaiting approval' : 'Ready',
2908
2918
  tokenUsage: formatClientTokenUsage(Date.now() - turnStartedAt, latestUsageSummary),
2909
2919
  }));
2920
+ waitingJobIds = result.waitingForJobs ?? [];
2910
2921
  recoverUnacknowledgedMessages();
2911
2922
  await flushQueuedMessage();
2912
2923
  }
@@ -2991,6 +3002,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2991
3002
  }
2992
3003
  recoverUnacknowledgedMessages();
2993
3004
  await drainDeferredJobMutations();
3005
+ if (turnGeneration === activeTurnGeneration)
3006
+ scheduleJobFollowUp();
2994
3007
  }
2995
3008
  };
2996
3009
  session.onImageAnalysis = (activeImageCount) => {
@@ -3096,10 +3109,36 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
3096
3109
  : null,
3097
3110
  }));
3098
3111
  };
3112
+ const scheduleJobFollowUp = () => {
3113
+ if (waitingJobIds.length === 0)
3114
+ return;
3115
+ setTimeout(async () => {
3116
+ const state = store.getState();
3117
+ if (exiting || state.busy || state.modelPickerOpen || state.resumePickerOpen)
3118
+ return;
3119
+ const decision = decideJobFollowUp(waitingJobIds, chainedJobFollowUps, session.sessionId);
3120
+ if (decision === 'wait')
3121
+ return;
3122
+ waitingJobIds = [];
3123
+ if (decision === 'stop') {
3124
+ appendStaticEntry({
3125
+ body: 'A background job finished. Send a message to continue.',
3126
+ kind: 'system',
3127
+ title: 'Background job',
3128
+ });
3129
+ return;
3130
+ }
3131
+ await drainDeferredJobMutations();
3132
+ if (exiting || store.getState().busy)
3133
+ return;
3134
+ void handleSubmit('', { jobFollowUp: true });
3135
+ }, 0);
3136
+ };
3099
3137
  setBackgroundJobUpdateHook((snapshot) => {
3100
3138
  syncBackgroundJobsState();
3101
3139
  if (snapshot.status !== 'running') {
3102
3140
  appendTurnAwareEntry(buildBackgroundJobNoticeEntry(snapshot));
3141
+ scheduleJobFollowUp();
3103
3142
  }
3104
3143
  });
3105
3144
  session.requestSudoPassword = async ({ command, prompt, signal }) => openSudoPasswordPrompt(command, prompt, signal);
@@ -3173,6 +3212,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
3173
3212
  };
3174
3213
  const unsubscribe = store.subscribe(() => {
3175
3214
  renderCurrentFrame();
3215
+ if (waitingJobIds.length > 0 && !store.getState().busy)
3216
+ scheduleJobFollowUp();
3176
3217
  });
3177
3218
  const spinnerTimer = setInterval(() => {
3178
3219
  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.50",
3
+ "version": "1.0.0-preview.52",
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.50",
49
- "@thegitai/tui-darwin-x64": "1.0.0-preview.50",
50
- "@thegitai/tui-linux-arm64": "1.0.0-preview.50",
51
- "@thegitai/tui-linux-x64": "1.0.0-preview.50",
52
- "@thegitai/tui-win32-x64": "1.0.0-preview.50",
48
+ "@thegitai/tui-darwin-arm64": "1.0.0-preview.52",
49
+ "@thegitai/tui-darwin-x64": "1.0.0-preview.52",
50
+ "@thegitai/tui-linux-arm64": "1.0.0-preview.52",
51
+ "@thegitai/tui-linux-x64": "1.0.0-preview.52",
52
+ "@thegitai/tui-win32-x64": "1.0.0-preview.52",
53
53
  "@vscode/ripgrep": "1.18.0"
54
54
  },
55
55
  "repository": {