@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 +2 -0
- package/dist/src/api/chat.js +4 -1
- package/dist/src/job-follow-up.js +11 -0
- package/dist/src/tools/browser.js +23 -10
- package/dist/src/tools/index.js +1 -1
- package/dist/src/tools/read-file.js +5 -0
- package/dist/src/ui/repl.js +58 -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,
|
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/src/tools/index.js
CHANGED
|
@@ -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 {
|
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 { 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
|
-
|
|
2495
|
-
|
|
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
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.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": {
|