tinker-agent 2.0.0 → 2.2.0
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/CHANGELOG.md +44 -1
- package/README.md +27 -2
- package/package.json +2 -1
- package/src/agent/context-meter.ts +2 -4
- package/src/agent/runtime-session.ts +9 -2
- package/src/agent/session-ledger.ts +12 -5
- package/src/agent/tool-result-content.ts +76 -0
- package/src/agent/types.ts +14 -2
- package/src/cli/config.ts +4 -0
- package/src/cli/model-profiles.ts +41 -2
- package/src/cli/public-config-contract.ts +30 -7
- package/src/cli/runner-dependencies.ts +5 -0
- package/src/cli/tui-memory.ts +1 -0
- package/src/cli/tui-runner.tsx +4 -0
- package/src/context/compiled-context-hash.ts +2 -1
- package/src/context/compiled-context-validator.ts +13 -4
- package/src/context/context-protocol-validator.ts +33 -2
- package/src/context/context-revision-compiler.ts +2 -1
- package/src/context/context-revision.ts +8 -2
- package/src/context/context-swap-renderer.ts +46 -12
- package/src/context/prefix-retirement-planner.ts +13 -9
- package/src/context/protocol-frame.ts +74 -7
- package/src/context/swap-planner.ts +19 -14
- package/src/events/observation-text-log.ts +1 -1
- package/src/events/stdout-event-printer.ts +6 -0
- package/src/image/image-asset-store.ts +32 -3
- package/src/memory/contracts.ts +63 -3
- package/src/memory/memory-coordinator.ts +319 -49
- package/src/memory/memory-extractor.ts +48 -48
- package/src/memory/memory-get-tool.ts +86 -0
- package/src/memory/memory-search-tool.ts +122 -33
- package/src/memory/memory-store.ts +227 -20
- package/src/model/fake-model-client.ts +129 -76
- package/src/model/model-client.ts +62 -11
- package/src/model/openai-chat-mapping.ts +2 -1
- package/src/model/openai-chat-model-client.ts +22 -10
- package/src/model/openai-model-utils.ts +61 -30
- package/src/model/openai-responses-mapping.ts +25 -1
- package/src/model/openai-responses-model-client.ts +27 -11
- package/src/model/token-estimator.ts +10 -0
- package/src/observation/observation-builder.ts +100 -25
- package/src/session/session-history-reader.ts +128 -5
- package/src/session/session-schema.ts +59 -9
- package/src/session/session-store.ts +343 -196
- package/src/tools/registry.ts +18 -0
- package/src/tools/types.ts +46 -0
- package/src/tools/view-image.ts +89 -0
- package/src/tools/wait.ts +85 -0
- package/src/tui/components/memory-browser.tsx +3 -0
- package/src/tui/components/prompt-input.tsx +48 -25
- package/src/tui/event-store.ts +61 -2
package/src/tools/registry.ts
CHANGED
|
@@ -12,10 +12,12 @@ import { createTaskInputToolExecutor } from "./task-input";
|
|
|
12
12
|
import { createTaskOutputToolExecutor } from "./task-output-tool";
|
|
13
13
|
import { createTaskStopToolExecutor } from "./task-stop";
|
|
14
14
|
import { createUpdatePlanToolExecutor } from "./update-plan";
|
|
15
|
+
import { createWaitToolExecutor } from "./wait";
|
|
15
16
|
import { createWebFetchToolExecutor } from "./web-fetch";
|
|
16
17
|
import type { Refiner } from "./web-fetch/refiner";
|
|
17
18
|
import { createWebSearchToolExecutor } from "./web-search";
|
|
18
19
|
import { createWriteToolExecutor } from "./write";
|
|
20
|
+
import { createViewImageToolExecutor } from "./view-image";
|
|
19
21
|
import { TurnUndoManager } from "./turn-undo-manager";
|
|
20
22
|
import { cancellationError, throwIfTurnCancelled } from "../agent/turn-cancellation";
|
|
21
23
|
import type {
|
|
@@ -39,6 +41,7 @@ import {
|
|
|
39
41
|
DEFAULT_PUBLIC_TOOLING_CONFIG,
|
|
40
42
|
type PublicToolingConfig,
|
|
41
43
|
} from "../cli/public-config-contract";
|
|
44
|
+
import type { ImageAssetStore } from "../image/image-asset-store";
|
|
42
45
|
|
|
43
46
|
export class ToolRegistry {
|
|
44
47
|
private readonly tools = new Map<string, ToolExecutor>();
|
|
@@ -158,7 +161,10 @@ export function createDefaultTooling(options: {
|
|
|
158
161
|
skillCoordinator?: SkillActivationCoordinator;
|
|
159
162
|
toolingConfig?: PublicToolingConfig;
|
|
160
163
|
memorySearch?: ToolExecutor;
|
|
164
|
+
memoryGet?: ToolExecutor;
|
|
161
165
|
enableTurnUndo?: boolean;
|
|
166
|
+
imageAssetStore?: ImageAssetStore;
|
|
167
|
+
supportsViewImage?: boolean;
|
|
162
168
|
bashGuard?: {
|
|
163
169
|
readonly surface: "tui" | "one-shot";
|
|
164
170
|
confirm(
|
|
@@ -206,6 +212,14 @@ export function createDefaultTooling(options: {
|
|
|
206
212
|
maxContentBytes: options.maxReadContentBytes,
|
|
207
213
|
}),
|
|
208
214
|
);
|
|
215
|
+
if (options.supportsViewImage === true) {
|
|
216
|
+
if (options.imageAssetStore === undefined) {
|
|
217
|
+
throw new Error("ViewImage tooling requires an image asset store.");
|
|
218
|
+
}
|
|
219
|
+
registry.register(
|
|
220
|
+
createViewImageToolExecutor({ imageAssetStore: options.imageAssetStore }),
|
|
221
|
+
);
|
|
222
|
+
}
|
|
209
223
|
registry.register(
|
|
210
224
|
createRecallSearchToolExecutor({ historyReader: options.historyReader }),
|
|
211
225
|
);
|
|
@@ -215,6 +229,9 @@ export function createDefaultTooling(options: {
|
|
|
215
229
|
if (options.memorySearch !== undefined) {
|
|
216
230
|
registry.register(options.memorySearch);
|
|
217
231
|
}
|
|
232
|
+
if (options.memoryGet !== undefined) {
|
|
233
|
+
registry.register(options.memoryGet);
|
|
234
|
+
}
|
|
218
235
|
if (options.skillCatalog !== undefined) {
|
|
219
236
|
if (options.skillCatalog.skills.size === 0) {
|
|
220
237
|
throw new Error("An empty Agent Skill catalog must not register tooling.");
|
|
@@ -262,6 +279,7 @@ export function createDefaultTooling(options: {
|
|
|
262
279
|
}),
|
|
263
280
|
);
|
|
264
281
|
registry.register(createUpdatePlanToolExecutor());
|
|
282
|
+
registry.register(createWaitToolExecutor());
|
|
265
283
|
registry.register(createTaskListToolExecutor({ taskManager }));
|
|
266
284
|
registry.register(createTaskOutputToolExecutor({ taskManager }));
|
|
267
285
|
registry.register(createTaskInputToolExecutor({ taskManager }));
|
package/src/tools/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ToolCall } from "../agent/types";
|
|
2
|
+
import type { ImageAssetRef } from "../image/image-types";
|
|
2
3
|
import type { SessionId } from "../ids/runtime-id";
|
|
3
4
|
import type {
|
|
4
5
|
RecallGetPage,
|
|
@@ -38,6 +39,14 @@ export type ReadFileRawResult = {
|
|
|
38
39
|
error?: string;
|
|
39
40
|
};
|
|
40
41
|
|
|
42
|
+
export type ViewImageRawResult = {
|
|
43
|
+
ok: boolean;
|
|
44
|
+
filePath: string;
|
|
45
|
+
originalName?: string;
|
|
46
|
+
asset?: ImageAssetRef;
|
|
47
|
+
error?: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
41
50
|
export type WriteFileRawResult = {
|
|
42
51
|
ok: boolean;
|
|
43
52
|
filePath: string;
|
|
@@ -304,10 +313,15 @@ export type RecallRawResult = RecallSearchRawResult | RecallGetRawResult;
|
|
|
304
313
|
export type MemorySearchRawResult =
|
|
305
314
|
| {
|
|
306
315
|
ok: true;
|
|
316
|
+
degraded: "vector" | "fts" | null;
|
|
307
317
|
matches: readonly {
|
|
318
|
+
memoryId: string;
|
|
308
319
|
text: string;
|
|
320
|
+
summary: string;
|
|
309
321
|
score: number;
|
|
322
|
+
via: readonly ("vector" | "fts")[];
|
|
310
323
|
sourceWorkspace: string;
|
|
324
|
+
sourceSessionId: string;
|
|
311
325
|
createdAt: string;
|
|
312
326
|
}[];
|
|
313
327
|
}
|
|
@@ -316,6 +330,24 @@ export type MemorySearchRawResult =
|
|
|
316
330
|
error: string;
|
|
317
331
|
};
|
|
318
332
|
|
|
333
|
+
export type MemoryGetRawResult =
|
|
334
|
+
| {
|
|
335
|
+
ok: true;
|
|
336
|
+
memory: {
|
|
337
|
+
memoryId: string;
|
|
338
|
+
text: string;
|
|
339
|
+
summary: string;
|
|
340
|
+
sourceWorkspace: string;
|
|
341
|
+
sourceSessionId: string;
|
|
342
|
+
sourceTurnId: string;
|
|
343
|
+
createdAt: string;
|
|
344
|
+
} | null;
|
|
345
|
+
}
|
|
346
|
+
| {
|
|
347
|
+
ok: false;
|
|
348
|
+
error: string;
|
|
349
|
+
};
|
|
350
|
+
|
|
319
351
|
export type SkillRawResult =
|
|
320
352
|
| {
|
|
321
353
|
ok: true;
|
|
@@ -365,8 +397,20 @@ export type McpToolRawResult = {
|
|
|
365
397
|
error?: string;
|
|
366
398
|
};
|
|
367
399
|
|
|
400
|
+
export type WaitRawResult =
|
|
401
|
+
| {
|
|
402
|
+
ok: true;
|
|
403
|
+
seconds: number;
|
|
404
|
+
waitedMs: number;
|
|
405
|
+
}
|
|
406
|
+
| {
|
|
407
|
+
ok: false;
|
|
408
|
+
error: string;
|
|
409
|
+
};
|
|
410
|
+
|
|
368
411
|
export type ToolRawResultByKind = {
|
|
369
412
|
read: ReadFileRawResult;
|
|
413
|
+
view_image: ViewImageRawResult;
|
|
370
414
|
write: WriteFileRawResult;
|
|
371
415
|
edit: EditFileRawResult;
|
|
372
416
|
delete: DeleteFileRawResult;
|
|
@@ -382,6 +426,8 @@ export type ToolRawResultByKind = {
|
|
|
382
426
|
web_fetch: WebFetchRawResult;
|
|
383
427
|
recall: RecallRawResult;
|
|
384
428
|
memory_search: MemorySearchRawResult;
|
|
429
|
+
memory_get: MemoryGetRawResult;
|
|
430
|
+
wait: WaitRawResult;
|
|
385
431
|
skill: SkillRawResult;
|
|
386
432
|
mcp: McpToolRawResult;
|
|
387
433
|
generic: GenericToolRawResult;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { ImageAssetStore } from "../image/image-asset-store";
|
|
2
|
+
import { defineToolExecutor } from "./types";
|
|
3
|
+
|
|
4
|
+
export const VIEW_IMAGE_TOOL_DEFINITION = Object.freeze({
|
|
5
|
+
name: "ViewImage",
|
|
6
|
+
description:
|
|
7
|
+
"View one local image and return it to the model. Supports PNG, JPEG, and WebP. " +
|
|
8
|
+
"Relative paths resolve within the workspace; absolute paths may point outside it. " +
|
|
9
|
+
"Symbolic links are not supported.",
|
|
10
|
+
parameters: Object.freeze({
|
|
11
|
+
type: "object",
|
|
12
|
+
additionalProperties: false,
|
|
13
|
+
properties: Object.freeze({
|
|
14
|
+
file_path: Object.freeze({
|
|
15
|
+
type: "string",
|
|
16
|
+
description: "Workspace-relative path or absolute path to one image file.",
|
|
17
|
+
}),
|
|
18
|
+
}),
|
|
19
|
+
required: Object.freeze(["file_path"]),
|
|
20
|
+
}),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export function createViewImageToolExecutor(input: {
|
|
24
|
+
imageAssetStore: ImageAssetStore;
|
|
25
|
+
}) {
|
|
26
|
+
return defineToolExecutor("view_image", {
|
|
27
|
+
definition: VIEW_IMAGE_TOOL_DEFINITION,
|
|
28
|
+
async execute(args, _call, context) {
|
|
29
|
+
context.signal.throwIfAborted();
|
|
30
|
+
const parsed = parseArguments(args);
|
|
31
|
+
if (!parsed.ok) {
|
|
32
|
+
return {
|
|
33
|
+
ok: false,
|
|
34
|
+
filePath: parsed.filePath,
|
|
35
|
+
error: parsed.error,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const imported = await input.imageAssetStore.importFile(parsed.filePath, {
|
|
40
|
+
signal: context.signal,
|
|
41
|
+
});
|
|
42
|
+
context.signal.throwIfAborted();
|
|
43
|
+
return {
|
|
44
|
+
ok: true,
|
|
45
|
+
filePath: parsed.filePath,
|
|
46
|
+
originalName: imported.originalName,
|
|
47
|
+
asset: imported.asset,
|
|
48
|
+
};
|
|
49
|
+
} catch (error) {
|
|
50
|
+
context.signal.throwIfAborted();
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
filePath: parsed.filePath,
|
|
54
|
+
error: error instanceof Error ? error.message : String(error),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseArguments(
|
|
62
|
+
value: unknown,
|
|
63
|
+
):
|
|
64
|
+
| { readonly ok: true; readonly filePath: string }
|
|
65
|
+
| { readonly ok: false; readonly filePath: string; readonly error: string } {
|
|
66
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
filePath: "",
|
|
70
|
+
error: "Arguments must be an object containing only file_path.",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const record = value as Record<string, unknown>;
|
|
74
|
+
const keys = Object.keys(record);
|
|
75
|
+
const filePath = typeof record.file_path === "string" ? record.file_path : "";
|
|
76
|
+
if (
|
|
77
|
+
keys.length !== 1 ||
|
|
78
|
+
keys[0] !== "file_path" ||
|
|
79
|
+
typeof record.file_path !== "string" ||
|
|
80
|
+
record.file_path.trim() === ""
|
|
81
|
+
) {
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
filePath,
|
|
85
|
+
error: "file_path must be the only argument and must be a non-empty string.",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return { ok: true, filePath: record.file_path };
|
|
89
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { cancellationError, throwIfTurnCancelled } from "../agent/turn-cancellation";
|
|
2
|
+
import {
|
|
3
|
+
defineToolExecutor,
|
|
4
|
+
type ToolExecutionContext,
|
|
5
|
+
type ToolExecutor,
|
|
6
|
+
type WaitRawResult,
|
|
7
|
+
} from "./types";
|
|
8
|
+
|
|
9
|
+
const MIN_WAIT_SECONDS = 1;
|
|
10
|
+
const MAX_WAIT_SECONDS = 3600;
|
|
11
|
+
|
|
12
|
+
export function createWaitToolExecutor(): ToolExecutor {
|
|
13
|
+
return defineToolExecutor("wait", {
|
|
14
|
+
definition: {
|
|
15
|
+
name: "Wait",
|
|
16
|
+
description:
|
|
17
|
+
`Wait for a given number of integer seconds before continuing, from ${MIN_WAIT_SECONDS} to ${MAX_WAIT_SECONDS}. ` +
|
|
18
|
+
"Use this to pause between polling attempts or to give an external process time to make progress.",
|
|
19
|
+
parameters: {
|
|
20
|
+
type: "object",
|
|
21
|
+
additionalProperties: false,
|
|
22
|
+
properties: {
|
|
23
|
+
seconds: {
|
|
24
|
+
type: "integer",
|
|
25
|
+
minimum: MIN_WAIT_SECONDS,
|
|
26
|
+
maximum: MAX_WAIT_SECONDS,
|
|
27
|
+
description: "Whole seconds to wait.",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
required: ["seconds"],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
async execute(args, _call, context: ToolExecutionContext): Promise<WaitRawResult> {
|
|
34
|
+
throwIfTurnCancelled(context.signal);
|
|
35
|
+
const parsed = parseWaitArgs(args);
|
|
36
|
+
if (!parsed.ok) {
|
|
37
|
+
return parsed;
|
|
38
|
+
}
|
|
39
|
+
const startedAt = Date.now();
|
|
40
|
+
await interruptibleSleep(parsed.seconds * 1_000, context.signal);
|
|
41
|
+
throwIfTurnCancelled(context.signal);
|
|
42
|
+
return { ok: true, seconds: parsed.seconds, waitedMs: Date.now() - startedAt };
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type ParsedWaitArgs = { ok: true; seconds: number } | { ok: false; error: string };
|
|
48
|
+
|
|
49
|
+
function parseWaitArgs(args: unknown): ParsedWaitArgs {
|
|
50
|
+
if (!isRecord(args)) {
|
|
51
|
+
return { ok: false, error: "Wait arguments must be an object." };
|
|
52
|
+
}
|
|
53
|
+
const unexpected = Object.keys(args).find((key) => key !== "seconds");
|
|
54
|
+
if (unexpected !== undefined) {
|
|
55
|
+
return { ok: false, error: `Wait received unexpected argument: ${unexpected}.` };
|
|
56
|
+
}
|
|
57
|
+
if (typeof args.seconds !== "number" || !Number.isInteger(args.seconds)) {
|
|
58
|
+
return { ok: false, error: "Wait seconds must be an integer." };
|
|
59
|
+
}
|
|
60
|
+
if (args.seconds < MIN_WAIT_SECONDS || args.seconds > MAX_WAIT_SECONDS) {
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
error: `Wait seconds must be between ${MIN_WAIT_SECONDS} and ${MAX_WAIT_SECONDS}.`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return { ok: true, seconds: args.seconds };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function interruptibleSleep(ms: number, signal: AbortSignal): Promise<void> {
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
const onAbort = () => {
|
|
72
|
+
clearTimeout(timer);
|
|
73
|
+
reject(cancellationError(signal));
|
|
74
|
+
};
|
|
75
|
+
const timer = setTimeout(() => {
|
|
76
|
+
signal.removeEventListener("abort", onAbort);
|
|
77
|
+
resolve();
|
|
78
|
+
}, ms);
|
|
79
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
84
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
85
|
+
}
|
|
@@ -93,6 +93,9 @@ export function MemoryBrowser(props: MemoryBrowserProps) {
|
|
|
93
93
|
{formatMemoryCreatedAt(memory.createdAt)} · {memory.sourceWorkspace}
|
|
94
94
|
</Text>
|
|
95
95
|
<Text>{normalizeMemoryDisplayText(memory.text)}</Text>
|
|
96
|
+
{memory.summary === "" ? null : (
|
|
97
|
+
<Text dimColor>{normalizeMemoryDisplayText(memory.summary)}</Text>
|
|
98
|
+
)}
|
|
96
99
|
</Box>
|
|
97
100
|
))}
|
|
98
101
|
</Box>
|
|
@@ -741,30 +741,42 @@ export function PromptInput(props: PromptInputProps) {
|
|
|
741
741
|
</Box>
|
|
742
742
|
{showSuggestions ? null : (
|
|
743
743
|
<Box>
|
|
744
|
-
<Text
|
|
745
|
-
{props.modelName}
|
|
746
|
-
{props.reasoningEffort === undefined ? null :
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
744
|
+
<Text>
|
|
745
|
+
<Text color={FOOTER_COLORS.model}>{props.modelName}</Text>
|
|
746
|
+
{props.reasoningEffort === undefined ? null : (
|
|
747
|
+
<Text dimColor> {props.reasoningEffort}</Text>
|
|
748
|
+
)}
|
|
749
|
+
<Text dimColor> · </Text>
|
|
750
|
+
<Text color={FOOTER_COLORS.workspacePath}>
|
|
751
|
+
{formatWorkspacePath(props.workspaceRoot)}
|
|
752
|
+
</Text>
|
|
753
|
+
{props.gitBranch === undefined ? null : (
|
|
754
|
+
<>
|
|
755
|
+
<Text dimColor> · </Text>
|
|
756
|
+
<Text color={FOOTER_COLORS.gitBranch}>{props.gitBranch}</Text>
|
|
757
|
+
</>
|
|
758
|
+
)}
|
|
759
|
+
{state.phase.kind === "idle" ? null : (
|
|
760
|
+
<>
|
|
761
|
+
<Text dimColor> · </Text>
|
|
762
|
+
<Text dimColor>{phaseLabel(state.phase)}</Text>
|
|
763
|
+
</>
|
|
764
|
+
)}
|
|
765
|
+
{props.contextUsage === undefined ? null : (
|
|
766
|
+
<>
|
|
767
|
+
<Text dimColor> · </Text>
|
|
768
|
+
<Text color={contextColor(props.contextUsage.pressure)}>
|
|
769
|
+
{formatContextUsageLine(props.contextUsage)}
|
|
770
|
+
</Text>
|
|
771
|
+
</>
|
|
772
|
+
)}
|
|
773
|
+
{cacheRate === undefined ? null : (
|
|
774
|
+
<>
|
|
775
|
+
<Text dimColor> · </Text>
|
|
776
|
+
<Text color={FOOTER_COLORS.cacheRate}>{cacheRate}</Text>
|
|
777
|
+
</>
|
|
778
|
+
)}
|
|
750
779
|
</Text>
|
|
751
|
-
{props.contextUsage === undefined ? null : (
|
|
752
|
-
<>
|
|
753
|
-
<Text dimColor> · </Text>
|
|
754
|
-
<Text
|
|
755
|
-
color={contextColor(props.contextUsage.pressure)}
|
|
756
|
-
dimColor={props.contextUsage.pressure === "normal"}
|
|
757
|
-
>
|
|
758
|
-
{formatContextUsageLine(props.contextUsage)}
|
|
759
|
-
</Text>
|
|
760
|
-
</>
|
|
761
|
-
)}
|
|
762
|
-
{cacheRate === undefined ? null : (
|
|
763
|
-
<>
|
|
764
|
-
<Text dimColor> · </Text>
|
|
765
|
-
<Text dimColor>{cacheRate}</Text>
|
|
766
|
-
</>
|
|
767
|
-
)}
|
|
768
780
|
</Box>
|
|
769
781
|
)}
|
|
770
782
|
{showFileSuggestions ? (
|
|
@@ -962,14 +974,25 @@ function fileMentionReplacement(
|
|
|
962
974
|
: `${filePath} `;
|
|
963
975
|
}
|
|
964
976
|
|
|
977
|
+
// Decorative palette for the footer status line, kept muted so it reads
|
|
978
|
+
// softly against the dim separators. Red and yellow are deliberately
|
|
979
|
+
// excluded: they stay reserved for context-pressure warnings.
|
|
980
|
+
const FOOTER_COLORS = {
|
|
981
|
+
model: "#7b9cc4",
|
|
982
|
+
workspacePath: "#6fa8a8",
|
|
983
|
+
gitBranch: "#a08fbd",
|
|
984
|
+
cacheRate: "#7aa37e",
|
|
985
|
+
contextUsage: "#8a9199",
|
|
986
|
+
} as const;
|
|
987
|
+
|
|
965
988
|
function contextColor(
|
|
966
989
|
pressure: ContextUsageSnapshot["pressure"],
|
|
967
|
-
): "yellow" | "red" |
|
|
990
|
+
): "yellow" | "red" | typeof FOOTER_COLORS.contextUsage {
|
|
968
991
|
return pressure === "blocked"
|
|
969
992
|
? "red"
|
|
970
993
|
: pressure === "triggered"
|
|
971
994
|
? "yellow"
|
|
972
|
-
:
|
|
995
|
+
: FOOTER_COLORS.contextUsage;
|
|
973
996
|
}
|
|
974
997
|
|
|
975
998
|
function formatWorkspacePath(workspaceRoot: string): string {
|
package/src/tui/event-store.ts
CHANGED
|
@@ -785,8 +785,11 @@ function toolCallSummary(input: { name: string; args: unknown }): string {
|
|
|
785
785
|
if (input.name === "Grep") {
|
|
786
786
|
return `Grep ${toolPattern(input.args) ?? ""}`.trim();
|
|
787
787
|
}
|
|
788
|
-
if (input.name === "WebSearch"
|
|
789
|
-
return
|
|
788
|
+
if (input.name === "WebSearch") {
|
|
789
|
+
return `WebSearch ${toolQuery(input.args) ?? ""}`.trim();
|
|
790
|
+
}
|
|
791
|
+
if (input.name === "MemorySearch") {
|
|
792
|
+
return `MemorySearch ${memorySearchDetail(input.args)}`.trim();
|
|
790
793
|
}
|
|
791
794
|
if (input.name === "WebFetch") {
|
|
792
795
|
return `WebFetch ${toolUrl(input.args) ?? ""}`.trim();
|
|
@@ -801,6 +804,9 @@ function toolCallSummary(input: { name: string; args: unknown }): string {
|
|
|
801
804
|
if (input.name === "TaskList") {
|
|
802
805
|
return "TaskList";
|
|
803
806
|
}
|
|
807
|
+
if (input.name === "Wait") {
|
|
808
|
+
return `Wait ${toolSeconds(input.args) ?? ""}`.trim();
|
|
809
|
+
}
|
|
804
810
|
if (input.name === "Skill") {
|
|
805
811
|
return `Skill ${stringProperty(asRecord(input.args), "name") ?? ""}`.trim();
|
|
806
812
|
}
|
|
@@ -816,6 +822,10 @@ function toolRawResultSummary(name: string, args: unknown, raw: ToolRawResult):
|
|
|
816
822
|
}
|
|
817
823
|
|
|
818
824
|
switch (raw.kind) {
|
|
825
|
+
case "view_image":
|
|
826
|
+
return raw.asset === undefined
|
|
827
|
+
? base
|
|
828
|
+
: `${base} -> ${raw.asset.mimeType}, ${raw.asset.width}x${raw.asset.height}, ${raw.asset.byteLength} bytes`;
|
|
819
829
|
case "glob":
|
|
820
830
|
return raw.ok && raw.matchCount !== undefined
|
|
821
831
|
? `${base} -> ${raw.matchCount} match${raw.matchCount === 1 ? "" : "es"}`
|
|
@@ -878,6 +888,11 @@ function toolRawResultSummary(name: string, args: unknown, raw: ToolRawResult):
|
|
|
878
888
|
return base;
|
|
879
889
|
}
|
|
880
890
|
return `${base} -> ${raw.matches.length} derived memor${raw.matches.length === 1 ? "y" : "ies"}`;
|
|
891
|
+
case "memory_get":
|
|
892
|
+
if (!raw.ok) {
|
|
893
|
+
return base;
|
|
894
|
+
}
|
|
895
|
+
return raw.memory === null ? `${base} -> not found` : `${base} -> found`;
|
|
881
896
|
case "skill":
|
|
882
897
|
if (!raw.ok) {
|
|
883
898
|
return `${base} failed -> ${boundedToolError(raw.error)}`;
|
|
@@ -915,6 +930,8 @@ function toolRawResultSummary(name: string, args: unknown, raw: ToolRawResult):
|
|
|
915
930
|
const completed = raw.plan.filter((step) => step.status === "completed").length;
|
|
916
931
|
return `${base} -> ${completed}/${raw.plan.length} completed`;
|
|
917
932
|
}
|
|
933
|
+
case "wait":
|
|
934
|
+
return raw.ok ? `${base} -> done` : base;
|
|
918
935
|
case "mcp":
|
|
919
936
|
case "generic":
|
|
920
937
|
return base;
|
|
@@ -951,6 +968,7 @@ function toolRawResultBashDetail(raw: ToolRawResult): Pick<TimelineItem, "bash">
|
|
|
951
968
|
return detail === undefined ? {} : { bash: detail };
|
|
952
969
|
}
|
|
953
970
|
case "read":
|
|
971
|
+
case "view_image":
|
|
954
972
|
case "write":
|
|
955
973
|
case "edit":
|
|
956
974
|
case "delete":
|
|
@@ -963,6 +981,8 @@ function toolRawResultBashDetail(raw: ToolRawResult): Pick<TimelineItem, "bash">
|
|
|
963
981
|
case "web_fetch":
|
|
964
982
|
case "recall":
|
|
965
983
|
case "memory_search":
|
|
984
|
+
case "memory_get":
|
|
985
|
+
case "wait":
|
|
966
986
|
case "skill":
|
|
967
987
|
case "mcp":
|
|
968
988
|
case "generic":
|
|
@@ -988,6 +1008,7 @@ function toolRawResultDiff(
|
|
|
988
1008
|
diffTruncated: raw.patchTruncated === true,
|
|
989
1009
|
};
|
|
990
1010
|
case "read":
|
|
1011
|
+
case "view_image":
|
|
991
1012
|
case "delete":
|
|
992
1013
|
case "glob":
|
|
993
1014
|
case "grep":
|
|
@@ -1001,6 +1022,8 @@ function toolRawResultDiff(
|
|
|
1001
1022
|
case "web_fetch":
|
|
1002
1023
|
case "recall":
|
|
1003
1024
|
case "memory_search":
|
|
1025
|
+
case "memory_get":
|
|
1026
|
+
case "wait":
|
|
1004
1027
|
case "skill":
|
|
1005
1028
|
case "mcp":
|
|
1006
1029
|
case "generic":
|
|
@@ -1067,10 +1090,46 @@ function toolQuery(args: unknown): string | undefined {
|
|
|
1067
1090
|
return stringProperty(asRecord(args), "query");
|
|
1068
1091
|
}
|
|
1069
1092
|
|
|
1093
|
+
function memorySearchDetail(args: unknown): string {
|
|
1094
|
+
const parts: string[] = [];
|
|
1095
|
+
const query = toolQuery(args);
|
|
1096
|
+
if (query !== undefined) {
|
|
1097
|
+
parts.push(query);
|
|
1098
|
+
}
|
|
1099
|
+
const keywords = toolKeywords(args);
|
|
1100
|
+
if (keywords !== undefined) {
|
|
1101
|
+
parts.push(`[${keywords}]`);
|
|
1102
|
+
}
|
|
1103
|
+
return parts.join(" ");
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
function toolKeywords(args: unknown): string | undefined {
|
|
1107
|
+
const limit = 120;
|
|
1108
|
+
const value = asRecord(args).keywords;
|
|
1109
|
+
if (!Array.isArray(value)) {
|
|
1110
|
+
return undefined;
|
|
1111
|
+
}
|
|
1112
|
+
const keywords = value.filter(
|
|
1113
|
+
(keyword): keyword is string => typeof keyword === "string" && keyword.length > 0,
|
|
1114
|
+
);
|
|
1115
|
+
if (keywords.length === 0) {
|
|
1116
|
+
return undefined;
|
|
1117
|
+
}
|
|
1118
|
+
const joined = keywords.join(", ");
|
|
1119
|
+
return joined.length <= limit ? joined : `${joined.slice(0, limit)}…`;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1070
1122
|
function toolUrl(args: unknown): string | undefined {
|
|
1071
1123
|
return stringProperty(asRecord(args), "url");
|
|
1072
1124
|
}
|
|
1073
1125
|
|
|
1126
|
+
function toolSeconds(args: unknown): string | undefined {
|
|
1127
|
+
const seconds = asRecord(args).seconds;
|
|
1128
|
+
return typeof seconds === "number" && Number.isFinite(seconds)
|
|
1129
|
+
? `${seconds}s`
|
|
1130
|
+
: undefined;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1074
1133
|
function toolPath(args: unknown): string | undefined {
|
|
1075
1134
|
const record = asRecord(args);
|
|
1076
1135
|
return stringProperty(record, "file_path");
|