tinker-agent 1.0.65
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 +173 -0
- package/package.json +78 -0
- package/patches/markdansi@0.3.2.patch +37 -0
- package/src/agent/context-builder.ts +43 -0
- package/src/agent/context-meter.ts +310 -0
- package/src/agent/loop.ts +525 -0
- package/src/agent/runtime-session.ts +1212 -0
- package/src/agent/session-ledger.ts +828 -0
- package/src/agent/turn-cancellation.ts +44 -0
- package/src/agent/types.ts +77 -0
- package/src/cli/config.ts +283 -0
- package/src/cli/index.ts +29 -0
- package/src/cli/model-profiles.ts +289 -0
- package/src/cli/run-runner.ts +107 -0
- package/src/cli/tui-runner.tsx +290 -0
- package/src/context/compiled-context-hash.ts +138 -0
- package/src/context/compiled-context-validator.ts +209 -0
- package/src/context/context-manager.ts +362 -0
- package/src/context/context-policy.ts +8 -0
- package/src/context/context-protocol-validator.ts +463 -0
- package/src/context/context-revision-compiler.ts +281 -0
- package/src/context/context-revision.ts +111 -0
- package/src/context/context-source.ts +30 -0
- package/src/context/context-swap-renderer.ts +272 -0
- package/src/context/protocol-frame.ts +240 -0
- package/src/context/swap-planner.ts +725 -0
- package/src/events/append-private-file.ts +16 -0
- package/src/events/bash-result-detail.ts +70 -0
- package/src/events/composite-event-sink.ts +82 -0
- package/src/events/event-sink.ts +16 -0
- package/src/events/jsonl-event-log.ts +13 -0
- package/src/events/observation-text-log.ts +195 -0
- package/src/events/stdout-event-printer.ts +396 -0
- package/src/events/types.ts +263 -0
- package/src/ids/runtime-id.ts +68 -0
- package/src/ids/uuid-v7.ts +5 -0
- package/src/instructions/project-instructions.ts +242 -0
- package/src/mcp/mcp-config.ts +144 -0
- package/src/mcp/mcp-manager.ts +216 -0
- package/src/mcp/mcp-tool-executor.ts +178 -0
- package/src/model/committed-prefix-auditor.ts +68 -0
- package/src/model/fake-model-client.ts +280 -0
- package/src/model/model-client.ts +64 -0
- package/src/model/model-context-profile.ts +134 -0
- package/src/model/model-request-preflight.ts +120 -0
- package/src/model/openai-chat-mapping.ts +444 -0
- package/src/model/openai-chat-model-client.ts +190 -0
- package/src/model/prompt-prefix-hash.ts +47 -0
- package/src/model/token-estimator.ts +148 -0
- package/src/observation/observation-builder.ts +481 -0
- package/src/session/resume-projection.ts +616 -0
- package/src/session/session-catalog.ts +270 -0
- package/src/session/session-errors.ts +121 -0
- package/src/session/session-history-reader.ts +535 -0
- package/src/session/session-lock.ts +291 -0
- package/src/session/session-schema.ts +741 -0
- package/src/session/session-store.ts +3067 -0
- package/src/session/sqlite-session-ledger.ts +153 -0
- package/src/tools/bash-task.ts +617 -0
- package/src/tools/bash.ts +450 -0
- package/src/tools/cwd-state.ts +22 -0
- package/src/tools/edit.ts +428 -0
- package/src/tools/file-diff.ts +116 -0
- package/src/tools/glob.ts +202 -0
- package/src/tools/grep.ts +550 -0
- package/src/tools/hash.ts +9 -0
- package/src/tools/path-safety.ts +33 -0
- package/src/tools/read.ts +319 -0
- package/src/tools/recall.ts +400 -0
- package/src/tools/registry.ts +213 -0
- package/src/tools/ripgrep.ts +220 -0
- package/src/tools/task-list.ts +59 -0
- package/src/tools/task-output-snapshot.ts +47 -0
- package/src/tools/task-output-tool.ts +62 -0
- package/src/tools/task-output.ts +159 -0
- package/src/tools/task-stop.ts +59 -0
- package/src/tools/task-tool-args.ts +29 -0
- package/src/tools/types.ts +330 -0
- package/src/tools/web-fetch/backend.ts +27 -0
- package/src/tools/web-fetch/browser-backend.ts +126 -0
- package/src/tools/web-fetch/exa-backend.ts +172 -0
- package/src/tools/web-fetch/index.ts +298 -0
- package/src/tools/web-fetch/local-backend.ts +267 -0
- package/src/tools/web-fetch/refiner.ts +78 -0
- package/src/tools/web-fetch/route.ts +95 -0
- package/src/tools/web-search.ts +300 -0
- package/src/tools/write.ts +244 -0
- package/src/tui/app.tsx +497 -0
- package/src/tui/components/assistant-markdown.tsx +47 -0
- package/src/tui/components/background-tasks.tsx +92 -0
- package/src/tui/components/bash-result-view.tsx +47 -0
- package/src/tui/components/context-status.tsx +127 -0
- package/src/tui/components/diff-view.tsx +151 -0
- package/src/tui/components/file-viewer.tsx +212 -0
- package/src/tui/components/footer.tsx +60 -0
- package/src/tui/components/header.tsx +21 -0
- package/src/tui/components/model-picker.tsx +142 -0
- package/src/tui/components/prompt-input.tsx +432 -0
- package/src/tui/components/resume-session-picker.tsx +273 -0
- package/src/tui/components/timeline.tsx +121 -0
- package/src/tui/context-format.ts +24 -0
- package/src/tui/event-store.ts +865 -0
- package/src/tui/git-branch.ts +23 -0
- package/src/tui/line-editor.ts +157 -0
- package/src/tui/prompt-history.ts +94 -0
- package/src/tui/slash-commands.ts +126 -0
- package/src/tui/tui-projection-policy.ts +35 -0
- package/src/tui/tui-projection-store.ts +123 -0
- package/src/tui/tui-session-controller.ts +170 -0
- package/src/tui/view-file.ts +122 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { Box, Text, useInput, useWindowSize } from "ink";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import type { SessionSummary } from "../../session/session-catalog";
|
|
4
|
+
|
|
5
|
+
const SESSION_ROWS = 3;
|
|
6
|
+
const PICKER_CHROME_ROWS = 3;
|
|
7
|
+
|
|
8
|
+
export type ResumeSessionPickerProps = {
|
|
9
|
+
sessions: readonly SessionSummary[];
|
|
10
|
+
isResuming?: boolean;
|
|
11
|
+
error?: string;
|
|
12
|
+
now?: Date;
|
|
13
|
+
visibleItemCount?: number;
|
|
14
|
+
onCancel: () => void;
|
|
15
|
+
onSelect: (session: SessionSummary) => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type PickerPosition = {
|
|
19
|
+
selectedIndex: number;
|
|
20
|
+
windowStart: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function ResumeSessionPicker(props: ResumeSessionPickerProps) {
|
|
24
|
+
if (props.sessions.length === 0) {
|
|
25
|
+
throw new Error("ResumeSessionPicker requires at least one session.");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return <ResumeSessionPickerContent {...props} />;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function ResumeSessionPickerContent(props: ResumeSessionPickerProps) {
|
|
32
|
+
const { rows } = useWindowSize();
|
|
33
|
+
const visibleItemCount = Math.min(
|
|
34
|
+
props.sessions.length,
|
|
35
|
+
Math.max(
|
|
36
|
+
1,
|
|
37
|
+
Math.floor(props.visibleItemCount ?? (rows - PICKER_CHROME_ROWS) / SESSION_ROWS),
|
|
38
|
+
),
|
|
39
|
+
);
|
|
40
|
+
const [position, setPosition] = useState<PickerPosition>(() => ({
|
|
41
|
+
selectedIndex: initialSelectedIndex(props.sessions),
|
|
42
|
+
windowStart: 0,
|
|
43
|
+
}));
|
|
44
|
+
const windowStart = keepSelectionVisible(
|
|
45
|
+
position.windowStart,
|
|
46
|
+
position.selectedIndex,
|
|
47
|
+
visibleItemCount,
|
|
48
|
+
props.sessions.length,
|
|
49
|
+
);
|
|
50
|
+
const windowEnd = Math.min(props.sessions.length, windowStart + visibleItemCount);
|
|
51
|
+
const selectedSession = props.sessions[position.selectedIndex];
|
|
52
|
+
|
|
53
|
+
useInput(
|
|
54
|
+
(input, key) => {
|
|
55
|
+
if (key.escape) {
|
|
56
|
+
props.onCancel();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (key.return) {
|
|
61
|
+
if (selectedSession !== undefined && isSessionSelectable(selectedSession)) {
|
|
62
|
+
props.onSelect(selectedSession);
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const direction =
|
|
68
|
+
key.upArrow || (input === "k" && !key.ctrl && !key.meta)
|
|
69
|
+
? -1
|
|
70
|
+
: key.downArrow || (input === "j" && !key.ctrl && !key.meta)
|
|
71
|
+
? 1
|
|
72
|
+
: 0;
|
|
73
|
+
if (direction === 0) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
setPosition((current) => {
|
|
78
|
+
const selectedIndex = clamp(
|
|
79
|
+
current.selectedIndex + direction,
|
|
80
|
+
0,
|
|
81
|
+
props.sessions.length - 1,
|
|
82
|
+
);
|
|
83
|
+
const nextWindowStart = keepSelectionVisible(
|
|
84
|
+
current.windowStart,
|
|
85
|
+
selectedIndex,
|
|
86
|
+
visibleItemCount,
|
|
87
|
+
props.sessions.length,
|
|
88
|
+
);
|
|
89
|
+
if (
|
|
90
|
+
selectedIndex === current.selectedIndex &&
|
|
91
|
+
nextWindowStart === current.windowStart
|
|
92
|
+
) {
|
|
93
|
+
return current;
|
|
94
|
+
}
|
|
95
|
+
return { selectedIndex, windowStart: nextWindowStart };
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
{ isActive: props.isResuming !== true },
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const now = props.now ?? new Date();
|
|
102
|
+
return (
|
|
103
|
+
<Box flexDirection="column">
|
|
104
|
+
<Text bold>Resume session</Text>
|
|
105
|
+
<Text dimColor>
|
|
106
|
+
{props.isResuming === true
|
|
107
|
+
? `Resuming ${shortSessionId(selectedSession?.sessionId ?? "")}`
|
|
108
|
+
: "↑/↓ or j/k to move · Enter to resume · Esc to cancel"}
|
|
109
|
+
</Text>
|
|
110
|
+
{props.sessions.slice(windowStart, windowEnd).map((session, offset) => (
|
|
111
|
+
<SessionOption
|
|
112
|
+
key={session.sessionId}
|
|
113
|
+
session={session}
|
|
114
|
+
isSelected={windowStart + offset === position.selectedIndex}
|
|
115
|
+
now={now}
|
|
116
|
+
/>
|
|
117
|
+
))}
|
|
118
|
+
<Text
|
|
119
|
+
color={props.error === undefined ? undefined : "red"}
|
|
120
|
+
dimColor={props.error === undefined}
|
|
121
|
+
wrap="truncate-end"
|
|
122
|
+
>
|
|
123
|
+
{props.error === undefined
|
|
124
|
+
? formatWindowStatus(windowStart, windowEnd, props.sessions.length)
|
|
125
|
+
: `Resume failed: ${singleLine(props.error)}`}
|
|
126
|
+
</Text>
|
|
127
|
+
</Box>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function ResumeSessionPickerLoading(props: { onCancel: () => void }) {
|
|
132
|
+
useInput((_input, key) => {
|
|
133
|
+
if (key.escape) {
|
|
134
|
+
props.onCancel();
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<Box flexDirection="column">
|
|
140
|
+
<Text bold>Resume session</Text>
|
|
141
|
+
<Text dimColor>Loading sessions for this workspace… · Esc to cancel</Text>
|
|
142
|
+
</Box>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function SessionOption(props: {
|
|
147
|
+
session: SessionSummary;
|
|
148
|
+
isSelected: boolean;
|
|
149
|
+
now: Date;
|
|
150
|
+
}) {
|
|
151
|
+
const selectable = isSessionSelectable(props.session);
|
|
152
|
+
const marker = props.isSelected ? "❯ " : " ";
|
|
153
|
+
const preview =
|
|
154
|
+
singleLine(props.session.firstUserPromptPreview ?? "") || "(no prompt)";
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<Box flexDirection="column">
|
|
158
|
+
<Text
|
|
159
|
+
color={props.isSelected && selectable ? "cyan" : undefined}
|
|
160
|
+
bold={props.isSelected && selectable}
|
|
161
|
+
dimColor={!selectable}
|
|
162
|
+
wrap="truncate-end"
|
|
163
|
+
>
|
|
164
|
+
{marker}
|
|
165
|
+
{formatRelativeTime(props.session.updatedAt, props.now)} ·{" "}
|
|
166
|
+
{props.session.turnCount} {props.session.turnCount === 1 ? "turn" : "turns"} ·{" "}
|
|
167
|
+
{sessionStatusText(props.session)}
|
|
168
|
+
</Text>
|
|
169
|
+
<Box marginLeft={2} overflow="hidden">
|
|
170
|
+
<Text dimColor={!selectable} wrap="truncate-end">
|
|
171
|
+
{preview}
|
|
172
|
+
</Text>
|
|
173
|
+
</Box>
|
|
174
|
+
<Box marginLeft={2} overflow="hidden">
|
|
175
|
+
<Box flexGrow={1} overflow="hidden">
|
|
176
|
+
<Text dimColor wrap="truncate-end">
|
|
177
|
+
{singleLine(props.session.modelName)}
|
|
178
|
+
</Text>
|
|
179
|
+
</Box>
|
|
180
|
+
<Text dimColor> {shortSessionId(props.session.sessionId)}</Text>
|
|
181
|
+
</Box>
|
|
182
|
+
</Box>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function isSessionSelectable(session: SessionSummary): boolean {
|
|
187
|
+
return session.status === "resumable" || session.status === "interrupted";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function formatRelativeTime(updatedAt: string, now: Date): string {
|
|
191
|
+
const timestamp = Date.parse(updatedAt);
|
|
192
|
+
if (Number.isNaN(timestamp)) {
|
|
193
|
+
throw new Error(`Invalid session timestamp: ${updatedAt}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const elapsedSeconds = Math.max(0, Math.floor((now.getTime() - timestamp) / 1000));
|
|
197
|
+
if (elapsedSeconds < 60) {
|
|
198
|
+
return "just now";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const elapsedMinutes = Math.floor(elapsedSeconds / 60);
|
|
202
|
+
if (elapsedMinutes < 60) {
|
|
203
|
+
return formatAgo(elapsedMinutes, "minute");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const elapsedHours = Math.floor(elapsedMinutes / 60);
|
|
207
|
+
if (elapsedHours < 24) {
|
|
208
|
+
return formatAgo(elapsedHours, "hour");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return formatAgo(Math.floor(elapsedHours / 24), "day");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function initialSelectedIndex(sessions: readonly SessionSummary[]): number {
|
|
215
|
+
const selectableIndex = sessions.findIndex(isSessionSelectable);
|
|
216
|
+
return selectableIndex === -1 ? 0 : selectableIndex;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function keepSelectionVisible(
|
|
220
|
+
windowStart: number,
|
|
221
|
+
selectedIndex: number,
|
|
222
|
+
visibleItemCount: number,
|
|
223
|
+
itemCount: number,
|
|
224
|
+
): number {
|
|
225
|
+
const maxWindowStart = Math.max(0, itemCount - visibleItemCount);
|
|
226
|
+
let nextWindowStart = clamp(windowStart, 0, maxWindowStart);
|
|
227
|
+
if (selectedIndex < nextWindowStart) {
|
|
228
|
+
nextWindowStart = selectedIndex;
|
|
229
|
+
} else if (selectedIndex >= nextWindowStart + visibleItemCount) {
|
|
230
|
+
nextWindowStart = selectedIndex - visibleItemCount + 1;
|
|
231
|
+
}
|
|
232
|
+
return clamp(nextWindowStart, 0, maxWindowStart);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function sessionStatusText(session: SessionSummary): string {
|
|
236
|
+
switch (session.status) {
|
|
237
|
+
case "resumable":
|
|
238
|
+
return "resumable";
|
|
239
|
+
case "interrupted":
|
|
240
|
+
return "interrupted · completes record; no tool retry";
|
|
241
|
+
case "current":
|
|
242
|
+
return "current session · not selectable";
|
|
243
|
+
case "active":
|
|
244
|
+
return "in use by another Tinker process";
|
|
245
|
+
case "incomplete":
|
|
246
|
+
return "initialization incomplete · cannot resume";
|
|
247
|
+
case "unavailable":
|
|
248
|
+
return `unavailable: ${singleLine(session.statusDetail ?? "") || "session data is unavailable"}`;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function shortSessionId(sessionId: string): string {
|
|
253
|
+
return `${sessionId.slice(0, 8)}…`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function formatWindowStatus(start: number, end: number, total: number): string {
|
|
257
|
+
if (start === 0 && end === total) {
|
|
258
|
+
return `${total} ${total === 1 ? "session" : "sessions"}`;
|
|
259
|
+
}
|
|
260
|
+
return `Showing ${start + 1}–${end} / ${total}${start > 0 ? " · ↑ more above" : ""}${end < total ? " · ↓ more below" : ""}`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function clamp(value: number, minimum: number, maximum: number): number {
|
|
264
|
+
return Math.min(Math.max(value, minimum), maximum);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function singleLine(value: string): string {
|
|
268
|
+
return value.replace(/\s+/g, " ").trim();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function formatAgo(value: number, unit: "minute" | "hour" | "day"): string {
|
|
272
|
+
return `${value} ${unit}${value === 1 ? "" : "s"} ago`;
|
|
273
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import { Fragment } from "react";
|
|
3
|
+
import type { TimelineItem } from "../event-store";
|
|
4
|
+
import { AssistantMarkdown } from "./assistant-markdown";
|
|
5
|
+
import { BashResultView } from "./bash-result-view";
|
|
6
|
+
import { DiffView } from "./diff-view";
|
|
7
|
+
|
|
8
|
+
export type TimelineProps = {
|
|
9
|
+
items: TimelineItem[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function Timeline(props: TimelineProps) {
|
|
13
|
+
return (
|
|
14
|
+
<Box flexDirection="column">
|
|
15
|
+
<Text bold>Timeline</Text>
|
|
16
|
+
{props.items.length === 0 ? (
|
|
17
|
+
<Text color="gray">idle</Text>
|
|
18
|
+
) : (
|
|
19
|
+
props.items.map((item) => renderTimelineItem(item))
|
|
20
|
+
)}
|
|
21
|
+
</Box>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function renderTimelineItem(item: TimelineItem) {
|
|
26
|
+
if (item.label !== undefined) {
|
|
27
|
+
if (item.label === "assistant") {
|
|
28
|
+
return (
|
|
29
|
+
<Fragment key={item.id}>
|
|
30
|
+
<Text color="gray">- {item.label}</Text>
|
|
31
|
+
<AssistantMarkdown text={item.text} />
|
|
32
|
+
</Fragment>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<Fragment key={item.id}>
|
|
38
|
+
<Text color="gray">- {item.label}</Text>
|
|
39
|
+
<Text color={colorForStatus(item.status)}>{formatTimelineItem(item)}</Text>
|
|
40
|
+
{renderItemBash(item)}
|
|
41
|
+
{renderItemDiff(item)}
|
|
42
|
+
</Fragment>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<Fragment key={item.id}>
|
|
48
|
+
<Text color={colorForStatus(item.status)}>{formatTimelineItem(item)}</Text>
|
|
49
|
+
{renderItemBash(item)}
|
|
50
|
+
{renderItemDiff(item)}
|
|
51
|
+
</Fragment>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function renderItemDiff(item: TimelineItem) {
|
|
56
|
+
if (item.diff === undefined || item.diff.length === 0) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return <DiffView hunks={item.diff} truncated={item.diffTruncated} />;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function renderItemBash(item: TimelineItem) {
|
|
64
|
+
if (item.bash === undefined) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return <BashResultView detail={item.bash} />;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function formatTimelineItem(item: TimelineItem): string {
|
|
72
|
+
if (item.status === "text") {
|
|
73
|
+
return item.text;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return `${symbolForStatus(item.status)} ${item.text}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function colorForStatus(status: TimelineItem["status"]): string {
|
|
80
|
+
if (status === "text") {
|
|
81
|
+
return "white";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (status === "ok") {
|
|
85
|
+
return "green";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (status === "failed") {
|
|
89
|
+
return "red";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (status === "running") {
|
|
93
|
+
return "yellow";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (status === "cancelled") {
|
|
97
|
+
return "gray";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return "gray";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function symbolForStatus(status: TimelineItem["status"]): string {
|
|
104
|
+
if (status === "ok") {
|
|
105
|
+
return "✔";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (status === "failed") {
|
|
109
|
+
return "✘";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (status === "running") {
|
|
113
|
+
return "…";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (status === "cancelled") {
|
|
117
|
+
return "⊘";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return "-";
|
|
121
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ContextUsageSnapshot } from "../agent/context-meter";
|
|
2
|
+
|
|
3
|
+
export function formatContextUsageLine(
|
|
4
|
+
usage: Pick<
|
|
5
|
+
ContextUsageSnapshot,
|
|
6
|
+
"usedInputTokens" | "inputBudgetTokens" | "pressure"
|
|
7
|
+
>,
|
|
8
|
+
): string {
|
|
9
|
+
const percent = Math.round((usage.usedInputTokens / usage.inputBudgetTokens) * 100);
|
|
10
|
+
return `context ${formatTokenCount(usage.usedInputTokens)} / ${formatTokenCount(usage.inputBudgetTokens)} (${percent}% used${usage.pressure === "blocked" ? ", blocked" : ""})`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function formatTokenCount(tokens: number): string {
|
|
14
|
+
if (!Number.isSafeInteger(tokens) || tokens < 0) {
|
|
15
|
+
throw new Error(`Invalid token count: ${tokens}`);
|
|
16
|
+
}
|
|
17
|
+
const unit = tokens >= 1_024 * 1_024 ? 1_024 * 1_024 : 1_024;
|
|
18
|
+
if (tokens < unit) {
|
|
19
|
+
return String(tokens);
|
|
20
|
+
}
|
|
21
|
+
const value = tokens / unit;
|
|
22
|
+
const formatted = Number.isInteger(value) ? String(value) : value.toFixed(1);
|
|
23
|
+
return `${formatted}${unit === 1_024 ? "K" : "M"}`;
|
|
24
|
+
}
|