@falkenslab/agent-kit 0.14.0 → 0.15.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/dist/core/messages/de.js +1 -0
- package/dist/core/messages/en.d.ts +2 -0
- package/dist/core/messages/en.js +1 -0
- package/dist/core/messages/es.js +1 -0
- package/dist/core/messages/fr.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/tui/ink/SessionView.js +8 -3
- package/dist/tui/ink/progressView.d.ts +5 -1
- package/dist/tui/ink/runChatInk.d.ts +16 -1
- package/dist/tui/ink/runChatInk.js +2 -0
- package/dist/tui/ink/sessionModel.d.ts +7 -3
- package/dist/tui/ink/sessionModel.js +12 -3
- package/dist/tui/ink/toolGroup.d.ts +23 -2
- package/dist/tui/ink/toolGroup.js +17 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,110 +1,110 @@
|
|
|
1
|
-
<h1>
|
|
2
|
-
<picture>
|
|
3
|
-
<source media="(prefers-color-scheme: dark)" srcset="docs/assets/logo-readme-dark.svg">
|
|
4
|
-
<img src="docs/assets/logo-readme.svg" alt="agent-kit" height="64">
|
|
5
|
-
</picture>
|
|
6
|
-
</h1>
|
|
7
|
-
|
|
8
|
-
[](https://www.npmjs.com/package/@falkenslab/agent-kit)
|
|
9
|
-
[](https://www.npmjs.com/package/@falkenslab/agent-kit)
|
|
10
|
-
[](https://www.npmjs.com/package/@falkenslab/agent-kit)
|
|
11
|
-
[](https://nodejs.org/)
|
|
12
|
-
[](LICENSE)
|
|
13
|
-
[](https://falkenslab.github.io/agent-kit/)
|
|
14
|
-
|
|
15
|
-
A foundation for building AI agents on top of the [Claude Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk). It isn't an agent itself: it solves once what every agent needs (human oversight, safety, memory and a good terminal interface), so each concrete agent only has to write what belongs to its own domain: what it knows how to do, with which tools, and how it talks.
|
|
16
|
-
|
|
17
|
-
## What an agent built with it can do
|
|
18
|
-
|
|
19
|
-
- **Work at different levels of oversight.** Autonomous, guided (it asks for permission before anything that can't be undone or that other people will see) or step by step (it asks before every action), and a plan mode where it only reads and plans until you let it act. Within the same conversation you can switch among guided, step by step and plan.
|
|
20
|
-
- **Ask a person for help.** It asks for approvals, or stops so someone can do something by hand, such as signing in to a website. It can be answered from the keyboard or from another program driving it, for example a desktop app.
|
|
21
|
-
- **Remember between sessions.** It keeps its own knowledge base, a small wiki of notes that grows with every session, and keeps the original documents you give it or that it downloads untouched. Each conversation stays in the project's folder, so you can pick it up again with `--continue` (the latest one) or `/resume` (any of them).
|
|
22
|
-
- **Speak your language.** Its interface comes in English, Spanish, French or German, the system's by default or the one you ask for with `--language=fr`, and it answers in that language unless you write to it in another one.
|
|
23
|
-
- **Delegate to subagents safely.** Only to the ones it declares, always waiting for their result, and without the main agent being able to slip past its own limits through them.
|
|
24
|
-
- **Stay within limits.** It only reads and writes in the folders that belong to it, and never in protected files, such as one holding passwords.
|
|
25
|
-
- **Use external tools.** MCP servers of its own or of the project, and skills and commands organized in plugins.
|
|
26
|
-
- **Leave a trail.** It records every action and the whole conversation, with secrets hidden.
|
|
27
|
-
- **Look the way you want.** The terminal's colors come from a theme by roles (the agent's replies, tool results, the focused option, borders…): pass only the ones you want to change, as a color name, a hex or a function.
|
|
28
|
-
- **Chat in a terminal the way Claude Code does.** Full screen, with the reply written live and its markdown already formatted, the tools it uses
|
|
29
|
-
- **Also work without a terminal.** The same foundation runs inside a desktop app or a server, which bring their own interface.
|
|
30
|
-
|
|
31
|
-
## Examples
|
|
32
|
-
|
|
33
|
-
### Captain Whiskers
|
|
34
|
-
|
|
35
|
-
[Captain Whiskers](./examples/captain-whiskers) is a toy agent to see the kit in action: a retired pirate cat who tells jokes in the terminal, in your language. It starts full screen with its logo and has a small crew of subagents: one looks for new jokes on the web, another rates them before they're told, and a third tells the time with a tool of the captain's own. It lets you try the different levels of oversight and is the best starting point for an agent of your own. Its README explains how to run it.
|
|
36
|
-
|
|
37
|
-

|
|
38
|
-
|
|
39
|
-
### teacher-agent
|
|
40
|
-
|
|
41
|
-
[teacher-agent](https://github.com/falkenslab/teacher-agent) is a real assistant for Moodle teachers. It signs in with your account in a Chrome window and works as you would: it grades submissions, answers in the forum, creates or reviews content and sums up how the class is going. It asks for your permission before publishing anything students will see, and takes notes about the course to remember everything in the next session.
|
|
42
|
-
|
|
43
|
-
Oversight, memory, safety and the chat come from the kit; teacher-agent brings what it knows about Moodle and driving the browser.
|
|
44
|
-
|
|
45
|
-
## Installation
|
|
46
|
-
|
|
47
|
-
```
|
|
48
|
-
npm install @falkenslab/agent-kit
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
To try changes to the kit without publishing them, point to a local clone with `"file:../agent-kit"` in your `package.json` and build it with `npm run build` after each change.
|
|
52
|
-
|
|
53
|
-
Requires Node.js 20 or later.
|
|
54
|
-
|
|
55
|
-
## A minimal agent
|
|
56
|
-
|
|
57
|
-
An agent only describes what belongs to it (who it is, which MCP servers, skills and subagents it has) and picks a level of oversight; the kit sets up the session and the chat:
|
|
58
|
-
|
|
59
|
-
```ts
|
|
60
|
-
import path from "node:path";
|
|
61
|
-
import { buildSessionOptions, ensureClaudeAuth, runChatInk, type AgentSpec, type BaseSessionConfig } from "@falkenslab/agent-kit";
|
|
62
|
-
|
|
63
|
-
// The only part specific to the agent: who it is and what it has.
|
|
64
|
-
const spec: AgentSpec<BaseSessionConfig> = {
|
|
65
|
-
buildSystemPrompt: () => "You are a pirate cat who tells jokes. Always answer in character.",
|
|
66
|
-
buildMcpServers: () => ({}), // its own MCP servers, if any
|
|
67
|
-
pluginRoots: () => [], // folders with skills and commands
|
|
68
|
-
buildSubagents: () => undefined, // subagents, if it delegates to any
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
await ensureClaudeAuth(); // offers to create a Claude token if it finds none
|
|
72
|
-
|
|
73
|
-
const config: BaseSessionConfig = { mode: "guided", projectDir: process.cwd() };
|
|
74
|
-
|
|
75
|
-
// Each run gets a folder of its own under .run/ (keep it out of git): its log, its
|
|
76
|
-
// transcript and the conversation, to pick it up again with --continue or /resume.
|
|
77
|
-
await runChatInk((run) => buildSessionOptions(config, run.dir, spec, { run }), {
|
|
78
|
-
runsDir: path.resolve(".run"),
|
|
79
|
-
header: { title: "My agent" },
|
|
80
|
-
mode: config.mode, // Shift+Tab cycles through guided, step by step and plan
|
|
81
|
-
fullscreen: true,
|
|
82
|
-
theme: { toolResult: "gray", selection: "#ffb86c" }, // only the colors you want to change
|
|
83
|
-
});
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
Save it as `agent.ts` in a project with the kit and `tsx` installed and `"type": "module"` in its `package.json`, and run it with `npx tsx agent.ts`. Captain Whiskers is this same skeleton with more pieces: skills, subagents and its logo.
|
|
87
|
-
|
|
88
|
-
## Documentation
|
|
89
|
-
|
|
90
|
-
The full documentation is at **[falkenslab.github.io/agent-kit](https://falkenslab.github.io/agent-kit/)**: a tutorial, guides for every part of the kit (modes, human in the loop, security, tools, subagents, the knowledge base, the terminal UI, runs, languages, themes, hosts without a terminal), recipes, and an API reference generated from the source, for each released version.
|
|
91
|
-
|
|
92
|
-
For whoever works on the kit itself, the specification in [`.minispec/`](./.minispec/README.md) explains how each part is built and why, including the SDK behaviors checked by hand that motivate several of its safeguards.
|
|
93
|
-
|
|
94
|
-
## Development
|
|
95
|
-
|
|
96
|
-
| Command | What it does |
|
|
97
|
-
| --- | --- |
|
|
98
|
-
| `npm run build` | Builds the kit into `dist/` |
|
|
99
|
-
| `npm run typecheck` | Checks the types without building |
|
|
100
|
-
| `npm run lint` | Checks the code style |
|
|
101
|
-
| `npm test` | Runs all the tests |
|
|
102
|
-
| `npm run docs:install` | Installs the documentation site's dependencies (once) |
|
|
103
|
-
| `npm run docs:start` | Serves the documentation site locally, reloading on changes |
|
|
104
|
-
| `npm run docs:build` | Builds the documentation site (fails on broken links) |
|
|
105
|
-
|
|
106
|
-
To run a single test file:
|
|
107
|
-
|
|
108
|
-
```
|
|
109
|
-
npx tsx --test test/tui/promptErrors.test.ts
|
|
110
|
-
```
|
|
1
|
+
<h1>
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/assets/logo-readme-dark.svg">
|
|
4
|
+
<img src="docs/assets/logo-readme.svg" alt="agent-kit" height="64">
|
|
5
|
+
</picture>
|
|
6
|
+
</h1>
|
|
7
|
+
|
|
8
|
+
[](https://www.npmjs.com/package/@falkenslab/agent-kit)
|
|
9
|
+
[](https://www.npmjs.com/package/@falkenslab/agent-kit)
|
|
10
|
+
[](https://www.npmjs.com/package/@falkenslab/agent-kit)
|
|
11
|
+
[](https://nodejs.org/)
|
|
12
|
+
[](LICENSE)
|
|
13
|
+
[](https://falkenslab.github.io/agent-kit/)
|
|
14
|
+
|
|
15
|
+
A foundation for building AI agents on top of the [Claude Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk). It isn't an agent itself: it solves once what every agent needs (human oversight, safety, memory and a good terminal interface), so each concrete agent only has to write what belongs to its own domain: what it knows how to do, with which tools, and how it talks.
|
|
16
|
+
|
|
17
|
+
## What an agent built with it can do
|
|
18
|
+
|
|
19
|
+
- **Work at different levels of oversight.** Autonomous, guided (it asks for permission before anything that can't be undone or that other people will see) or step by step (it asks before every action), and a plan mode where it only reads and plans until you let it act. Within the same conversation you can switch among guided, step by step and plan.
|
|
20
|
+
- **Ask a person for help.** It asks for approvals, or stops so someone can do something by hand, such as signing in to a website. It can be answered from the keyboard or from another program driving it, for example a desktop app.
|
|
21
|
+
- **Remember between sessions.** It keeps its own knowledge base, a small wiki of notes that grows with every session, and keeps the original documents you give it or that it downloads untouched. Each conversation stays in the project's folder, so you can pick it up again with `--continue` (the latest one) or `/resume` (any of them).
|
|
22
|
+
- **Speak your language.** Its interface comes in English, Spanish, French or German, the system's by default or the one you ask for with `--language=fr`, and it answers in that language unless you write to it in another one.
|
|
23
|
+
- **Delegate to subagents safely.** Only to the ones it declares, always waiting for their result, and without the main agent being able to slip past its own limits through them.
|
|
24
|
+
- **Stay within limits.** It only reads and writes in the folders that belong to it, and never in protected files, such as one holding passwords.
|
|
25
|
+
- **Use external tools.** MCP servers of its own or of the project, and skills and commands organized in plugins.
|
|
26
|
+
- **Leave a trail.** It records every action and the whole conversation, with secrets hidden.
|
|
27
|
+
- **Look the way you want.** The terminal's colors come from a theme by roles (the agent's replies, tool results, the focused option, borders…): pass only the ones you want to change, as a color name, a hex or a function.
|
|
28
|
+
- **Chat in a terminal the way Claude Code does.** Full screen, with the reply written live and its markdown already formatted, the tools it uses shown in as much detail as the agent chooses (each call with its result, the calls alone, or one line per group; Ctrl+O unfolds them), panels for approvals, history and search, a suggestion for the next message, pasted blocks, file mentions with `@`, mouse selection (right-click to copy), and the agent's status in the tab and in the Windows taskbar.
|
|
29
|
+
- **Also work without a terminal.** The same foundation runs inside a desktop app or a server, which bring their own interface.
|
|
30
|
+
|
|
31
|
+
## Examples
|
|
32
|
+
|
|
33
|
+
### Captain Whiskers
|
|
34
|
+
|
|
35
|
+
[Captain Whiskers](./examples/captain-whiskers) is a toy agent to see the kit in action: a retired pirate cat who tells jokes in the terminal, in your language. It starts full screen with its logo and has a small crew of subagents: one looks for new jokes on the web, another rates them before they're told, and a third tells the time with a tool of the captain's own. It lets you try the different levels of oversight and is the best starting point for an agent of your own. Its README explains how to run it.
|
|
36
|
+
|
|
37
|
+

|
|
38
|
+
|
|
39
|
+
### teacher-agent
|
|
40
|
+
|
|
41
|
+
[teacher-agent](https://github.com/falkenslab/teacher-agent) is a real assistant for Moodle teachers. It signs in with your account in a Chrome window and works as you would: it grades submissions, answers in the forum, creates or reviews content and sums up how the class is going. It asks for your permission before publishing anything students will see, and takes notes about the course to remember everything in the next session.
|
|
42
|
+
|
|
43
|
+
Oversight, memory, safety and the chat come from the kit; teacher-agent brings what it knows about Moodle and driving the browser.
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
npm install @falkenslab/agent-kit
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
To try changes to the kit without publishing them, point to a local clone with `"file:../agent-kit"` in your `package.json` and build it with `npm run build` after each change.
|
|
52
|
+
|
|
53
|
+
Requires Node.js 20 or later.
|
|
54
|
+
|
|
55
|
+
## A minimal agent
|
|
56
|
+
|
|
57
|
+
An agent only describes what belongs to it (who it is, which MCP servers, skills and subagents it has) and picks a level of oversight; the kit sets up the session and the chat:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import path from "node:path";
|
|
61
|
+
import { buildSessionOptions, ensureClaudeAuth, runChatInk, type AgentSpec, type BaseSessionConfig } from "@falkenslab/agent-kit";
|
|
62
|
+
|
|
63
|
+
// The only part specific to the agent: who it is and what it has.
|
|
64
|
+
const spec: AgentSpec<BaseSessionConfig> = {
|
|
65
|
+
buildSystemPrompt: () => "You are a pirate cat who tells jokes. Always answer in character.",
|
|
66
|
+
buildMcpServers: () => ({}), // its own MCP servers, if any
|
|
67
|
+
pluginRoots: () => [], // folders with skills and commands
|
|
68
|
+
buildSubagents: () => undefined, // subagents, if it delegates to any
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
await ensureClaudeAuth(); // offers to create a Claude token if it finds none
|
|
72
|
+
|
|
73
|
+
const config: BaseSessionConfig = { mode: "guided", projectDir: process.cwd() };
|
|
74
|
+
|
|
75
|
+
// Each run gets a folder of its own under .run/ (keep it out of git): its log, its
|
|
76
|
+
// transcript and the conversation, to pick it up again with --continue or /resume.
|
|
77
|
+
await runChatInk((run) => buildSessionOptions(config, run.dir, spec, { run }), {
|
|
78
|
+
runsDir: path.resolve(".run"),
|
|
79
|
+
header: { title: "My agent" },
|
|
80
|
+
mode: config.mode, // Shift+Tab cycles through guided, step by step and plan
|
|
81
|
+
fullscreen: true,
|
|
82
|
+
theme: { toolResult: "gray", selection: "#ffb86c" }, // only the colors you want to change
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Save it as `agent.ts` in a project with the kit and `tsx` installed and `"type": "module"` in its `package.json`, and run it with `npx tsx agent.ts`. Captain Whiskers is this same skeleton with more pieces: skills, subagents and its logo.
|
|
87
|
+
|
|
88
|
+
## Documentation
|
|
89
|
+
|
|
90
|
+
The full documentation is at **[falkenslab.github.io/agent-kit](https://falkenslab.github.io/agent-kit/)**: a tutorial, guides for every part of the kit (modes, human in the loop, security, tools, subagents, the knowledge base, the terminal UI, runs, languages, themes, hosts without a terminal), recipes, and an API reference generated from the source, for each released version.
|
|
91
|
+
|
|
92
|
+
For whoever works on the kit itself, the specification in [`.minispec/`](./.minispec/README.md) explains how each part is built and why, including the SDK behaviors checked by hand that motivate several of its safeguards.
|
|
93
|
+
|
|
94
|
+
## Development
|
|
95
|
+
|
|
96
|
+
| Command | What it does |
|
|
97
|
+
| --- | --- |
|
|
98
|
+
| `npm run build` | Builds the kit into `dist/` |
|
|
99
|
+
| `npm run typecheck` | Checks the types without building |
|
|
100
|
+
| `npm run lint` | Checks the code style |
|
|
101
|
+
| `npm test` | Runs all the tests |
|
|
102
|
+
| `npm run docs:install` | Installs the documentation site's dependencies (once) |
|
|
103
|
+
| `npm run docs:start` | Serves the documentation site locally, reloading on changes |
|
|
104
|
+
| `npm run docs:build` | Builds the documentation site (fails on broken links) |
|
|
105
|
+
|
|
106
|
+
To run a single test file:
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
npx tsx --test test/tui/promptErrors.test.ts
|
|
110
|
+
```
|
package/dist/core/messages/de.js
CHANGED
|
@@ -70,6 +70,7 @@ export const de = {
|
|
|
70
70
|
otherTools: ["hat {n} Werkzeug verwendet", "hat {n} Werkzeuge verwendet"],
|
|
71
71
|
noOutput: "(keine Ausgabe)",
|
|
72
72
|
error: "Fehler",
|
|
73
|
+
toolFailed: "Konnte nicht abgeschlossen werden",
|
|
73
74
|
moreResultLines: (count) => `(+${count} ${plural(count, "Zeile", "Zeilen")})`,
|
|
74
75
|
earlierCalls: (count) => `… ${count} frühere`,
|
|
75
76
|
labels: {
|
|
@@ -55,6 +55,8 @@ export interface Messages {
|
|
|
55
55
|
otherTools: ToolPhrase;
|
|
56
56
|
noOutput: string;
|
|
57
57
|
error: string;
|
|
58
|
+
/** A failed tool call when results are hidden (`toolDetail: "calls"`): no tool output, just that it failed. */
|
|
59
|
+
toolFailed: string;
|
|
58
60
|
moreResultLines(count: number): string;
|
|
59
61
|
earlierCalls(count: number): string;
|
|
60
62
|
labels: {
|
package/dist/core/messages/en.js
CHANGED
|
@@ -69,6 +69,7 @@ export const en = {
|
|
|
69
69
|
otherTools: ["used {n} tool", "used {n} tools"],
|
|
70
70
|
noOutput: "(no output)",
|
|
71
71
|
error: "error",
|
|
72
|
+
toolFailed: "Couldn't complete it",
|
|
72
73
|
moreResultLines: (count) => `(+${count} ${plural(count, "line", "lines")})`,
|
|
73
74
|
earlierCalls: (count) => `… ${count} earlier`,
|
|
74
75
|
labels: {
|
package/dist/core/messages/es.js
CHANGED
|
@@ -70,6 +70,7 @@ export const es = {
|
|
|
70
70
|
otherTools: ["usó {n} herramienta", "usó {n} herramientas"],
|
|
71
71
|
noOutput: "(sin salida)",
|
|
72
72
|
error: "error",
|
|
73
|
+
toolFailed: "No se pudo completar",
|
|
73
74
|
moreResultLines: (count) => `(+${count} ${plural(count, "línea", "líneas")})`,
|
|
74
75
|
earlierCalls: (count) => `… ${count} ${plural(count, "anterior", "anteriores")}`,
|
|
75
76
|
labels: {
|
package/dist/core/messages/fr.js
CHANGED
|
@@ -70,6 +70,7 @@ export const fr = {
|
|
|
70
70
|
otherTools: ["a utilisé {n} outil", "a utilisé {n} outils"],
|
|
71
71
|
noOutput: "(aucune sortie)",
|
|
72
72
|
error: "erreur",
|
|
73
|
+
toolFailed: "Impossible de terminer",
|
|
73
74
|
moreResultLines: (count) => `(+${count} ${plural(count, "ligne", "lignes")})`,
|
|
74
75
|
earlierCalls: (count) => `… ${count} ${plural(count, "précédent", "précédents")}`,
|
|
75
76
|
labels: {
|
package/dist/index.d.ts
CHANGED
|
@@ -32,7 +32,7 @@ export { runChatTui, type ChatTuiOptions } from "./tui/chatTui.js";
|
|
|
32
32
|
export type { SessionOpener } from "./tui/runs.js";
|
|
33
33
|
export { createConsoleRenderer, type ConsoleRenderer, type ConsoleRendererOptions } from "./tui/consoleRenderer.js";
|
|
34
34
|
export { runChatInk, type InkChatOptions, type HeaderInfo } from "./tui/ink/runChatInk.js";
|
|
35
|
-
export type { ToolPhrase } from "./tui/ink/toolGroup.js";
|
|
35
|
+
export type { ToolPhrase, ToolDetail, ResultFormatter } from "./tui/ink/toolGroup.js";
|
|
36
36
|
export type { RenderApproval } from "./tui/ink/SessionView.js";
|
|
37
37
|
export { createProgressView, type ProgressView, type ProgressViewOptions } from "./tui/ink/progressView.js";
|
|
38
38
|
export { runWizard, type WizardStep, type SelectStep, type InputStep, type PasswordStep, type ConfirmStep, type WizardAnswers, type WizardOptions, } from "./tui/ink/wizard.js";
|
|
@@ -161,12 +161,17 @@ function FullscreenSession({ session, checkpoint, renderApproval, mode, header,
|
|
|
161
161
|
if (!historyRef.current || end === start)
|
|
162
162
|
return null;
|
|
163
163
|
const box = framePosition(historyRef.current);
|
|
164
|
-
|
|
164
|
+
// Measured now, not `historyHeight`: that state is set by an effect after the layout, so
|
|
165
|
+
// a click on the first frames would find the rows where they were before it.
|
|
166
|
+
const height = measureElement(historyRef.current).height;
|
|
167
|
+
const firstY = box.y + height - (end - start); // the rows are bottom-aligned
|
|
165
168
|
let offset = y - firstY;
|
|
166
|
-
|
|
169
|
+
// Rows that don't fit are clipped above the box, not clickable.
|
|
170
|
+
const firstShown = Math.max(0, box.y - firstY);
|
|
171
|
+
if (offset < firstShown || offset >= end - start) {
|
|
167
172
|
if (!clamp)
|
|
168
173
|
return null;
|
|
169
|
-
offset = Math.min(end - start - 1, Math.max(
|
|
174
|
+
offset = Math.min(end - start - 1, Math.max(firstShown, offset));
|
|
170
175
|
}
|
|
171
176
|
return { row: start + offset, col: Math.max(0, x - box.x) };
|
|
172
177
|
}
|
|
@@ -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),
|
|
@@ -1,12 +1,12 @@
|
|
|
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
4
|
export interface HistoryItem {
|
|
5
5
|
id: number;
|
|
6
6
|
text: string;
|
|
7
7
|
/** What it is: the view draws the human's lines as a bar, and the spacing follows it. */
|
|
8
8
|
kind?: OutputKind;
|
|
9
|
-
/** A
|
|
9
|
+
/** A group of tool calls: the lines it unfolds into with Ctrl+O (`text` is the folded view). */
|
|
10
10
|
expanded?: string[];
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
@@ -30,7 +30,7 @@ export interface SessionSnapshot {
|
|
|
30
30
|
activity: string | null;
|
|
31
31
|
/** Label of the latest action taken inside a subagent in this turn. */
|
|
32
32
|
subagentActivity: string | null;
|
|
33
|
-
/** Tool
|
|
33
|
+
/** Tool groups unfolded into every call with its result (Ctrl+O toggles it); folded, they show as `toolDetail` says. */
|
|
34
34
|
expanded: boolean;
|
|
35
35
|
turns: number;
|
|
36
36
|
usage: SessionUsage | null;
|
|
@@ -41,6 +41,10 @@ export interface SessionModelOptions {
|
|
|
41
41
|
formatAction?: (toolName: string, toolInput: unknown) => string;
|
|
42
42
|
/** How a tool counts in a folded group's summary ("read 2 files"); built-in tools have their own. */
|
|
43
43
|
toolPhrase?: (toolName: string) => ToolPhrase | undefined;
|
|
44
|
+
/** How much of the tool calls shows until Ctrl+O (see `ToolDetail`); "full" if not given. */
|
|
45
|
+
toolDetail?: ToolDetail;
|
|
46
|
+
/** The result line under a tool call (see `ResultFormatter`). */
|
|
47
|
+
formatResult?: ResultFormatter;
|
|
44
48
|
agentLabel?: string;
|
|
45
49
|
/** Called with everything the console renderer writes, e.g. to mirror a session log. */
|
|
46
50
|
onWrite?: (text: string) => void;
|
|
@@ -29,6 +29,7 @@ const isBlank = (line) => stripAnsi(line).trim() === "";
|
|
|
29
29
|
*/
|
|
30
30
|
export function createSessionModel(options = {}) {
|
|
31
31
|
const formatAction = options.formatAction ?? createFriendlyToolLabel();
|
|
32
|
+
const toolDetail = options.toolDetail ?? "full";
|
|
32
33
|
const width = () => Math.max(20, options.width?.() ?? 80);
|
|
33
34
|
const listeners = new Set();
|
|
34
35
|
let nextId = 0;
|
|
@@ -40,7 +41,7 @@ export function createSessionModel(options = {}) {
|
|
|
40
41
|
turnStartedAt: null,
|
|
41
42
|
activity: null,
|
|
42
43
|
subagentActivity: null,
|
|
43
|
-
expanded:
|
|
44
|
+
expanded: toolDetail === "full",
|
|
44
45
|
turns: 0,
|
|
45
46
|
usage: null,
|
|
46
47
|
contextPercent: null,
|
|
@@ -146,7 +147,15 @@ export function createSessionModel(options = {}) {
|
|
|
146
147
|
return;
|
|
147
148
|
const calls = group;
|
|
148
149
|
group = null;
|
|
149
|
-
push([
|
|
150
|
+
push([folded(calls).join("\n")], "action", { expanded: unfolded(calls) });
|
|
151
|
+
}
|
|
152
|
+
/** A group with every call and its result: "full", or any level unfolded with Ctrl+O. */
|
|
153
|
+
function unfolded(calls) {
|
|
154
|
+
return toolGroupExpanded(calls, width(), { formatResult: options.formatResult });
|
|
155
|
+
}
|
|
156
|
+
/** A group as its level shows it folded: the calls without results, or one summary line. */
|
|
157
|
+
function folded(calls) {
|
|
158
|
+
return toolDetail === "calls" ? toolGroupExpanded(calls, width(), { results: false }) : [groupLine(calls)];
|
|
150
159
|
}
|
|
151
160
|
function groupLine(calls) {
|
|
152
161
|
return INDENT + ui.dim(fitWidth(toolGroupSummary(calls, options.toolPhrase), width() - INDENT.length));
|
|
@@ -161,7 +170,7 @@ export function createSessionModel(options = {}) {
|
|
|
161
170
|
let rows = liveRows;
|
|
162
171
|
let kind = liveRows.length > 0 ? "agent" : null;
|
|
163
172
|
if (group) {
|
|
164
|
-
rows = snapshot.expanded ?
|
|
173
|
+
rows = snapshot.expanded ? unfolded(group) : folded(group);
|
|
165
174
|
kind = "action";
|
|
166
175
|
}
|
|
167
176
|
const live = rows.join("\n");
|
|
@@ -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)}`,
|