@falkenslab/agent-kit 0.14.0 → 0.16.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/README.md +110 -110
- package/assets/knowledge-plugin/.claude-plugin/plugin.json +1 -1
- package/assets/knowledge-plugin/commands/ingest.md +1 -1
- package/assets/knowledge-plugin/commands/lint.md +1 -1
- package/assets/knowledge-plugin/skills/knowledge-ingest/SKILL.md +13 -19
- package/assets/knowledge-plugin/skills/knowledge-lint/SKILL.md +18 -14
- package/assets/knowledge-plugin/skills/knowledge-query/SKILL.md +5 -5
- package/assets/knowledge-plugin-files/.claude-plugin/plugin.json +4 -0
- package/assets/knowledge-plugin-files/commands/ingest.md +7 -0
- package/assets/knowledge-plugin-files/commands/lint.md +5 -0
- package/assets/knowledge-plugin-files/commands/query.md +5 -0
- package/assets/knowledge-plugin-files/skills/knowledge-ingest/SKILL.md +46 -0
- package/assets/knowledge-plugin-files/skills/knowledge-lint/SKILL.md +26 -0
- package/assets/knowledge-plugin-files/skills/knowledge-query/SKILL.md +14 -0
- package/dist/core/agentSpec.d.ts +27 -2
- package/dist/core/extractText.d.ts +13 -0
- package/dist/core/extractText.js +147 -0
- package/dist/core/fileKnowledgeStore.d.ts +10 -0
- package/dist/core/fileKnowledgeStore.js +378 -0
- package/dist/core/hooks/fileScopeGate.d.ts +8 -0
- package/dist/core/hooks/fileScopeGate.js +20 -0
- package/dist/core/hooks/humanInput.d.ts +18 -0
- package/dist/core/hooks/humanInput.js +30 -2
- package/dist/core/hooks/planGate.js +14 -5
- package/dist/core/hooks/stepGate.js +6 -0
- package/dist/core/interaction.d.ts +20 -0
- package/dist/core/knowledge.d.ts +14 -4
- package/dist/core/knowledge.js +36 -6
- package/dist/core/knowledgeStore.d.ts +134 -0
- package/dist/core/knowledgeStore.js +84 -0
- package/dist/core/messages/de.js +65 -0
- package/dist/core/messages/en.d.ts +41 -0
- package/dist/core/messages/en.js +65 -0
- package/dist/core/messages/es.js +65 -0
- package/dist/core/messages/fr.js +65 -0
- package/dist/core/modeControl.d.ts +36 -0
- package/dist/core/modeControl.js +61 -0
- package/dist/core/session.d.ts +6 -31
- package/dist/core/session.js +76 -71
- package/dist/core/sources.d.ts +122 -0
- package/dist/core/sources.js +312 -0
- package/dist/core/todos.d.ts +23 -0
- package/dist/core/todos.js +33 -0
- package/dist/core/toolLabels.d.ts +1 -1
- package/dist/core/toolLabels.js +42 -2
- package/dist/core/tools/humanApproval.d.ts +9 -5
- package/dist/core/tools/humanApproval.js +39 -7
- package/dist/core/tools/knowledgeTools.d.ts +16 -0
- package/dist/core/tools/knowledgeTools.js +161 -0
- package/dist/core/tools/saveToSources.d.ts +27 -8
- package/dist/core/tools/saveToSources.js +203 -55
- package/dist/core/tools/time.d.ts +29 -0
- package/dist/core/tools/time.js +129 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.js +4 -1
- package/dist/tui/chatTui.js +14 -1
- package/dist/tui/consoleRenderer.js +16 -2
- package/dist/tui/ink/SessionView.js +72 -17
- package/dist/tui/ink/inkInteraction.d.ts +4 -2
- package/dist/tui/ink/inkInteraction.js +20 -2
- package/dist/tui/ink/progressView.d.ts +5 -1
- package/dist/tui/ink/runChatInk.d.ts +16 -1
- package/dist/tui/ink/runChatInk.js +7 -0
- package/dist/tui/ink/sessionModel.d.ts +10 -3
- package/dist/tui/ink/sessionModel.js +26 -4
- package/dist/tui/ink/toolGroup.d.ts +23 -2
- package/dist/tui/ink/toolGroup.js +17 -4
- package/dist/tui/terminalInteraction.js +2 -0
- package/package.json +31 -1
- /package/assets/{knowledge-plugin → knowledge-plugin-files}/skills/knowledge-pages/SKILL.md +0 -0
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import wrapAnsi from "wrap-ansi";
|
|
3
3
|
import { useEffect, useReducer, useRef, useState, useSyncExternalStore } from "react";
|
|
4
4
|
import { Box, measureElement, Static, Text, useInput, useStdout } from "ink";
|
|
5
|
-
import { Select } from "@inkjs/ui";
|
|
5
|
+
import { MultiSelect, Select, TextInput } from "@inkjs/ui";
|
|
6
6
|
import { liveWidth } from "./sessionModel.js";
|
|
7
7
|
import { createRowCache, highlightRow, isMouseReport, mouseEvents, rowsBelow, scrollBy, selectedText, visibleRows, userBarRows, } from "./fullscreen.js";
|
|
8
8
|
import { framePosition } from "./terminalCursor.js";
|
|
@@ -11,6 +11,7 @@ import { fitWidth, stripAnsi } from "./lineBuffer.js";
|
|
|
11
11
|
import * as ui from "../ui.js";
|
|
12
12
|
import { t } from "../../core/messages/index.js";
|
|
13
13
|
import { inkColor } from "../theme.js";
|
|
14
|
+
import { renderMarkdown } from "./markdown.js";
|
|
14
15
|
// Numbered as in the Claude Code CLI's permission prompt; the digits and y/n/q answer too.
|
|
15
16
|
const decisionOptions = () => [
|
|
16
17
|
{ label: `1. ${t().yes}`, value: "y" },
|
|
@@ -44,16 +45,42 @@ export function previewLines(lines, terminalRows, reservedRows = PANEL_RESERVED_
|
|
|
44
45
|
}
|
|
45
46
|
function CheckpointPanel({ checkpoint, renderApproval, reservedRows, width, }) {
|
|
46
47
|
const decision = checkpoint.kind === "decision";
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
const text = checkpoint.kind === "text";
|
|
49
|
+
const choice = checkpoint.kind === "choice";
|
|
50
|
+
useInput((input, key) => {
|
|
51
|
+
if (text || choice) {
|
|
52
|
+
if (key.escape)
|
|
53
|
+
checkpoint.answer("");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const lower = input.toLowerCase();
|
|
57
|
+
if (decision && DECISION_KEYS[lower])
|
|
58
|
+
checkpoint.answer(DECISION_KEYS[lower]);
|
|
59
|
+
else if (!decision && lower === "1")
|
|
52
60
|
checkpoint.answer("");
|
|
53
61
|
});
|
|
54
62
|
const { prompt } = checkpoint;
|
|
55
63
|
const { stdout } = useStdout();
|
|
56
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: inkColor("accent"), paddingX: 1, children: [renderApproval ? (renderApproval(prompt)) : (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: prompt.title }), previewLines(prompt.lines, stdout.rows, reservedRows, width - PANEL_FRAME_COLUMNS).map((line, index) => (_jsx(Text, { children: line }, index))), !decision && prompt.question ? _jsx(Text, { dimColor: true, children: prompt.question }) : null] })), decision ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { children: t().proceed }) })) : null, _jsx(Box, { marginTop: decision ? 0 : 1, children: _jsx(Select, { options: decision ? decisionOptions() : manualOptions(), onChange: (value) => checkpoint.answer(decision ? value : "") }, checkpoint.id) })] }));
|
|
64
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: inkColor("accent"), paddingX: 1, children: [renderApproval ? (renderApproval(prompt)) : (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: prompt.title }), previewLines(prompt.markdown ? renderMarkdown(prompt.lines.join("\n"), width - PANEL_FRAME_COLUMNS) : prompt.lines, stdout.rows, reservedRows, width - PANEL_FRAME_COLUMNS).map((line, index) => (_jsx(Text, { children: line }, index))), !decision && prompt.question ? _jsx(Text, { dimColor: true, children: prompt.question }) : null] })), decision ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { children: t().proceed }) })) : null, choice && checkpoint.choice ? (_jsx(ChoiceInput, { choice: checkpoint.choice, onAnswer: (value) => checkpoint.answer(value) }, checkpoint.id)) : text ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { children: "> " }), _jsx(TextInput, { placeholder: t().textAnswerHint, onSubmit: (value) => checkpoint.answer(value.trim()) }, checkpoint.id)] }), _jsx(Text, { dimColor: true, children: t().textAnswerKeys })] })) : (_jsx(Box, { marginTop: decision ? 0 : 1, children: _jsx(Select, { options: decision ? decisionOptions() : manualOptions(), onChange: (value) => checkpoint.answer(decision ? value : "") }, checkpoint.id) }))] }));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* A choice's options (one with Select, several with MultiSelect), plus "Other" to type an
|
|
68
|
+
* answer of one's own; answers as `InteractionPort.askChoice()` says: the numbers, then the
|
|
69
|
+
* typed answer on the next line.
|
|
70
|
+
*/
|
|
71
|
+
function ChoiceInput({ choice, onAnswer }) {
|
|
72
|
+
const [typing, setTyping] = useState(null);
|
|
73
|
+
const options = [...choice.options.map((label, i) => ({ label: `${i + 1}. ${label}`, value: String(i + 1) })), { label: t().otherOption, value: "other" }];
|
|
74
|
+
if (typing !== null) {
|
|
75
|
+
return (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { children: "> " }), _jsx(TextInput, { placeholder: t().textAnswerHint, onSubmit: (value) => onAnswer([typing, value.trim()].filter(Boolean).join("\n")) })] }), _jsx(Text, { dimColor: true, children: t().textAnswerKeys })] }));
|
|
76
|
+
}
|
|
77
|
+
return (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [choice.multiple ? (_jsx(MultiSelect, { options: options, onSubmit: (values) => {
|
|
78
|
+
const picked = values.filter((value) => value !== "other").join(",");
|
|
79
|
+
if (values.includes("other"))
|
|
80
|
+
setTyping(picked);
|
|
81
|
+
else
|
|
82
|
+
onAnswer(picked);
|
|
83
|
+
} })) : (_jsx(Select, { options: options, onChange: (value) => (value === "other" ? setTyping("") : onAnswer(value)) })), _jsx(Text, { dimColor: true, children: t().choiceKeys(choice.multiple) })] }));
|
|
57
84
|
}
|
|
58
85
|
function formatTokens(count) {
|
|
59
86
|
return count >= 1000 ? `${(count / 1000).toFixed(1)}k` : String(count);
|
|
@@ -89,7 +116,21 @@ function LiveArea({ session, checkpoint, renderApproval, mode, width, statusExtr
|
|
|
89
116
|
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, width: width, marginTop: session.liveGap ? 1 : 0, children: [stripAnsi(session.live) ? _jsx(Text, { children: session.live }) : null, checkpoint ? (_jsx(CheckpointPanel, { checkpoint: checkpoint, renderApproval: renderApproval, reservedRows: reservedRows, width: width })) : session.busy ? (
|
|
90
117
|
// Always one blank line above the spinner: from this margin, or from the live area's
|
|
91
118
|
// own when a separator is pending and nothing is written above the spinner yet.
|
|
92
|
-
_jsxs(Box, { flexDirection: "column", marginTop: stripAnsi(session.live) || !session.liveGap ? 1 : 0, children: [_jsx(Working, { label: session.activity ?? t().thinking, startedAt: session.turnStartedAt, width: width }), session.subagentActivity ? _jsx(Text, { dimColor: true, children: fitWidth(` ↳ ${session.subagentActivity}`, width) }) : null] })) : null, checkpoint ? null : children, _jsx(Text, { children: fitWidth(status, width) })] }));
|
|
119
|
+
_jsxs(Box, { flexDirection: "column", marginTop: stripAnsi(session.live) || !session.liveGap ? 1 : 0, children: [_jsx(Working, { label: session.activity ?? t().thinking, startedAt: session.turnStartedAt, width: width }), session.subagentActivity ? _jsx(Text, { dimColor: true, children: fitWidth(` ↳ ${session.subagentActivity}`, width) }) : null] })) : null, checkpoint || !session.todos ? null : _jsx(TodoList, { todos: session.todos, width: width }), checkpoint ? null : children, _jsx(Text, { children: fitWidth(status, width) })] }));
|
|
120
|
+
}
|
|
121
|
+
// At most this many tasks show; the rest are counted (the one in progress always shows).
|
|
122
|
+
const MAX_TODOS = 8;
|
|
123
|
+
/** The agent's task list (TodoWrite), as in the Claude Code CLI: `☐` pending, `◼` in progress, `☑` done. */
|
|
124
|
+
function TodoList({ todos, width }) {
|
|
125
|
+
const current = todos.findIndex((todo) => todo.status === "in_progress");
|
|
126
|
+
const first = Math.max(0, Math.min(current - 2, todos.length - MAX_TODOS));
|
|
127
|
+
const shown = todos.slice(first, first + MAX_TODOS);
|
|
128
|
+
const hidden = todos.length - shown.length;
|
|
129
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [shown.map((todo, i) => {
|
|
130
|
+
const line = ` ${i === 0 ? "⎿ " : " "}${todo.status === "completed" ? "☑" : todo.status === "in_progress" ? "◼" : "☐"} ${todo.content}`;
|
|
131
|
+
const fitted = fitWidth(line, width);
|
|
132
|
+
return (_jsx(Text, { children: todo.status === "completed" ? ui.dim(ui.strike(fitted)) : todo.status === "in_progress" ? ui.bold(fitted) : fitted }, first + i));
|
|
133
|
+
}), hidden > 0 ? _jsx(Text, { children: ui.dim(fitWidth(` ${t().moreTodos(hidden)}`, width)) }) : null] }));
|
|
93
134
|
}
|
|
94
135
|
/** A history line for the inline view: the human's line as a bar, a tool group unfolded with Ctrl+O. */
|
|
95
136
|
function inlineItem(item, expanded, width) {
|
|
@@ -133,6 +174,16 @@ function FullscreenSession({ session, checkpoint, renderApproval, mode, header,
|
|
|
133
174
|
if (height !== historyHeight)
|
|
134
175
|
setHistoryHeight(height);
|
|
135
176
|
});
|
|
177
|
+
// The history's height now: `historyHeight` is set by the effect above after the layout,
|
|
178
|
+
// so a key or a click on the first frames would still find the terminal's height.
|
|
179
|
+
function heightNow() {
|
|
180
|
+
if (!historyRef.current)
|
|
181
|
+
return historyHeight;
|
|
182
|
+
const { height } = measureElement(historyRef.current);
|
|
183
|
+
if (height !== historyHeight)
|
|
184
|
+
setHistoryHeight(height);
|
|
185
|
+
return height;
|
|
186
|
+
}
|
|
136
187
|
const [anchor, setAnchor] = useState(null);
|
|
137
188
|
// Mirrored in a ref: mouse reports can arrive faster than React re-renders.
|
|
138
189
|
const [selection, setSelectionState] = useState(null);
|
|
@@ -161,12 +212,15 @@ function FullscreenSession({ session, checkpoint, renderApproval, mode, header,
|
|
|
161
212
|
if (!historyRef.current || end === start)
|
|
162
213
|
return null;
|
|
163
214
|
const box = framePosition(historyRef.current);
|
|
164
|
-
const
|
|
215
|
+
const height = heightNow();
|
|
216
|
+
const firstY = box.y + height - (end - start); // the rows are bottom-aligned
|
|
165
217
|
let offset = y - firstY;
|
|
166
|
-
|
|
218
|
+
// Rows that don't fit are clipped above the box, not clickable.
|
|
219
|
+
const firstShown = Math.max(0, box.y - firstY);
|
|
220
|
+
if (offset < firstShown || offset >= end - start) {
|
|
167
221
|
if (!clamp)
|
|
168
222
|
return null;
|
|
169
|
-
offset = Math.min(end - start - 1, Math.max(
|
|
223
|
+
offset = Math.min(end - start - 1, Math.max(firstShown, offset));
|
|
170
224
|
}
|
|
171
225
|
return { row: start + offset, col: Math.max(0, x - box.x) };
|
|
172
226
|
}
|
|
@@ -174,7 +228,8 @@ function FullscreenSession({ session, checkpoint, renderApproval, mode, header,
|
|
|
174
228
|
for (const event of mouseEvents(text)) {
|
|
175
229
|
if (event.kind === "wheel-up" || event.kind === "wheel-down") {
|
|
176
230
|
const steps = event.kind === "wheel-up" ? -WHEEL_ROWS : WHEEL_ROWS;
|
|
177
|
-
|
|
231
|
+
const height = heightNow();
|
|
232
|
+
setAnchor((a) => scrollBy(a, steps, total, height));
|
|
178
233
|
}
|
|
179
234
|
else if (event.kind === "press") {
|
|
180
235
|
const cell = cellAt(event.x, event.y, false);
|
|
@@ -205,15 +260,15 @@ function FullscreenSession({ session, checkpoint, renderApproval, mode, header,
|
|
|
205
260
|
}
|
|
206
261
|
}
|
|
207
262
|
useInput((text, key) => {
|
|
208
|
-
const page = Math.max(1, historyHeight - 1);
|
|
209
263
|
if (isMouseReport(text))
|
|
210
264
|
handleMouse(text);
|
|
211
265
|
else if (isFocusReport(text))
|
|
212
266
|
return;
|
|
213
|
-
else if (key.pageUp)
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
setAnchor((a) => scrollBy(a, page, total,
|
|
267
|
+
else if (key.pageUp || key.pageDown) {
|
|
268
|
+
const height = heightNow();
|
|
269
|
+
const page = Math.max(1, height - 1);
|
|
270
|
+
setAnchor((a) => scrollBy(a, key.pageUp ? -page : page, total, height));
|
|
271
|
+
}
|
|
217
272
|
else if (key.ctrl && key.end)
|
|
218
273
|
setAnchor(null);
|
|
219
274
|
else if (text && !key.ctrl && !key.meta && !key.escape)
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import type { ApprovalPrompt, InteractionPort } from "../../core/interaction.js";
|
|
2
|
-
export type CheckpointKind = "decision" | "manual-intervention";
|
|
1
|
+
import type { ApprovalPrompt, ChoiceSettings, InteractionPort } from "../../core/interaction.js";
|
|
2
|
+
export type CheckpointKind = "decision" | "manual-intervention" | "text" | "choice";
|
|
3
3
|
export interface Checkpoint {
|
|
4
4
|
id: number;
|
|
5
5
|
kind: CheckpointKind;
|
|
6
6
|
prompt: ApprovalPrompt;
|
|
7
|
+
/** For a choice: the options and whether several can be picked. */
|
|
8
|
+
choice?: ChoiceSettings;
|
|
7
9
|
/** Settles the checkpoint with the raw answer ("y", "n", "q" for a decision). */
|
|
8
10
|
answer(value: string): void;
|
|
9
11
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { parseChoice } from "../../core/hooks/humanInput.js";
|
|
1
2
|
import * as ui from "../ui.js";
|
|
3
|
+
import { renderMarkdown } from "./markdown.js";
|
|
2
4
|
import { t } from "../../core/messages/index.js";
|
|
3
5
|
const outcome = (answer) => ({ y: ui.success(t().approved), n: ui.warn(t().rejected), q: ui.error(t().stopped) })[answer];
|
|
4
6
|
/**
|
|
@@ -25,16 +27,30 @@ export function createInkInteraction(note) {
|
|
|
25
27
|
function record(prompt, outcome) {
|
|
26
28
|
note([ui.heading(`=== ${prompt.title} ===`), ...prompt.lines, outcome].join("\n"));
|
|
27
29
|
}
|
|
28
|
-
|
|
30
|
+
/** How a settled checkpoint is written to the history. */
|
|
31
|
+
function settledAs(kind, value, choice) {
|
|
32
|
+
if (kind === "decision")
|
|
33
|
+
return outcome(value) ?? value;
|
|
34
|
+
if (kind === "text")
|
|
35
|
+
return value ? ui.success(`✔ ${value}`) : ui.dim(t().noAnswer);
|
|
36
|
+
if (kind === "choice" && choice) {
|
|
37
|
+
const answer = parseChoice(value, choice.options, choice.multiple);
|
|
38
|
+
const words = [...answer.chosen, ...(answer.other ? [answer.other] : [])];
|
|
39
|
+
return words.length ? ui.success(`✔ ${words.join("; ")}`) : ui.dim(t().noAnswer);
|
|
40
|
+
}
|
|
41
|
+
return ui.success(t().done);
|
|
42
|
+
}
|
|
43
|
+
function ask(kind, prompt, signal, choice) {
|
|
29
44
|
return new Promise((resolve) => {
|
|
30
45
|
const checkpoint = {
|
|
31
46
|
id: nextId++,
|
|
32
47
|
kind,
|
|
33
48
|
prompt,
|
|
49
|
+
...(choice ? { choice } : {}),
|
|
34
50
|
answer(value) {
|
|
35
51
|
if (!remove(checkpoint))
|
|
36
52
|
return;
|
|
37
|
-
record(prompt
|
|
53
|
+
record(prompt.markdown ? { ...prompt, lines: renderMarkdown(prompt.lines.join("\n"), 80) } : prompt, settledAs(kind, value, choice));
|
|
38
54
|
resolve(value);
|
|
39
55
|
},
|
|
40
56
|
};
|
|
@@ -50,6 +66,8 @@ export function createInkInteraction(note) {
|
|
|
50
66
|
port: {
|
|
51
67
|
askDecision: (prompt, signal) => ask("decision", prompt, signal),
|
|
52
68
|
askManualIntervention: (prompt, signal) => ask("manual-intervention", prompt, signal),
|
|
69
|
+
askText: (prompt, signal) => ask("text", prompt, signal),
|
|
70
|
+
askChoice: (prompt, choice, signal) => ask("choice", prompt, signal, choice),
|
|
53
71
|
notify: (message) => note(message),
|
|
54
72
|
},
|
|
55
73
|
subscribe(listener) {
|
|
@@ -2,12 +2,16 @@ import type { Mode } from "../../core/agentSpec.js";
|
|
|
2
2
|
import { type ConsoleRenderer, type ConsoleRendererOptions } from "../consoleRenderer.js";
|
|
3
3
|
import { type SessionModel } from "./sessionModel.js";
|
|
4
4
|
import { type RenderApproval } from "./SessionView.js";
|
|
5
|
-
import type { ToolPhrase } from "./toolGroup.js";
|
|
5
|
+
import type { ResultFormatter, ToolDetail, ToolPhrase } from "./toolGroup.js";
|
|
6
6
|
import { type Theme } from "../theme.js";
|
|
7
7
|
/** Options of `createProgressView()`. */
|
|
8
8
|
export interface ProgressViewOptions extends Omit<ConsoleRendererOptions, "output"> {
|
|
9
9
|
/** How one of the agent's own tools counts in a folded group's summary (see `InkChatOptions.toolPhrase`). */
|
|
10
10
|
toolPhrase?: (toolName: string) => ToolPhrase | undefined;
|
|
11
|
+
/** How much of the tool calls shows (see `InkChatOptions.toolDetail`); "full" if not given. */
|
|
12
|
+
toolDetail?: ToolDetail;
|
|
13
|
+
/** The result line under a tool call (see `InkChatOptions.formatResult`). */
|
|
14
|
+
formatResult?: ResultFormatter;
|
|
11
15
|
/** Replaces the default preview in the approval and manual-intervention panels. */
|
|
12
16
|
renderApproval?: RenderApproval;
|
|
13
17
|
/** Shown in the status bar. */
|
|
@@ -4,7 +4,7 @@ import { type ModeControl } from "../../core/session.js";
|
|
|
4
4
|
import { type ChatTuiOptions } from "../chatTui.js";
|
|
5
5
|
import { type SessionOpener } from "../runs.js";
|
|
6
6
|
import { type RenderApproval } from "./SessionView.js";
|
|
7
|
-
import type { ToolPhrase } from "./toolGroup.js";
|
|
7
|
+
import type { ResultFormatter, ToolDetail, ToolPhrase } from "./toolGroup.js";
|
|
8
8
|
import { type HeaderInfo } from "./header.js";
|
|
9
9
|
export type { HeaderInfo } from "./header.js";
|
|
10
10
|
/** Options of `runChatInk()`: the chat's options plus the Ink-only ones. */
|
|
@@ -45,6 +45,21 @@ export interface InkChatOptions extends ChatTuiOptions {
|
|
|
45
45
|
* counts as "used {n} tools".
|
|
46
46
|
*/
|
|
47
47
|
toolPhrase?: (toolName: string) => ToolPhrase | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* How much of the tool calls shows: `"full"` (the default), every call with its result
|
|
50
|
+
* line; `"calls"`, the calls without their results, a failed one with a short line in the
|
|
51
|
+
* kit's language instead of the tool's error; `"summary"`, one line per group ("Read 2
|
|
52
|
+
* files, ran 1 shell command"). Ctrl+O unfolds any of them into the full view. For an
|
|
53
|
+
* audience that doesn't need the tools' raw output. The session log is the same whatever
|
|
54
|
+
* the level.
|
|
55
|
+
*/
|
|
56
|
+
toolDetail?: ToolDetail;
|
|
57
|
+
/**
|
|
58
|
+
* The line under a tool call for its result, e.g. a friendly one for an MCP tool whose
|
|
59
|
+
* output is written for the model: a string replaces the kit's (the result's first line),
|
|
60
|
+
* `null` hides it, `undefined` keeps it. Shown wherever results are (`"full"`, or unfolded).
|
|
61
|
+
*/
|
|
62
|
+
formatResult?: ResultFormatter;
|
|
48
63
|
/**
|
|
49
64
|
* The session's mode control (`buildSessionOptions()` returns it): the status bar shows the
|
|
50
65
|
* current mode, Shift+Tab switches between the modes it allows ("guided", "interactive"
|
|
@@ -210,6 +210,8 @@ export async function runChatInk(options, tuiOptions = {}) {
|
|
|
210
210
|
const model = createSessionModel({
|
|
211
211
|
formatAction: tuiOptions.formatAction,
|
|
212
212
|
toolPhrase: tuiOptions.toolPhrase,
|
|
213
|
+
toolDetail: tuiOptions.toolDetail,
|
|
214
|
+
formatResult: tuiOptions.formatResult,
|
|
213
215
|
agentLabel: tuiOptions.agentLabel,
|
|
214
216
|
onWrite: mirror,
|
|
215
217
|
width: () => liveWidth(stdout.columns),
|
|
@@ -242,12 +244,17 @@ export async function runChatInk(options, tuiOptions = {}) {
|
|
|
242
244
|
let run;
|
|
243
245
|
let knownCommands;
|
|
244
246
|
let generation = 0;
|
|
247
|
+
let unsubscribeMode;
|
|
245
248
|
function connect() {
|
|
246
249
|
const own = ++generation;
|
|
247
250
|
queue = createInputQueue({ modeControl: modeControl() });
|
|
248
251
|
run = runQuery(queue.iterable, { ...current.options, promptSuggestions: tuiOptions.promptSuggestions ?? true });
|
|
249
252
|
const events = run.events[Symbol.asyncIterator]();
|
|
250
253
|
input.setMode(modeControl()?.mode ?? null);
|
|
254
|
+
// The mode can also change from inside the session (the agent leaving plan mode with
|
|
255
|
+
// present_plan): the status bar follows it.
|
|
256
|
+
unsubscribeMode?.();
|
|
257
|
+
unsubscribeMode = modeControl()?.subscribe?.((mode) => input.setMode(mode));
|
|
251
258
|
// Every registered command name and alias, built-ins included, for completion and for
|
|
252
259
|
// catching an unknown "/command" before it reaches the model as plain text (see chatTui.ts).
|
|
253
260
|
knownCommands = run.supportedCommands().then((commands) => new Set(commands.flatMap((c) => [c.name, ...(c.aliases ?? [])])), () => null);
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { AgentEvent, SessionUsage } from "../../core/runner.js";
|
|
2
2
|
import { type ConsoleRenderer } from "../consoleRenderer.js";
|
|
3
|
-
import { type ToolPhrase } from "./toolGroup.js";
|
|
3
|
+
import { type ResultFormatter, type ToolDetail, type ToolPhrase } from "./toolGroup.js";
|
|
4
|
+
import { type Todo } from "../../core/todos.js";
|
|
4
5
|
export interface HistoryItem {
|
|
5
6
|
id: number;
|
|
6
7
|
text: string;
|
|
7
8
|
/** What it is: the view draws the human's lines as a bar, and the spacing follows it. */
|
|
8
9
|
kind?: OutputKind;
|
|
9
|
-
/** A
|
|
10
|
+
/** A group of tool calls: the lines it unfolds into with Ctrl+O (`text` is the folded view). */
|
|
10
11
|
expanded?: string[];
|
|
11
12
|
}
|
|
12
13
|
/**
|
|
@@ -30,17 +31,23 @@ export interface SessionSnapshot {
|
|
|
30
31
|
activity: string | null;
|
|
31
32
|
/** Label of the latest action taken inside a subagent in this turn. */
|
|
32
33
|
subagentActivity: string | null;
|
|
33
|
-
/** Tool
|
|
34
|
+
/** Tool groups unfolded into every call with its result (Ctrl+O toggles it); folded, they show as `toolDetail` says. */
|
|
34
35
|
expanded: boolean;
|
|
35
36
|
turns: number;
|
|
36
37
|
usage: SessionUsage | null;
|
|
37
38
|
/** How full the context window is (0-100), after the latest turn; null until known. */
|
|
38
39
|
contextPercent: number | null;
|
|
40
|
+
/** The main agent's task list (TodoWrite), while any task is still to do; null otherwise. */
|
|
41
|
+
todos: Todo[] | null;
|
|
39
42
|
}
|
|
40
43
|
export interface SessionModelOptions {
|
|
41
44
|
formatAction?: (toolName: string, toolInput: unknown) => string;
|
|
42
45
|
/** How a tool counts in a folded group's summary ("read 2 files"); built-in tools have their own. */
|
|
43
46
|
toolPhrase?: (toolName: string) => ToolPhrase | undefined;
|
|
47
|
+
/** How much of the tool calls shows until Ctrl+O (see `ToolDetail`); "full" if not given. */
|
|
48
|
+
toolDetail?: ToolDetail;
|
|
49
|
+
/** The result line under a tool call (see `ResultFormatter`). */
|
|
50
|
+
formatResult?: ResultFormatter;
|
|
44
51
|
agentLabel?: string;
|
|
45
52
|
/** Called with everything the console renderer writes, e.g. to mirror a session log. */
|
|
46
53
|
onWrite?: (text: string) => void;
|
|
@@ -5,6 +5,7 @@ import { finishedLength, hasTable, renderMarkdown } from "./markdown.js";
|
|
|
5
5
|
import { toolGroupExpanded, toolGroupSummary } from "./toolGroup.js";
|
|
6
6
|
import * as ui from "../ui.js";
|
|
7
7
|
import { t } from "../../core/messages/index.js";
|
|
8
|
+
import { hasOpenTodos, parseTodos, TODO_TOOL } from "../../core/todos.js";
|
|
8
9
|
/**
|
|
9
10
|
* Columns the live area may use: short of the edge, where the Windows console wraps early
|
|
10
11
|
* (see lineBuffer.ts). Read on every use, so a resized terminal is picked up.
|
|
@@ -29,6 +30,7 @@ const isBlank = (line) => stripAnsi(line).trim() === "";
|
|
|
29
30
|
*/
|
|
30
31
|
export function createSessionModel(options = {}) {
|
|
31
32
|
const formatAction = options.formatAction ?? createFriendlyToolLabel();
|
|
33
|
+
const toolDetail = options.toolDetail ?? "full";
|
|
32
34
|
const width = () => Math.max(20, options.width?.() ?? 80);
|
|
33
35
|
const listeners = new Set();
|
|
34
36
|
let nextId = 0;
|
|
@@ -40,10 +42,11 @@ export function createSessionModel(options = {}) {
|
|
|
40
42
|
turnStartedAt: null,
|
|
41
43
|
activity: null,
|
|
42
44
|
subagentActivity: null,
|
|
43
|
-
expanded:
|
|
45
|
+
expanded: toolDetail === "full",
|
|
44
46
|
turns: 0,
|
|
45
47
|
usage: null,
|
|
46
48
|
contextPercent: null,
|
|
49
|
+
todos: null,
|
|
47
50
|
};
|
|
48
51
|
function update(changes) {
|
|
49
52
|
snapshot = { ...snapshot, ...changes };
|
|
@@ -146,7 +149,15 @@ export function createSessionModel(options = {}) {
|
|
|
146
149
|
return;
|
|
147
150
|
const calls = group;
|
|
148
151
|
group = null;
|
|
149
|
-
push([
|
|
152
|
+
push([folded(calls).join("\n")], "action", { expanded: unfolded(calls) });
|
|
153
|
+
}
|
|
154
|
+
/** A group with every call and its result: "full", or any level unfolded with Ctrl+O. */
|
|
155
|
+
function unfolded(calls) {
|
|
156
|
+
return toolGroupExpanded(calls, width(), { formatResult: options.formatResult });
|
|
157
|
+
}
|
|
158
|
+
/** A group as its level shows it folded: the calls without results, or one summary line. */
|
|
159
|
+
function folded(calls) {
|
|
160
|
+
return toolDetail === "calls" ? toolGroupExpanded(calls, width(), { results: false }) : [groupLine(calls)];
|
|
150
161
|
}
|
|
151
162
|
function groupLine(calls) {
|
|
152
163
|
return INDENT + ui.dim(fitWidth(toolGroupSummary(calls, options.toolPhrase), width() - INDENT.length));
|
|
@@ -161,7 +172,7 @@ export function createSessionModel(options = {}) {
|
|
|
161
172
|
let rows = liveRows;
|
|
162
173
|
let kind = liveRows.length > 0 ? "agent" : null;
|
|
163
174
|
if (group) {
|
|
164
|
-
rows = snapshot.expanded ?
|
|
175
|
+
rows = snapshot.expanded ? unfolded(group) : folded(group);
|
|
165
176
|
kind = "action";
|
|
166
177
|
}
|
|
167
178
|
const live = rows.join("\n");
|
|
@@ -181,6 +192,15 @@ export function createSessionModel(options = {}) {
|
|
|
181
192
|
return;
|
|
182
193
|
}
|
|
183
194
|
case "action": {
|
|
195
|
+
// The task list isn't a tool call to show: it's drawn above the prompt, and the task
|
|
196
|
+
// in progress becomes the spinner's label.
|
|
197
|
+
const todos = event.toolName === TODO_TOOL ? parseTodos(event.input) : null;
|
|
198
|
+
if (todos) {
|
|
199
|
+
const current = todos.find((todo) => todo.status === "in_progress");
|
|
200
|
+
snapshot = { ...snapshot, todos: hasOpenTodos(todos) ? todos : null, activity: current?.activeForm ?? snapshot.activity };
|
|
201
|
+
refreshLive();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
184
204
|
flushSegment(true);
|
|
185
205
|
group ??= [];
|
|
186
206
|
group.push({ id: event.toolUseId, label: formatAction(event.toolName, event.input), toolName: event.toolName, result: null });
|
|
@@ -189,6 +209,8 @@ export function createSessionModel(options = {}) {
|
|
|
189
209
|
return;
|
|
190
210
|
}
|
|
191
211
|
case "tool-result": {
|
|
212
|
+
if (event.toolName === TODO_TOOL)
|
|
213
|
+
return;
|
|
192
214
|
const call = group?.find((c) => c.id === event.toolUseId);
|
|
193
215
|
if (call)
|
|
194
216
|
call.result = { isError: event.isError, text: event.text };
|
|
@@ -273,7 +295,7 @@ export function createSessionModel(options = {}) {
|
|
|
273
295
|
lastKind = null;
|
|
274
296
|
gapPending = false;
|
|
275
297
|
reply = "";
|
|
276
|
-
snapshot = { ...snapshot, items: [], live: "", liveGap: false, turns: 0, usage: null, contextPercent: null };
|
|
298
|
+
snapshot = { ...snapshot, items: [], live: "", liveGap: false, turns: 0, usage: null, contextPercent: null, todos: null };
|
|
277
299
|
refreshLive();
|
|
278
300
|
},
|
|
279
301
|
replay(messages, userLine) {
|
|
@@ -13,6 +13,27 @@ export interface ToolCall {
|
|
|
13
13
|
}
|
|
14
14
|
/** How a tool counts in a group's summary: the phrase for one call and for several, "{n}" standing for the count. */
|
|
15
15
|
export type { ToolPhrase };
|
|
16
|
+
/**
|
|
17
|
+
* How much of its tool calls a chat shows: `"full"`, every call with its result line;
|
|
18
|
+
* `"calls"`, the calls without their results (a failed call says so in one short line);
|
|
19
|
+
* `"summary"`, one line per group. Ctrl+O unfolds any of them into the full view.
|
|
20
|
+
*/
|
|
21
|
+
export type ToolDetail = "full" | "calls" | "summary";
|
|
22
|
+
/**
|
|
23
|
+
* The line shown under a tool call for its result: a string replaces the kit's (plain text,
|
|
24
|
+
* drawn in the result's color, or the error color if it failed), `null` hides it, and
|
|
25
|
+
* `undefined` keeps the kit's (the result's first line).
|
|
26
|
+
*/
|
|
27
|
+
export type ResultFormatter = (toolName: string, result: {
|
|
28
|
+
isError: boolean;
|
|
29
|
+
text: string;
|
|
30
|
+
}) => string | null | undefined;
|
|
31
|
+
/** How `toolGroupExpanded()` draws the results. */
|
|
32
|
+
export interface ResultOptions {
|
|
33
|
+
/** Show the result lines (the default); without them a failed call shows a short failure line. */
|
|
34
|
+
results?: boolean;
|
|
35
|
+
formatResult?: ResultFormatter;
|
|
36
|
+
}
|
|
16
37
|
/**
|
|
17
38
|
* A folded group's one line, as in the Claude Code CLI: "Read 2 files, ran 1 shell
|
|
18
39
|
* command". Calls to the same kind of tool are counted together, in the order they first
|
|
@@ -32,6 +53,6 @@ export declare function resultSummary(result: {
|
|
|
32
53
|
/**
|
|
33
54
|
* The calls one by one: each behind a `●`, then after `⎿` the calls a subagent made (the
|
|
34
55
|
* latest few, dim) and the result in one line (a subagent's answer with its markdown
|
|
35
|
-
* rendered), or "…" while it runs.
|
|
56
|
+
* rendered), or "…" while it runs. Without `options.results`, only a failure's short line.
|
|
36
57
|
*/
|
|
37
|
-
export declare function toolGroupExpanded(calls: readonly ToolCall[], width: number): string[];
|
|
58
|
+
export declare function toolGroupExpanded(calls: readonly ToolCall[], width: number, options?: ResultOptions): string[];
|
|
@@ -43,20 +43,33 @@ export function resultSummary(result, markdown = false) {
|
|
|
43
43
|
// would cancel a dim around it.
|
|
44
44
|
return ui.toolResult(markdown ? stripAnsi(renderMarkdown(first, 1000)[0] ?? first) : first) + more;
|
|
45
45
|
}
|
|
46
|
+
/** The line under a call for its result, or null for none. */
|
|
47
|
+
function resultLine(call, { results = true, formatResult }) {
|
|
48
|
+
if (!call.result)
|
|
49
|
+
return results ? ui.dim("…") : null;
|
|
50
|
+
if (!results)
|
|
51
|
+
return call.result.isError ? ui.error(t().toolFailed) : null;
|
|
52
|
+
const custom = formatResult?.(call.toolName, call.result);
|
|
53
|
+
if (custom === null)
|
|
54
|
+
return null;
|
|
55
|
+
if (custom !== undefined)
|
|
56
|
+
return call.result.isError ? ui.error(custom) : ui.toolResult(custom);
|
|
57
|
+
return resultSummary(call.result, SUBAGENT_TOOLS.has(call.toolName));
|
|
58
|
+
}
|
|
46
59
|
/**
|
|
47
60
|
* The calls one by one: each behind a `●`, then after `⎿` the calls a subagent made (the
|
|
48
61
|
* latest few, dim) and the result in one line (a subagent's answer with its markdown
|
|
49
|
-
* rendered), or "…" while it runs.
|
|
62
|
+
* rendered), or "…" while it runs. Without `options.results`, only a failure's short line.
|
|
50
63
|
*/
|
|
51
|
-
export function toolGroupExpanded(calls, width) {
|
|
64
|
+
export function toolGroupExpanded(calls, width, options = {}) {
|
|
52
65
|
return calls.flatMap((call) => {
|
|
53
66
|
const children = call.children ?? [];
|
|
54
67
|
const shown = children.slice(-MAX_CHILDREN);
|
|
55
|
-
const result =
|
|
68
|
+
const result = resultLine(call, options);
|
|
56
69
|
const under = [
|
|
57
70
|
...(children.length > shown.length ? [ui.dim(t().earlierCalls(children.length - shown.length))] : []),
|
|
58
71
|
...shown.map((label) => fitWidth(ui.dim(`· ${label}`), width - 5)),
|
|
59
|
-
result,
|
|
72
|
+
...(result === null ? [] : [fitWidth(result, width - 5)]),
|
|
60
73
|
];
|
|
61
74
|
return [
|
|
62
75
|
`${ui.toolBullet("●")} ${fitWidth(ui.toolLabel(call.label), width - 2)}`,
|
|
@@ -80,6 +80,8 @@ async function askOnTerminal(prompt, signal) {
|
|
|
80
80
|
export const terminalInteractionPort = {
|
|
81
81
|
askDecision: askOnTerminal,
|
|
82
82
|
askManualIntervention: askOnTerminal,
|
|
83
|
+
askText: askOnTerminal,
|
|
84
|
+
askChoice: (prompt, choice, signal) => askOnTerminal({ ...prompt, lines: [...prompt.lines, ...choice.options.map((option, i) => `${i + 1}. ${option}`)], question: t().choiceQuestion(choice.multiple) }, signal),
|
|
83
85
|
notify(message) {
|
|
84
86
|
if (stdin.isTTY)
|
|
85
87
|
console.log(message);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@falkenslab/agent-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Library for building an agent on top of the Claude Agent SDK",
|
|
5
5
|
"author": "Francisco Vargas Ruiz",
|
|
6
6
|
"license": "MIT",
|
|
@@ -62,11 +62,17 @@
|
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@eslint/js": "^10.0.1",
|
|
65
|
+
"@mozilla/readability": "^0.6.0",
|
|
65
66
|
"@types/node": "^24.0.0",
|
|
67
|
+
"@types/turndown": "^5.0.6",
|
|
66
68
|
"eslint": "^10.9.1",
|
|
69
|
+
"fflate": "^0.8.3",
|
|
67
70
|
"globals": "^17.11.0",
|
|
68
71
|
"ink-testing-library": "^4.0.0",
|
|
72
|
+
"linkedom": "^0.18.13",
|
|
73
|
+
"mammoth": "^1.13.0",
|
|
69
74
|
"tsx": "^4.19.0",
|
|
75
|
+
"turndown": "^7.2.4",
|
|
70
76
|
"typescript": "^5.7.0",
|
|
71
77
|
"typescript-eslint": "^8.68.0"
|
|
72
78
|
},
|
|
@@ -75,5 +81,29 @@
|
|
|
75
81
|
},
|
|
76
82
|
"publishConfig": {
|
|
77
83
|
"access": "public"
|
|
84
|
+
},
|
|
85
|
+
"peerDependencies": {
|
|
86
|
+
"@mozilla/readability": ">=0.5.0",
|
|
87
|
+
"linkedom": ">=0.16.0",
|
|
88
|
+
"turndown": ">=7.0.0",
|
|
89
|
+
"mammoth": ">=1.6.0",
|
|
90
|
+
"fflate": ">=0.8.0"
|
|
91
|
+
},
|
|
92
|
+
"peerDependenciesMeta": {
|
|
93
|
+
"@mozilla/readability": {
|
|
94
|
+
"optional": true
|
|
95
|
+
},
|
|
96
|
+
"linkedom": {
|
|
97
|
+
"optional": true
|
|
98
|
+
},
|
|
99
|
+
"turndown": {
|
|
100
|
+
"optional": true
|
|
101
|
+
},
|
|
102
|
+
"mammoth": {
|
|
103
|
+
"optional": true
|
|
104
|
+
},
|
|
105
|
+
"fflate": {
|
|
106
|
+
"optional": true
|
|
107
|
+
}
|
|
78
108
|
}
|
|
79
109
|
}
|
|
File without changes
|