@thegitai/cli 1.0.0-beta.19 → 1.0.0-beta.20
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 +17 -0
- package/dist/src/agent-mode.js +1 -0
- package/dist/src/todo-list.js +106 -0
- package/dist/src/tool-executor.js +8 -0
- package/dist/src/tools/index.js +2 -0
- package/dist/src/tools/update-todos.js +27 -0
- package/dist/src/ui/repl.js +58 -9
- package/dist/src/ui/tui/build-frame.js +109 -17
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -26,6 +26,23 @@ ai --version print the version and exit
|
|
|
26
26
|
|
|
27
27
|
Run `ai --help` for sessions, modes, keys, and chat commands.
|
|
28
28
|
|
|
29
|
+
## Visible to-do list
|
|
30
|
+
|
|
31
|
+
For larger multi-step tasks, the agent keeps a compact to-do list on screen so
|
|
32
|
+
you can see what it plans to do, what it is working on right now, and what is
|
|
33
|
+
already done.
|
|
34
|
+
|
|
35
|
+
- While the agent works, the list sits at the bottom of the live **Working**
|
|
36
|
+
area, right above where you type, and updates as steps start and finish —
|
|
37
|
+
one step in progress at a time — so it stays visible even when tool output
|
|
38
|
+
above it runs long.
|
|
39
|
+
- When the turn ends, a final snapshot of the list stays readable in the
|
|
40
|
+
transcript, and the footer shows a small progress chip (e.g. `◐ 4/6 to-dos`)
|
|
41
|
+
while steps remain open.
|
|
42
|
+
- The agent's current reasoning stays visible too, right below the list, in a
|
|
43
|
+
compact one-line form so it doesn't compete with the list for space.
|
|
44
|
+
- The list is managed entirely by the agent; simple one-step requests skip it.
|
|
45
|
+
|
|
29
46
|
## Background jobs
|
|
30
47
|
|
|
31
48
|
Some commands are meant to keep running — a dev server, a file watcher, a local
|
package/dist/src/agent-mode.js
CHANGED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export const MAX_TODO_ITEMS = 20;
|
|
2
|
+
export const MAX_TODO_TEXT_CHARS = 160;
|
|
3
|
+
let items = [];
|
|
4
|
+
let activeSessionId = null;
|
|
5
|
+
const STATUS_ALIASES = {
|
|
6
|
+
pending: 'pending',
|
|
7
|
+
todo: 'pending',
|
|
8
|
+
not_started: 'pending',
|
|
9
|
+
in_progress: 'in_progress',
|
|
10
|
+
active: 'in_progress',
|
|
11
|
+
doing: 'in_progress',
|
|
12
|
+
completed: 'completed',
|
|
13
|
+
complete: 'completed',
|
|
14
|
+
done: 'completed',
|
|
15
|
+
};
|
|
16
|
+
function normalizeStatus(raw) {
|
|
17
|
+
const key = String(raw ?? '')
|
|
18
|
+
.trim()
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.replace(/[-\s]+/g, '_');
|
|
21
|
+
return STATUS_ALIASES[key] ?? null;
|
|
22
|
+
}
|
|
23
|
+
export function isCompletedStatus(raw) {
|
|
24
|
+
return normalizeStatus(raw) === 'completed';
|
|
25
|
+
}
|
|
26
|
+
const TODOS_ARG_ALIASES = ['items', 'todo_list', 'todoList', 'list', 'tasks'];
|
|
27
|
+
export function extractTodosArg(args) {
|
|
28
|
+
if (!args || typeof args !== 'object')
|
|
29
|
+
return undefined;
|
|
30
|
+
const record = args;
|
|
31
|
+
if (record.todos !== undefined)
|
|
32
|
+
return record.todos;
|
|
33
|
+
for (const key of TODOS_ARG_ALIASES) {
|
|
34
|
+
if (record[key] !== undefined)
|
|
35
|
+
return record[key];
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
export function setTodoSession(sessionId) {
|
|
40
|
+
const next = String(sessionId ?? '').trim() || null;
|
|
41
|
+
if (activeSessionId !== next) {
|
|
42
|
+
items = [];
|
|
43
|
+
}
|
|
44
|
+
activeSessionId = next;
|
|
45
|
+
}
|
|
46
|
+
export function listTodos() {
|
|
47
|
+
return items.map((item) => ({ ...item }));
|
|
48
|
+
}
|
|
49
|
+
export function getTodoSnapshot() {
|
|
50
|
+
return {
|
|
51
|
+
items: listTodos(),
|
|
52
|
+
completedCount: items.filter((item) => item.status === 'completed').length,
|
|
53
|
+
totalCount: items.length,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function clearTodos() {
|
|
57
|
+
items = [];
|
|
58
|
+
}
|
|
59
|
+
export function replaceTodos(raw) {
|
|
60
|
+
if (!Array.isArray(raw)) {
|
|
61
|
+
return { ok: false, error: 'todos must be an array of { text, status } items.' };
|
|
62
|
+
}
|
|
63
|
+
const normalizations = [];
|
|
64
|
+
if (raw.length > MAX_TODO_ITEMS) {
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
error: `todos supports at most ${MAX_TODO_ITEMS} items; got ${raw.length}. Use fewer, broader steps.`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const next = [];
|
|
71
|
+
let sawInProgress = false;
|
|
72
|
+
for (const [index, entry] of raw.entries()) {
|
|
73
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
74
|
+
return { ok: false, error: `todos[${index}] must be an object with text and status.` };
|
|
75
|
+
}
|
|
76
|
+
const text = String(entry.text ?? '')
|
|
77
|
+
.replace(/\s+/g, ' ')
|
|
78
|
+
.trim();
|
|
79
|
+
if (!text) {
|
|
80
|
+
return { ok: false, error: `todos[${index}].text must be a non-empty string.` };
|
|
81
|
+
}
|
|
82
|
+
const status = normalizeStatus(entry.status);
|
|
83
|
+
if (!status) {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
error: `todos[${index}].status must be one of: pending, in_progress, completed.`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
let boundedText = text;
|
|
90
|
+
if (boundedText.length > MAX_TODO_TEXT_CHARS) {
|
|
91
|
+
boundedText = `${boundedText.slice(0, MAX_TODO_TEXT_CHARS - 1)}…`;
|
|
92
|
+
normalizations.push(`todos[${index}].text truncated to ${MAX_TODO_TEXT_CHARS} chars`);
|
|
93
|
+
}
|
|
94
|
+
let finalStatus = status;
|
|
95
|
+
if (status === 'in_progress') {
|
|
96
|
+
if (sawInProgress) {
|
|
97
|
+
finalStatus = 'pending';
|
|
98
|
+
normalizations.push(`todos[${index}] demoted to pending: only one item can be in_progress`);
|
|
99
|
+
}
|
|
100
|
+
sawInProgress = true;
|
|
101
|
+
}
|
|
102
|
+
next.push({ text: boundedText, status: finalStatus });
|
|
103
|
+
}
|
|
104
|
+
items = next;
|
|
105
|
+
return { ok: true, snapshot: getTodoSnapshot(), normalizations };
|
|
106
|
+
}
|
|
@@ -6,6 +6,7 @@ import { dispatchTool } from './tools/index.js';
|
|
|
6
6
|
import { syncIndexFromDisk } from './project-index.js';
|
|
7
7
|
import { PATH_REPAIRING_EDIT_TOOLS, repairFilePath } from './tools/path-suggest.js';
|
|
8
8
|
import { invalidateShellDiagnosticsCache, runShellDiagnostics, } from './tools/shell-diagnostics.js';
|
|
9
|
+
import { extractTodosArg } from './todo-list.js';
|
|
9
10
|
const EDIT_FILE_PATH_ARG_ALIASES = [
|
|
10
11
|
'filePath',
|
|
11
12
|
'file_path',
|
|
@@ -27,6 +28,13 @@ function toolCallSummary(call) {
|
|
|
27
28
|
call.name === 'shell_job_kill') {
|
|
28
29
|
return String(args.job_id ?? '').trim();
|
|
29
30
|
}
|
|
31
|
+
if (call.name === 'update_todos') {
|
|
32
|
+
const raw = extractTodosArg(args);
|
|
33
|
+
const todos = Array.isArray(raw) ? raw : [];
|
|
34
|
+
if (todos.length === 0)
|
|
35
|
+
return '';
|
|
36
|
+
return `${todos.length} item${todos.length === 1 ? '' : 's'}`;
|
|
37
|
+
}
|
|
30
38
|
const filePath = getEditToolFilePath(call);
|
|
31
39
|
if (filePath)
|
|
32
40
|
return filePath;
|
package/dist/src/tools/index.js
CHANGED
|
@@ -21,6 +21,7 @@ import { getSignatureHelp } from './signature-help.js';
|
|
|
21
21
|
import { shellJobKill } from './shell-job-kill.js';
|
|
22
22
|
import { strReplace } from './str-replace.js';
|
|
23
23
|
import { undoEdit } from './undo-edit.js';
|
|
24
|
+
import { updateTodos } from './update-todos.js';
|
|
24
25
|
import { writeFile } from './write-file.js';
|
|
25
26
|
export const TOOL_MAP = {
|
|
26
27
|
search_code: (context, args) => searchCode(context.projectIndex, args),
|
|
@@ -48,6 +49,7 @@ export const TOOL_MAP = {
|
|
|
48
49
|
run_node_script: runNodeScript,
|
|
49
50
|
shell_job_output: shellJobOutput,
|
|
50
51
|
shell_job_kill: shellJobKill,
|
|
52
|
+
update_todos: updateTodos,
|
|
51
53
|
};
|
|
52
54
|
function invalidToolCall(error) {
|
|
53
55
|
return {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { extractTodosArg, replaceTodos } from '../todo-list.js';
|
|
2
|
+
export function updateTodos(_context, args) {
|
|
3
|
+
const rawTodos = extractTodosArg(args);
|
|
4
|
+
if (rawTodos === undefined) {
|
|
5
|
+
return {
|
|
6
|
+
ok: false,
|
|
7
|
+
error: 'todos is required (pass [] to clear the list).',
|
|
8
|
+
failureCategory: 'missing_required_argument',
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
const result = replaceTodos(rawTodos);
|
|
12
|
+
if (!result.ok) {
|
|
13
|
+
return {
|
|
14
|
+
ok: false,
|
|
15
|
+
error: result.error,
|
|
16
|
+
failureCategory: 'invalid_argument',
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const { snapshot, normalizations } = result;
|
|
20
|
+
return {
|
|
21
|
+
ok: true,
|
|
22
|
+
todos: snapshot.items,
|
|
23
|
+
completedCount: snapshot.completedCount,
|
|
24
|
+
totalCount: snapshot.totalCount,
|
|
25
|
+
...(normalizations.length ? { normalizations } : {}),
|
|
26
|
+
};
|
|
27
|
+
}
|
package/dist/src/ui/repl.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createRatatuiBridge } from './tui/bridge.js';
|
|
2
|
-
import { buildTuiFrame, formatJobElapsed, renderTranscriptEntryLines, } from './tui/build-frame.js';
|
|
2
|
+
import { buildTuiFrame, formatJobElapsed, formatTodoProgress, renderTranscriptEntryLines, } from './tui/build-frame.js';
|
|
3
3
|
export { getSlashCommandSuggestions } from './tui/build-frame.js';
|
|
4
4
|
import { agentModeLabel, nextAgentMode, } from '../agent-mode.js';
|
|
5
5
|
import { chat, models } from '../api/index.js';
|
|
6
6
|
import { isTurnCancelledError } from '../api/chat.js';
|
|
7
7
|
import { getJobBufferedOutput, getJobOutputPreview, hasRunningBackgroundJobs, killAllBackgroundJobs, killBackgroundJob, listBackgroundJobs, setBackgroundJobSession, setBackgroundJobUpdateHook, } from '../background-jobs.js';
|
|
8
|
+
import { clearTodos, listTodos, setTodoSession } from '../todo-list.js';
|
|
8
9
|
import { cancelActiveCommand } from '../executor.js';
|
|
9
10
|
import { setCommandOutputHook, withTuiMode } from '../runtime-mode.js';
|
|
10
11
|
import { collectBackgroundJobUiKillMutations, collectBackgroundJobUiOutputMutations, } from '../tool-executor.js';
|
|
@@ -867,11 +868,16 @@ function createShellStore(initialState) {
|
|
|
867
868
|
},
|
|
868
869
|
appendWorkingTool: (entry) => {
|
|
869
870
|
const lastEntry = state.workingTools.at(-1);
|
|
870
|
-
|
|
871
|
+
const isDuplicate = sameTranscriptDraft(lastEntry, entry);
|
|
872
|
+
if (isDuplicate && state.commandLog.length === 0) {
|
|
871
873
|
return;
|
|
874
|
+
}
|
|
872
875
|
state = {
|
|
873
876
|
...state,
|
|
874
|
-
|
|
877
|
+
commandLog: [],
|
|
878
|
+
workingTools: isDuplicate
|
|
879
|
+
? state.workingTools
|
|
880
|
+
: [...state.workingTools, entry].slice(-WORKING_TOOL_PREVIEW_ITEMS),
|
|
875
881
|
};
|
|
876
882
|
notify();
|
|
877
883
|
},
|
|
@@ -956,6 +962,7 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
956
962
|
exitConfirmUntil: null,
|
|
957
963
|
thinkingTitle: '',
|
|
958
964
|
thinkingNotes: [],
|
|
965
|
+
todos: listTodos(),
|
|
959
966
|
tokenUsage: formatClientTokenUsage(null),
|
|
960
967
|
transcript: [],
|
|
961
968
|
turnCounter: Math.max(0, session.history.filter((entry) => entry.role === 'user').length),
|
|
@@ -1008,6 +1015,14 @@ function thinkingNoteFromStatus(status) {
|
|
|
1008
1015
|
const note = text.slice('Thinking:'.length).trim();
|
|
1009
1016
|
return note ? note : null;
|
|
1010
1017
|
}
|
|
1018
|
+
function stripMarkdownEmphasis(text) {
|
|
1019
|
+
return text
|
|
1020
|
+
.replace(/\*\*\*(.+?)\*\*\*/g, '$1')
|
|
1021
|
+
.replace(/___(.+?)___/g, '$1')
|
|
1022
|
+
.replace(/\*\*(.+?)\*\*/g, '$1')
|
|
1023
|
+
.replace(/__(.+?)__/g, '$1')
|
|
1024
|
+
.replace(/(?<!\*)\*(?!\*)([^*\n]+?)\*(?!\*)/g, '$1');
|
|
1025
|
+
}
|
|
1011
1026
|
function splitThinkingLines(text) {
|
|
1012
1027
|
return text
|
|
1013
1028
|
.split('\n')
|
|
@@ -1016,13 +1031,13 @@ function splitThinkingLines(text) {
|
|
|
1016
1031
|
.flatMap((line) => line
|
|
1017
1032
|
.split(/(?<=[.!?])\s+(?=[A-Z0-9"'`])/)
|
|
1018
1033
|
.map((part) => part.trim())
|
|
1019
|
-
.filter(Boolean))
|
|
1020
|
-
.map((line) => truncate(line, 120));
|
|
1034
|
+
.filter(Boolean));
|
|
1021
1035
|
}
|
|
1022
1036
|
function thinkingPanelFromStatus(status) {
|
|
1023
|
-
const
|
|
1024
|
-
if (!
|
|
1037
|
+
const rawText = thinkingNoteFromStatus(status);
|
|
1038
|
+
if (!rawText)
|
|
1025
1039
|
return null;
|
|
1040
|
+
const text = stripMarkdownEmphasis(rawText);
|
|
1026
1041
|
const rawLines = text
|
|
1027
1042
|
.split('\n')
|
|
1028
1043
|
.map((line) => line.trim())
|
|
@@ -1031,12 +1046,12 @@ function thinkingPanelFromStatus(status) {
|
|
|
1031
1046
|
return null;
|
|
1032
1047
|
if (rawLines.length === 1 && rawLines[0].length <= 72) {
|
|
1033
1048
|
return {
|
|
1034
|
-
title:
|
|
1049
|
+
title: rawLines[0],
|
|
1035
1050
|
notes: [],
|
|
1036
1051
|
};
|
|
1037
1052
|
}
|
|
1038
1053
|
const title = rawLines.length > 1 && rawLines[0].length <= 72
|
|
1039
|
-
?
|
|
1054
|
+
? rawLines[0]
|
|
1040
1055
|
: 'Thinking';
|
|
1041
1056
|
const bodyLines = rawLines.length > 1 && rawLines[0].length <= 72 ? rawLines.slice(1) : rawLines;
|
|
1042
1057
|
return {
|
|
@@ -1241,6 +1256,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1241
1256
|
}
|
|
1242
1257
|
await withTuiMode(async () => {
|
|
1243
1258
|
setBackgroundJobSession(session.sessionId);
|
|
1259
|
+
setTodoSession(session.sessionId);
|
|
1244
1260
|
const store = createShellStore(createInitialShellState(session, serverModels, debugUi));
|
|
1245
1261
|
store.replaceTranscript(createSessionTranscript(session));
|
|
1246
1262
|
let currentServerModels = serverModels;
|
|
@@ -1263,6 +1279,10 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1263
1279
|
let latestUsageSummary = null;
|
|
1264
1280
|
let pendingTurnEntries = [];
|
|
1265
1281
|
let activeTurnAbort = null;
|
|
1282
|
+
let todosTouchedThisTurn = false;
|
|
1283
|
+
const syncTodosState = () => {
|
|
1284
|
+
store.update((current) => ({ ...current, todos: listTodos() }));
|
|
1285
|
+
};
|
|
1266
1286
|
let activeTurnGeneration = 0;
|
|
1267
1287
|
let exitCtrlCArmed = false;
|
|
1268
1288
|
let exitCtrlCTimer = null;
|
|
@@ -1904,6 +1924,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1904
1924
|
applySessionSnapshot(session, snapshot);
|
|
1905
1925
|
setBackgroundJobSession(session.sessionId);
|
|
1906
1926
|
syncBackgroundJobsState();
|
|
1927
|
+
setTodoSession(session.sessionId);
|
|
1928
|
+
syncTodosState();
|
|
1907
1929
|
await saveActiveSession();
|
|
1908
1930
|
syncShellStateFromSession();
|
|
1909
1931
|
store.update((next) => ({
|
|
@@ -2140,6 +2162,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2140
2162
|
}
|
|
2141
2163
|
if (input === '/clear') {
|
|
2142
2164
|
clearConversation(session);
|
|
2165
|
+
clearTodos();
|
|
2166
|
+
syncTodosState();
|
|
2143
2167
|
latestUsageSummary = null;
|
|
2144
2168
|
await saveActiveSession();
|
|
2145
2169
|
store.replaceTranscript([
|
|
@@ -2161,6 +2185,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2161
2185
|
const turnAbort = new AbortController();
|
|
2162
2186
|
activeTurnAbort = turnAbort;
|
|
2163
2187
|
lastTurnStartedAt = turnStartedAt;
|
|
2188
|
+
todosTouchedThisTurn = false;
|
|
2164
2189
|
const userEntry = {
|
|
2165
2190
|
body: input,
|
|
2166
2191
|
kind: 'user',
|
|
@@ -2215,6 +2240,22 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2215
2240
|
await appendSettledTurnEntries(turnEntries, result.text ?? '', () => turnGeneration === activeTurnGeneration && !turnAbort.signal.aborted);
|
|
2216
2241
|
if (turnGeneration !== activeTurnGeneration)
|
|
2217
2242
|
return;
|
|
2243
|
+
if (todosTouchedThisTurn) {
|
|
2244
|
+
todosTouchedThisTurn = false;
|
|
2245
|
+
const todoItems = listTodos();
|
|
2246
|
+
if (todoItems.length > 0) {
|
|
2247
|
+
appendStaticEntry({
|
|
2248
|
+
body: '',
|
|
2249
|
+
kind: 'system',
|
|
2250
|
+
title: `To-dos · ${formatTodoProgress(todoItems)}`,
|
|
2251
|
+
todoList: todoItems,
|
|
2252
|
+
});
|
|
2253
|
+
if (todoItems.every((item) => item.status === 'completed')) {
|
|
2254
|
+
clearTodos();
|
|
2255
|
+
syncTodosState();
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2218
2259
|
store.update((current) => ({
|
|
2219
2260
|
...current,
|
|
2220
2261
|
busy: false,
|
|
@@ -2295,6 +2336,13 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2295
2336
|
};
|
|
2296
2337
|
session.onToolEvent = (event) => {
|
|
2297
2338
|
syncShellStateFromSession();
|
|
2339
|
+
if (event.call.name === 'update_todos') {
|
|
2340
|
+
syncTodosState();
|
|
2341
|
+
if (event.result?.ok === true) {
|
|
2342
|
+
todosTouchedThisTurn = true;
|
|
2343
|
+
return;
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2298
2346
|
if (isFileChangeTool(event.call.name)) {
|
|
2299
2347
|
const entry = buildFileChangeEntry(event);
|
|
2300
2348
|
store.appendWorkingTool(entry);
|
|
@@ -2472,6 +2520,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2472
2520
|
clearLiveFrameRemountTimer();
|
|
2473
2521
|
killAllBackgroundJobs();
|
|
2474
2522
|
setBackgroundJobSession(null);
|
|
2523
|
+
setTodoSession(null);
|
|
2475
2524
|
setBackgroundJobUpdateHook(null);
|
|
2476
2525
|
await bridge.close();
|
|
2477
2526
|
setCommandOutputHook(null);
|
|
@@ -5,6 +5,8 @@ import { renderFormattedBodyLines, renderPreformattedBodyLines, } from './markdo
|
|
|
5
5
|
import { line, plainLine, span, wrapText } from './text.js';
|
|
6
6
|
const WORKING_CLOCK_ICON = '◷';
|
|
7
7
|
const BRAILLE_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴'];
|
|
8
|
+
const TODO_PANEL_MAX_ROWS = 12;
|
|
9
|
+
const TODO_IN_PROGRESS_COLOR = 'ansi256(214)';
|
|
8
10
|
const COMMAND_PREVIEW_LINES = 10;
|
|
9
11
|
const WORKING_TOOL_PREVIEW_ROWS = 3;
|
|
10
12
|
const TRANSCRIPT_DIFF_PREVIEW_LINES = 24;
|
|
@@ -131,7 +133,7 @@ function diffLinePrefix(kind) {
|
|
|
131
133
|
return ' ';
|
|
132
134
|
}
|
|
133
135
|
}
|
|
134
|
-
function
|
|
136
|
+
function fitLine(content, maxWidth) {
|
|
135
137
|
if (maxWidth <= 0)
|
|
136
138
|
return '';
|
|
137
139
|
if (content.length <= maxWidth)
|
|
@@ -230,6 +232,55 @@ function formatRelativeTime(isoDate) {
|
|
|
230
232
|
return `${hours}h ago`;
|
|
231
233
|
return `${Math.floor(hours / 24)}d ago`;
|
|
232
234
|
}
|
|
235
|
+
function todoItemLine(item, width) {
|
|
236
|
+
const text = fitLine(item.text, Math.max(8, width - 5));
|
|
237
|
+
if (item.status === 'completed') {
|
|
238
|
+
return line(span(' ✔ ', { color: 'green' }), span(text, { color: 'gray', dim: true }));
|
|
239
|
+
}
|
|
240
|
+
if (item.status === 'in_progress') {
|
|
241
|
+
return line(span(' ◐ ', { color: TODO_IN_PROGRESS_COLOR }), span(text, { color: TODO_IN_PROGRESS_COLOR, bold: true }));
|
|
242
|
+
}
|
|
243
|
+
return line(span(' ○ ', { color: 'gray' }), span(text));
|
|
244
|
+
}
|
|
245
|
+
export function formatTodoProgress(items) {
|
|
246
|
+
const done = items.filter((item) => item.status === 'completed').length;
|
|
247
|
+
return `${done}/${items.length} done`;
|
|
248
|
+
}
|
|
249
|
+
export function renderTodoListLines(items, width, { header = false } = {}) {
|
|
250
|
+
if (items.length === 0)
|
|
251
|
+
return [];
|
|
252
|
+
const lines = [];
|
|
253
|
+
if (header) {
|
|
254
|
+
lines.push(line(span('To-dos', { color: 'cyan', bold: true }), span(` · ${formatTodoProgress(items)}`, { color: 'gray' })));
|
|
255
|
+
}
|
|
256
|
+
const itemRowBudget = TODO_PANEL_MAX_ROWS - (header ? 1 : 0);
|
|
257
|
+
let collapsedDone = 0;
|
|
258
|
+
if (items.length > itemRowBudget) {
|
|
259
|
+
const over = items.length - (itemRowBudget - 1);
|
|
260
|
+
let leadingDone = 0;
|
|
261
|
+
while (leadingDone < items.length &&
|
|
262
|
+
items[leadingDone].status === 'completed') {
|
|
263
|
+
leadingDone += 1;
|
|
264
|
+
}
|
|
265
|
+
collapsedDone = Math.min(over, leadingDone);
|
|
266
|
+
}
|
|
267
|
+
if (collapsedDone > 0) {
|
|
268
|
+
lines.push(line(span(' ✔ ', { color: 'green' }), span(`${collapsedDone} completed`, { color: 'gray', dim: true })));
|
|
269
|
+
}
|
|
270
|
+
const remaining = items.slice(collapsedDone);
|
|
271
|
+
const budget = itemRowBudget - (collapsedDone > 0 ? 1 : 0);
|
|
272
|
+
const visible = remaining.length > budget ? remaining.slice(0, budget - 1) : remaining;
|
|
273
|
+
for (const item of visible) {
|
|
274
|
+
lines.push(todoItemLine(item, width));
|
|
275
|
+
}
|
|
276
|
+
if (remaining.length > visible.length) {
|
|
277
|
+
lines.push(plainLine(` … +${remaining.length - visible.length} more`, {
|
|
278
|
+
color: 'gray',
|
|
279
|
+
dim: true,
|
|
280
|
+
}));
|
|
281
|
+
}
|
|
282
|
+
return lines;
|
|
283
|
+
}
|
|
233
284
|
export function renderTranscriptEntryLines(entry, width) {
|
|
234
285
|
const color = getEntryColor(entry.kind);
|
|
235
286
|
const lines = [
|
|
@@ -241,11 +292,14 @@ export function renderTranscriptEntryLines(entry, width) {
|
|
|
241
292
|
if (entry.diffPreview) {
|
|
242
293
|
lines.push(plainLine(` Added ${entry.diffPreview.added} line${entry.diffPreview.added === 1 ? '' : 's'}, removed ${entry.diffPreview.removed} line${entry.diffPreview.removed === 1 ? '' : 's'}`, { color: 'gray' }));
|
|
243
294
|
for (const diffLine of entry.diffPreview.lines.slice(0, TRANSCRIPT_DIFF_PREVIEW_LINES)) {
|
|
244
|
-
lines.push(line(span(`${diffLinePrefix(diffLine.kind)} `, { color: diffLineColor(diffLine.kind) }), span(
|
|
295
|
+
lines.push(line(span(`${diffLinePrefix(diffLine.kind)} `, { color: diffLineColor(diffLine.kind) }), span(fitLine(diffLine.content || ' ', width - 4), {
|
|
245
296
|
color: diffLineColor(diffLine.kind),
|
|
246
297
|
})));
|
|
247
298
|
}
|
|
248
299
|
}
|
|
300
|
+
if (entry.todoList && entry.todoList.length > 0) {
|
|
301
|
+
lines.push(...renderTodoListLines(entry.todoList, width));
|
|
302
|
+
}
|
|
249
303
|
return lines;
|
|
250
304
|
}
|
|
251
305
|
function tokenUsageLines(usage) {
|
|
@@ -298,6 +352,23 @@ function backgroundJobIndicatorSpans(state) {
|
|
|
298
352
|
span(' · /jobs', { color: 'gray', dim: true }),
|
|
299
353
|
];
|
|
300
354
|
}
|
|
355
|
+
function todoIndicatorSpans(state) {
|
|
356
|
+
if (state.busy)
|
|
357
|
+
return [];
|
|
358
|
+
const items = state.todos ?? [];
|
|
359
|
+
if (items.length === 0)
|
|
360
|
+
return [];
|
|
361
|
+
const done = items.filter((item) => item.status === 'completed').length;
|
|
362
|
+
if (done >= items.length)
|
|
363
|
+
return [];
|
|
364
|
+
return [
|
|
365
|
+
span(' ', { color: 'gray', dim: true }),
|
|
366
|
+
span(`◐ ${done}/${items.length} to-dos`, {
|
|
367
|
+
color: TODO_IN_PROGRESS_COLOR,
|
|
368
|
+
bold: true,
|
|
369
|
+
}),
|
|
370
|
+
];
|
|
371
|
+
}
|
|
301
372
|
function composerFooterLines(state) {
|
|
302
373
|
const visibleSessionId = formatPromptSessionIdLabel(state.showSessionId, state.sessionId);
|
|
303
374
|
const transientStatus = footerTransientStatus(state.status);
|
|
@@ -316,7 +387,7 @@ function composerFooterLines(state) {
|
|
|
316
387
|
]
|
|
317
388
|
: []), ...(visibleSessionId
|
|
318
389
|
? [span(` ${visibleSessionId}`, { color: 'gray', dim: true })]
|
|
319
|
-
: []), ...backgroundJobIndicatorSpans(state)),
|
|
390
|
+
: []), ...backgroundJobIndicatorSpans(state), ...todoIndicatorSpans(state)),
|
|
320
391
|
plainLine(''),
|
|
321
392
|
plainLine(formatModelLabel(state.currentModelId, state.serverModels), {
|
|
322
393
|
color: 'cyan',
|
|
@@ -432,6 +503,16 @@ function buildJobsPickerLines(state, width, nowMs) {
|
|
|
432
503
|
}));
|
|
433
504
|
return lines;
|
|
434
505
|
}
|
|
506
|
+
function thinkingHeaderLine(spinnerFrame, title, width) {
|
|
507
|
+
const spinner = `${BRAILLE_SPINNER_FRAMES[spinnerFrame % BRAILLE_SPINNER_FRAMES.length]} `;
|
|
508
|
+
const fittedTitle = title
|
|
509
|
+
? fitLine(title, Math.max(8, width - spinner.length - 'Thinking'.length - 3))
|
|
510
|
+
: '';
|
|
511
|
+
return line(span(spinner, { color: 'green' }), span('Thinking', { color: 'green', bold: true }), ...(fittedTitle ? [span(` · ${fittedTitle}`, { color: 'ansi256(248)' })] : []));
|
|
512
|
+
}
|
|
513
|
+
function thinkingNoteLine(note, width) {
|
|
514
|
+
return line(span('│ ', { color: 'green' }), span(fitLine(note, Math.max(8, width - 3)), { color: 'ansi256(248)' }));
|
|
515
|
+
}
|
|
435
516
|
function buildLiveLines(state, width, spinnerFrame, elapsedSeconds) {
|
|
436
517
|
if (!state.busy)
|
|
437
518
|
return [];
|
|
@@ -445,33 +526,44 @@ function buildLiveLines(state, width, spinnerFrame, elapsedSeconds) {
|
|
|
445
526
|
}, width));
|
|
446
527
|
lines.push(plainLine(''));
|
|
447
528
|
}
|
|
448
|
-
lines.push(plainLine(`${WORKING_CLOCK_ICON} Working · ${elapsedSeconds < 60 ? `${elapsedSeconds}s` : `${Math.floor(elapsedSeconds / 60)}m ${String(elapsedSeconds % 60).padStart(2, '0')}s`}`, { color: 'yellow' }));
|
|
449
|
-
const visibleTitle = state.thinkingTitle.trim();
|
|
450
|
-
const visibleNotes = state.thinkingNotes.filter(Boolean).slice(-THINKING_NOTE_PREVIEW_ROWS);
|
|
451
|
-
if (visibleTitle || visibleNotes.length > 0) {
|
|
452
|
-
lines.push(plainLine(''));
|
|
453
|
-
lines.push(line(span(`${BRAILLE_SPINNER_FRAMES[spinnerFrame % BRAILLE_SPINNER_FRAMES.length]} `, {
|
|
454
|
-
color: 'green',
|
|
455
|
-
}), span('Thinking', { color: 'green', bold: true }), ...(visibleTitle ? [span(` · ${visibleTitle}`, { color: 'ansi256(248)' })] : [])));
|
|
456
|
-
for (const note of visibleNotes) {
|
|
457
|
-
lines.push(line(span('│ ', { color: 'green' }), span(note, { color: 'ansi256(248)' })));
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
529
|
const toolEntries = state.workingTools.slice(-WORKING_TOOL_PREVIEW_ROWS);
|
|
461
530
|
if (toolEntries.length > 0) {
|
|
462
|
-
lines.push(plainLine(''));
|
|
463
531
|
for (const entry of toolEntries) {
|
|
464
532
|
const color = getEntryColor(entry.kind);
|
|
465
533
|
const body = entry.body.split('\n')[0]?.trim();
|
|
466
534
|
lines.push(line(span('● ', { color }), span(entry.title, { color, bold: true }), ...(body ? [span(` ${truncate(body, 140)}`, { color })] : [])));
|
|
467
535
|
}
|
|
536
|
+
lines.push(plainLine(''));
|
|
468
537
|
}
|
|
469
538
|
const logLines = state.commandLog.slice(-COMMAND_PREVIEW_LINES);
|
|
470
539
|
if (logLines.length > 0) {
|
|
471
|
-
lines.push(plainLine(''));
|
|
540
|
+
lines.push(plainLine('⋮ output', { color: 'gray', dim: true }));
|
|
472
541
|
for (const outputLine of logLines) {
|
|
473
542
|
lines.push(plainLine(outputLine, { color: 'gray', dim: true }));
|
|
474
543
|
}
|
|
544
|
+
lines.push(plainLine(''));
|
|
545
|
+
}
|
|
546
|
+
lines.push(plainLine(`${WORKING_CLOCK_ICON} Working · ${elapsedSeconds < 60 ? `${elapsedSeconds}s` : `${Math.floor(elapsedSeconds / 60)}m ${String(elapsedSeconds % 60).padStart(2, '0')}s`}`, { color: 'yellow' }));
|
|
547
|
+
if (state.todos.length > 0) {
|
|
548
|
+
lines.push(plainLine(''));
|
|
549
|
+
lines.push(...renderTodoListLines(state.todos, width, { header: true }));
|
|
550
|
+
}
|
|
551
|
+
const visibleTitle = state.thinkingTitle.trim();
|
|
552
|
+
const visibleNotes = state.thinkingNotes.filter(Boolean);
|
|
553
|
+
if (visibleTitle || visibleNotes.length > 0) {
|
|
554
|
+
lines.push(plainLine(''));
|
|
555
|
+
if (state.todos.length > 0) {
|
|
556
|
+
const minimalText = visibleTitle && visibleTitle !== 'Thinking'
|
|
557
|
+
? visibleTitle
|
|
558
|
+
: (visibleNotes[visibleNotes.length - 1] ?? '');
|
|
559
|
+
lines.push(thinkingHeaderLine(spinnerFrame, minimalText, width));
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
lines.push(thinkingHeaderLine(spinnerFrame, visibleTitle, width));
|
|
563
|
+
for (const note of visibleNotes.slice(-THINKING_NOTE_PREVIEW_ROWS)) {
|
|
564
|
+
lines.push(thinkingNoteLine(note, width));
|
|
565
|
+
}
|
|
566
|
+
}
|
|
475
567
|
}
|
|
476
568
|
lines.push(plainLine(''));
|
|
477
569
|
return lines;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thegitai/cli",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.20",
|
|
4
4
|
"description": "TheGitAI CLI client (source-visible, proprietary)",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"homepage": "https://thegit.ai",
|
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
"@lydell/node-pty-linux-x64": "1.1.0",
|
|
26
26
|
"@lydell/node-pty-win32-arm64": "1.1.0",
|
|
27
27
|
"@lydell/node-pty-win32-x64": "1.1.0",
|
|
28
|
-
"@thegitai/tui-darwin-arm64": "1.0.0-beta.
|
|
29
|
-
"@thegitai/tui-darwin-x64": "1.0.0-beta.
|
|
30
|
-
"@thegitai/tui-linux-x64": "1.0.0-beta.
|
|
31
|
-
"@thegitai/tui-win32-x64": "1.0.0-beta.
|
|
28
|
+
"@thegitai/tui-darwin-arm64": "1.0.0-beta.20",
|
|
29
|
+
"@thegitai/tui-darwin-x64": "1.0.0-beta.20",
|
|
30
|
+
"@thegitai/tui-linux-x64": "1.0.0-beta.20",
|
|
31
|
+
"@thegitai/tui-win32-x64": "1.0.0-beta.20",
|
|
32
32
|
"@vscode/ripgrep": "1.18.0"
|
|
33
33
|
},
|
|
34
34
|
"publishConfig": {
|