mini-coder 0.5.3 → 0.5.5
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 +31 -27
- package/bun.lock +7 -7
- package/package.json +4 -4
- package/src/index.ts +5 -0
- package/src/prompt.ts +4 -1
- package/src/session.ts +198 -18
- package/src/submit.ts +13 -0
- package/src/ui/commands.test.ts +5 -0
- package/src/ui/commands.ts +5 -0
- package/src/ui/conversation.test.ts +181 -15
- package/src/ui/conversation.ts +109 -31
- package/src/ui/help.test.ts +9 -4
- package/src/ui/help.ts +5 -3
- package/src/ui/status.test.ts +58 -24
- package/src/ui/status.ts +2 -139
- package/src/ui.ts +7 -3
package/README.md
CHANGED
|
@@ -59,32 +59,34 @@ Plugins can add more tools, but the core stays intentionally small.
|
|
|
59
59
|
|
|
60
60
|
## Commands
|
|
61
61
|
|
|
62
|
-
| Command | Description
|
|
63
|
-
| ------------ |
|
|
64
|
-
| `/model` | Switch models
|
|
65
|
-
| `/session` |
|
|
66
|
-
| `/new` | Start a fresh session and reset
|
|
67
|
-
| `/fork` |
|
|
68
|
-
| `/undo` | Remove the last conversational turn
|
|
69
|
-
| `/reasoning` |
|
|
70
|
-
| `/verbose` |
|
|
71
|
-
| `/login` |
|
|
72
|
-
| `/logout` |
|
|
73
|
-
| `/effort` |
|
|
74
|
-
| `/help` |
|
|
62
|
+
| Command | Description |
|
|
63
|
+
| ------------ | ------------------------------------------------------------------------------------------------------ |
|
|
64
|
+
| `/model` | Switch models and save the choice as the global default. |
|
|
65
|
+
| `/session` | Open the session picker for the current working directory. |
|
|
66
|
+
| `/new` | Start a fresh session and reset the running token and cost totals. |
|
|
67
|
+
| `/fork` | Fork the current chat into a new session, keep the original, and add a UI-only `Forked session.` note. |
|
|
68
|
+
| `/undo` | Remove the last conversational turn without touching filesystem changes. |
|
|
69
|
+
| `/reasoning` | Show or hide model thinking. The setting is saved and restored on launch. |
|
|
70
|
+
| `/verbose` | Expand shell output plus edit previews and edit errors in the conversation log. |
|
|
71
|
+
| `/login` | Sign in with a supported OAuth provider. |
|
|
72
|
+
| `/logout` | Remove saved OAuth credentials for a logged-in provider. |
|
|
73
|
+
| `/effort` | Choose low, medium, high, or xhigh reasoning effort. |
|
|
74
|
+
| `/help` | Show commands, current toggles, loaded AGENTS.md files, skills, and plugins. |
|
|
75
75
|
|
|
76
76
|
## Key bindings
|
|
77
77
|
|
|
78
|
-
| Key | Action
|
|
79
|
-
| ------------- |
|
|
80
|
-
| `Enter` | Submit message
|
|
81
|
-
| `Shift+Enter` | Insert newline
|
|
82
|
-
| `Escape` |
|
|
83
|
-
| `Tab` |
|
|
84
|
-
| `Ctrl+R` | Search global raw input history
|
|
85
|
-
| `Ctrl+C` | Graceful exit
|
|
86
|
-
| `Ctrl+D` | Graceful exit
|
|
87
|
-
| `:q` | Graceful exit
|
|
78
|
+
| Key | Action |
|
|
79
|
+
| ------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
80
|
+
| `Enter` | Submit message |
|
|
81
|
+
| `Shift+Enter` | Insert newline |
|
|
82
|
+
| `Escape` | Dismiss the overlay without changing the draft; otherwise interrupt the running turn; otherwise do nothing |
|
|
83
|
+
| `Tab` | Autocomplete a path, or open the command picker when the draft starts with `/` |
|
|
84
|
+
| `Ctrl+R` | Search global raw input history |
|
|
85
|
+
| `Ctrl+C` | Graceful exit |
|
|
86
|
+
| `Ctrl+D` | Graceful exit when the input is empty |
|
|
87
|
+
| `:q` | Graceful exit |
|
|
88
|
+
| `Ctrl+Z` | Suspend the process |
|
|
89
|
+
| Mouse wheel | Scroll conversation history |
|
|
88
90
|
|
|
89
91
|
## Headless one-shot mode
|
|
90
92
|
|
|
@@ -95,10 +97,12 @@ $ mc -p "summarize this repo"
|
|
|
95
97
|
$ printf '%s\n' 'fix the failing tests' | mc
|
|
96
98
|
```
|
|
97
99
|
|
|
98
|
-
-
|
|
99
|
-
-
|
|
100
|
-
-
|
|
101
|
-
-
|
|
100
|
+
- Starts when `-p/--prompt` is provided or when stdin or stdout is not a TTY.
|
|
101
|
+
- If stdout is redirected but stdin is still interactive, pass `-p`; headless mode will not fall back to an interactive prompt.
|
|
102
|
+
- Uses the same parser as the TUI for plain text, `/skill:name`, and standalone image paths.
|
|
103
|
+
- Writes raw NDJSON events to stdout for text deltas, tool activity, final messages, and `done` / `error` / `aborted` outcomes.
|
|
104
|
+
- Headless runs still persist like normal sessions and show up in `/session` history for that working directory.
|
|
105
|
+
- Interactive slash commands such as `/model`, `/session`, and `/help` are not available in headless mode.
|
|
102
106
|
|
|
103
107
|
## Docs
|
|
104
108
|
|
package/bun.lock
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
"": {
|
|
6
6
|
"name": "mini-coder",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@cel-tui/components": "
|
|
9
|
-
"@cel-tui/core": "
|
|
10
|
-
"@cel-tui/types": "
|
|
8
|
+
"@cel-tui/components": "0.7.2",
|
|
9
|
+
"@cel-tui/core": "0.7.2",
|
|
10
|
+
"@cel-tui/types": "0.7.2",
|
|
11
11
|
"@mariozechner/pi-ai": "^0.66.1",
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
@@ -108,11 +108,11 @@
|
|
|
108
108
|
|
|
109
109
|
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.11", "", { "os": "win32", "cpu": "x64" }, "sha512-A8D3JM/00C2KQgUV3oj8Ba15EHEYwebAGCy5Sf9GAjr5Y3+kJIYOiESoqRDeuRZueuMdCsbLZIUqmPhpYXJE9A=="],
|
|
110
110
|
|
|
111
|
-
"@cel-tui/components": ["@cel-tui/components@0.
|
|
111
|
+
"@cel-tui/components": ["@cel-tui/components@0.7.2", "", { "dependencies": { "@cel-tui/core": "0.7.2", "@cel-tui/types": "0.7.2", "lextide": "^0.2.1", "yoctomarkdown": "^0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" } }, "sha512-RHq7G/lS6A733l534cSStQp+cJECkxZZWrLghbPHwGBsSeDdJ2MkrQm+02roh6EH/N40ZAYjQqplq8acRrnFFQ=="],
|
|
112
112
|
|
|
113
|
-
"@cel-tui/core": ["@cel-tui/core@0.
|
|
113
|
+
"@cel-tui/core": ["@cel-tui/core@0.7.2", "", { "dependencies": { "@cel-tui/types": "0.7.2", "get-east-asian-width": "^1.5.0" }, "peerDependencies": { "typescript": ">=5.0.0" } }, "sha512-Y5qdI9Zoa77Olq/LD+vqf3j0hyCRJYkkTkIdUfQplSDSUJS5NM+VSnxcQ8RCUNmrIsHeLmmAPHYsBsS1WMWk1w=="],
|
|
114
114
|
|
|
115
|
-
"@cel-tui/types": ["@cel-tui/types@0.
|
|
115
|
+
"@cel-tui/types": ["@cel-tui/types@0.7.2", "", { "peerDependencies": { "typescript": ">=5.0.0" } }, "sha512-XEmYkGqtBMOXI/DKLopv5OL7d5/8ybkj1OpJO4weWPyx4Y6VY9pYFaj8zYhVaFSQZcB23/yoA+DPYi0kOgoZhQ=="],
|
|
116
116
|
|
|
117
117
|
"@google/genai": ["@google/genai@1.48.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-plonYK4ML2PrxsRD9SeqmFt76eREWkQdPCglOA6aYDzL1AAbE+7PUnT54SvpWGfws13L0AZEqGSpL7+1IPnTxQ=="],
|
|
118
118
|
|
|
@@ -342,7 +342,7 @@
|
|
|
342
342
|
|
|
343
343
|
"lefthook-windows-x64": ["lefthook-windows-x64@2.1.5", "", { "os": "win32", "cpu": "x64" }, "sha512-yiAh8qxml6uqy10jDxOdN9fOQpyLxBFY1fgCEAhn7sVJYmJKRhjqSBwZX6LG5MQjzr29KStrIdw7TR3lf3rT7Q=="],
|
|
344
344
|
|
|
345
|
-
"lextide": ["lextide@0.1
|
|
345
|
+
"lextide": ["lextide@0.2.1", "", {}, "sha512-fP7liS0LoNJz2EF6G7+K21yvB58bJEjaTrsDcPi4Qv7xZynB9NzjY44N6tFK6SJaECHIUqF2kcWbWQ84hPfmlg=="],
|
|
346
346
|
|
|
347
347
|
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
|
348
348
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mini-coder",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"description": "A small, fast CLI coding agent",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
"test": "bun test"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@cel-tui/components": "^0.
|
|
22
|
-
"@cel-tui/core": "^0.
|
|
23
|
-
"@cel-tui/types": "^0.
|
|
21
|
+
"@cel-tui/components": "^0.7.2",
|
|
22
|
+
"@cel-tui/core": "^0.7.2",
|
|
23
|
+
"@cel-tui/types": "^0.7.2",
|
|
24
24
|
"@mariozechner/pi-ai": "^0.66.1"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
} from "./prompt.ts";
|
|
48
48
|
import {
|
|
49
49
|
appendMessage,
|
|
50
|
+
computeContextTokens,
|
|
50
51
|
computeStats,
|
|
51
52
|
createSession,
|
|
52
53
|
filterModelMessages,
|
|
@@ -526,6 +527,8 @@ export interface AppState {
|
|
|
526
527
|
messages: ReturnType<typeof loadMessages>;
|
|
527
528
|
/** Cumulative session input/output/cost stats for the status bar. */
|
|
528
529
|
stats: SessionStats;
|
|
530
|
+
/** Estimated model-visible context tokens for the next request. */
|
|
531
|
+
contextTokens: number;
|
|
529
532
|
/** Discovered AGENTS.md files. */
|
|
530
533
|
agentsMd: AgentsMdFile[];
|
|
531
534
|
/** Discovered skills. */
|
|
@@ -610,6 +613,7 @@ export async function init(): Promise<AppState> {
|
|
|
610
613
|
const effort = startup.effort;
|
|
611
614
|
const messages: ReturnType<typeof loadMessages> = [];
|
|
612
615
|
const stats = computeStats(messages);
|
|
616
|
+
const contextTokens = computeContextTokens(messages);
|
|
613
617
|
const promptContext = await loadPromptContext(filterModelMessages(messages), {
|
|
614
618
|
cwd,
|
|
615
619
|
});
|
|
@@ -621,6 +625,7 @@ export async function init(): Promise<AppState> {
|
|
|
621
625
|
effort,
|
|
622
626
|
messages,
|
|
623
627
|
stats,
|
|
628
|
+
contextTokens,
|
|
624
629
|
agentsMd: promptContext.agentsMd,
|
|
625
630
|
skills: promptContext.skills,
|
|
626
631
|
plugins: promptContext.plugins,
|
package/src/prompt.ts
CHANGED
|
@@ -225,6 +225,7 @@ Workflow: **inspect with shell → mutate with edit → verify with shell**.
|
|
|
225
225
|
|
|
226
226
|
- Think first: before any tool call, decide all files and information you need.
|
|
227
227
|
- Early in the task, look for acceptance criteria in tests, verifier scripts, eval scripts, examples, and expected-output files. Do not rely on the task text alone when machine-checkable criteria are available.
|
|
228
|
+
- Before relying on a binary, interpreter, or CLI helper you have not yet confirmed, probe availability with focused checks such as \`command -v <tool>\`, \`<tool> --version\`, or \`python3 -m pip --version\`, then adapt to what is actually installed.
|
|
228
229
|
- Batch reads: if you need multiple files, read them together in parallel rather than one at a time.
|
|
229
230
|
- Only make sequential calls when a later call genuinely depends on an earlier result.
|
|
230
231
|
|
|
@@ -233,13 +234,15 @@ Workflow: **inspect with shell → mutate with edit → verify with shell**.
|
|
|
233
234
|
- Be concise. Friendly coding teammate tone.
|
|
234
235
|
- After making changes: lead with a quick explanation of what changed and why, then suggest logical next steps if any.
|
|
235
236
|
- Don't dump large file contents you've written — reference file paths.
|
|
236
|
-
- When
|
|
237
|
+
- When discussing multiple options, use numbered lists so the user can reply with a number without turning it into a questionnaire.
|
|
237
238
|
- If asked for a review, focus on bugs, risks, regressions, and missing tests. Findings first, ordered by severity.
|
|
238
239
|
|
|
239
240
|
# Persistence
|
|
240
241
|
|
|
241
242
|
- Carry work through to completion within the current turn. Don't stop at analysis or partial fixes.
|
|
242
243
|
- Once the contract is clear, create the required artifact early, then iterate and improve it. Do not spend most of the turn exploring.
|
|
244
|
+
- Run the narrowest verifier or test as soon as there is a plausible first implementation or artifact. Do not delay the first verification run until the end.
|
|
245
|
+
- When verification is down to a small number of failures, stop broad exploration and focus only on the remaining failing assertions or exact contract gaps until the last detail passes.
|
|
243
246
|
- If you encounter an error, diagnose and fix it rather than reporting it and stopping.
|
|
244
247
|
- Before concluding, run the smallest targeted verification that checks the exact contract: required files exist, names and signatures match, outputs are in the required format, and no forbidden extra artifacts were left behind.
|
|
245
248
|
- Avoid excessive looping: if you're re-reading or re-editing the same files without progress, stop and ask the user.`;
|
package/src/session.ts
CHANGED
|
@@ -107,7 +107,7 @@ export interface UiMessage {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
/** Any message persisted in session history. */
|
|
110
|
-
type PersistedMessage = Message | UiMessage;
|
|
110
|
+
export type PersistedMessage = Message | UiMessage;
|
|
111
111
|
|
|
112
112
|
/** Options for creating a new session. */
|
|
113
113
|
interface CreateSessionOpts {
|
|
@@ -160,6 +160,8 @@ const EMPTY_ASSISTANT_USAGE: AssistantMessage["usage"] = {
|
|
|
160
160
|
},
|
|
161
161
|
};
|
|
162
162
|
|
|
163
|
+
const SQLITE_BUSY_TIMEOUT_MS = 1_000;
|
|
164
|
+
|
|
163
165
|
/** Row shape returned by `SELECT * FROM prompt_history`. */
|
|
164
166
|
type PromptHistoryRow = {
|
|
165
167
|
id: number;
|
|
@@ -253,6 +255,7 @@ CREATE INDEX IF NOT EXISTS idx_prompt_history_created_at ON prompt_history(creat
|
|
|
253
255
|
export function openDatabase(path: string): Database {
|
|
254
256
|
const db = new Database(path);
|
|
255
257
|
db.run("PRAGMA journal_mode = WAL");
|
|
258
|
+
db.run(`PRAGMA busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS}`);
|
|
256
259
|
db.run("PRAGMA foreign_keys = ON");
|
|
257
260
|
db.exec(SCHEMA);
|
|
258
261
|
return db;
|
|
@@ -709,6 +712,31 @@ export function getAssistantUsage(
|
|
|
709
712
|
};
|
|
710
713
|
}
|
|
711
714
|
|
|
715
|
+
function runInImmediateTransaction<T>(db: Database, callback: () => T): T {
|
|
716
|
+
if (db.inTransaction) {
|
|
717
|
+
return callback();
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
db.run("BEGIN IMMEDIATE");
|
|
721
|
+
try {
|
|
722
|
+
const result = callback();
|
|
723
|
+
db.run("COMMIT");
|
|
724
|
+
return result;
|
|
725
|
+
} catch (error) {
|
|
726
|
+
if (db.inTransaction) {
|
|
727
|
+
try {
|
|
728
|
+
db.run("ROLLBACK");
|
|
729
|
+
} catch (rollbackError) {
|
|
730
|
+
throw new AggregateError(
|
|
731
|
+
[error, rollbackError],
|
|
732
|
+
"Failed to roll back SQLite transaction",
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
throw error;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
712
740
|
/**
|
|
713
741
|
* Append a UI-only message to a session's history.
|
|
714
742
|
*
|
|
@@ -790,25 +818,27 @@ export function appendMessage(
|
|
|
790
818
|
message: PersistedMessage,
|
|
791
819
|
turn?: number,
|
|
792
820
|
): number | null {
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
821
|
+
return runInImmediateTransaction(db, () => {
|
|
822
|
+
const now = Date.now();
|
|
823
|
+
|
|
824
|
+
let effectiveTurn: number | null;
|
|
825
|
+
if (isUiMessage(message)) {
|
|
826
|
+
effectiveTurn = null;
|
|
827
|
+
} else if (turn !== undefined) {
|
|
828
|
+
effectiveTurn = turn;
|
|
829
|
+
} else {
|
|
830
|
+
const row = db.query<MaxTurnRow, [string]>(SQL.maxTurn).get(sessionId);
|
|
831
|
+
effectiveTurn = (row?.max_turn ?? 0) + 1;
|
|
832
|
+
}
|
|
804
833
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
834
|
+
db.run(
|
|
835
|
+
"INSERT INTO messages (session_id, turn, data, created_at) VALUES (?, ?, ?, ?)",
|
|
836
|
+
[sessionId, effectiveTurn, JSON.stringify(message), now],
|
|
837
|
+
);
|
|
838
|
+
db.run("UPDATE sessions SET updated_at = ? WHERE id = ?", [now, sessionId]);
|
|
810
839
|
|
|
811
|
-
|
|
840
|
+
return effectiveTurn;
|
|
841
|
+
});
|
|
812
842
|
}
|
|
813
843
|
|
|
814
844
|
/**
|
|
@@ -1041,3 +1071,153 @@ export function computeStats(
|
|
|
1041
1071
|
|
|
1042
1072
|
return stats;
|
|
1043
1073
|
}
|
|
1074
|
+
|
|
1075
|
+
// ---------------------------------------------------------------------------
|
|
1076
|
+
// Context estimation
|
|
1077
|
+
// ---------------------------------------------------------------------------
|
|
1078
|
+
|
|
1079
|
+
/** Conservative fixed estimate for an image block's token footprint. */
|
|
1080
|
+
const ESTIMATED_IMAGE_TOKENS = 1_200;
|
|
1081
|
+
|
|
1082
|
+
/** Calculate context tokens from assistant usage, falling back when `totalTokens` is zero. */
|
|
1083
|
+
function calculateUsageTokens(usage: AssistantMessage["usage"]): number {
|
|
1084
|
+
return (
|
|
1085
|
+
usage.totalTokens ||
|
|
1086
|
+
usage.input + usage.output + usage.cacheRead + usage.cacheWrite
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/** Estimate token usage from a character count using a conservative chars/4 heuristic. */
|
|
1091
|
+
function estimateCharacterTokens(charCount: number): number {
|
|
1092
|
+
return Math.ceil(charCount / 4);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
type UserMultipartContent = Exclude<
|
|
1096
|
+
Extract<Message, { role: "user" }>["content"],
|
|
1097
|
+
string
|
|
1098
|
+
>;
|
|
1099
|
+
type TextOrImageContentBlock =
|
|
1100
|
+
| UserMultipartContent[number]
|
|
1101
|
+
| Extract<Message, { role: "toolResult" }>["content"][number];
|
|
1102
|
+
|
|
1103
|
+
function estimateTextOrImageContentTokens(
|
|
1104
|
+
content: readonly TextOrImageContentBlock[],
|
|
1105
|
+
): number {
|
|
1106
|
+
let chars = 0;
|
|
1107
|
+
let imageTokens = 0;
|
|
1108
|
+
|
|
1109
|
+
for (const block of content) {
|
|
1110
|
+
if (block.type === "text") {
|
|
1111
|
+
chars += block.text.length;
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
if (block.type === "image") {
|
|
1115
|
+
imageTokens += ESTIMATED_IMAGE_TOKENS;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
return estimateCharacterTokens(chars) + imageTokens;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function estimateUserMessageTokens(
|
|
1123
|
+
message: Extract<Message, { role: "user" }>,
|
|
1124
|
+
): number {
|
|
1125
|
+
if (typeof message.content === "string") {
|
|
1126
|
+
return estimateCharacterTokens(message.content.length);
|
|
1127
|
+
}
|
|
1128
|
+
return estimateTextOrImageContentTokens(message.content);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
function estimateAssistantBlockCharacters(
|
|
1132
|
+
block: Extract<Message, { role: "assistant" }>["content"][number],
|
|
1133
|
+
): number {
|
|
1134
|
+
if (block.type === "text") {
|
|
1135
|
+
return block.text.length;
|
|
1136
|
+
}
|
|
1137
|
+
if (block.type === "thinking") {
|
|
1138
|
+
return block.thinking.length;
|
|
1139
|
+
}
|
|
1140
|
+
return block.name.length + JSON.stringify(block.arguments).length;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
function estimateAssistantMessageTokens(
|
|
1144
|
+
message: Extract<Message, { role: "assistant" }>,
|
|
1145
|
+
): number {
|
|
1146
|
+
const chars = message.content.reduce((total, block) => {
|
|
1147
|
+
return total + estimateAssistantBlockCharacters(block);
|
|
1148
|
+
}, 0);
|
|
1149
|
+
return estimateCharacterTokens(chars);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function estimateToolResultMessageTokens(
|
|
1153
|
+
message: Extract<Message, { role: "toolResult" }>,
|
|
1154
|
+
): number {
|
|
1155
|
+
return estimateTextOrImageContentTokens(message.content);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/** Estimate token usage for a model-visible message. */
|
|
1159
|
+
function estimateMessageTokens(message: Message): number {
|
|
1160
|
+
switch (message.role) {
|
|
1161
|
+
case "user":
|
|
1162
|
+
return estimateUserMessageTokens(message);
|
|
1163
|
+
case "assistant":
|
|
1164
|
+
return estimateAssistantMessageTokens(message);
|
|
1165
|
+
case "toolResult":
|
|
1166
|
+
return estimateToolResultMessageTokens(message);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* Fold one persisted message into the running context estimate for the next request.
|
|
1172
|
+
*
|
|
1173
|
+
* Assistant messages with valid usage anchor the full model-visible context for
|
|
1174
|
+
* that point in the transcript, so they replace the running estimate. All other
|
|
1175
|
+
* model-visible messages are added incrementally using the same conservative
|
|
1176
|
+
* estimation logic used before the first valid assistant usage appears.
|
|
1177
|
+
*
|
|
1178
|
+
* @param contextTokens - Running estimate before this message.
|
|
1179
|
+
* @param message - Persisted message to fold into the estimate.
|
|
1180
|
+
* @returns Updated context-token estimate.
|
|
1181
|
+
*/
|
|
1182
|
+
export function addMessageToContextTokens(
|
|
1183
|
+
contextTokens: number,
|
|
1184
|
+
message: PersistedMessage,
|
|
1185
|
+
): number {
|
|
1186
|
+
if (message.role === "ui") {
|
|
1187
|
+
return contextTokens;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
const usage = getAssistantUsage(message);
|
|
1191
|
+
if (
|
|
1192
|
+
message.role === "assistant" &&
|
|
1193
|
+
usage &&
|
|
1194
|
+
message.stopReason !== "aborted" &&
|
|
1195
|
+
message.stopReason !== "error"
|
|
1196
|
+
) {
|
|
1197
|
+
return calculateUsageTokens(usage);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
return contextTokens + estimateMessageTokens(message);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
/**
|
|
1204
|
+
* Estimate the current model-visible context size for the next request.
|
|
1205
|
+
*
|
|
1206
|
+
* Recomputed on session-load boundaries and maintained incrementally during an
|
|
1207
|
+
* active turn so render-time status-bar updates do not need to rescan the full
|
|
1208
|
+
* message history.
|
|
1209
|
+
*
|
|
1210
|
+
* @param messages - Full persisted session history.
|
|
1211
|
+
* @returns Estimated context tokens visible to the next model request.
|
|
1212
|
+
*/
|
|
1213
|
+
export function computeContextTokens(
|
|
1214
|
+
messages: readonly PersistedMessage[],
|
|
1215
|
+
): number {
|
|
1216
|
+
let contextTokens = 0;
|
|
1217
|
+
|
|
1218
|
+
for (const message of messages) {
|
|
1219
|
+
contextTokens = addMessageToContextTokens(contextTokens, message);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
return contextTokens;
|
|
1223
|
+
}
|
package/src/submit.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from "./index.ts";
|
|
20
20
|
import { parseInput } from "./input.ts";
|
|
21
21
|
import {
|
|
22
|
+
addMessageToContextTokens,
|
|
22
23
|
addMessageToStats,
|
|
23
24
|
appendMessage,
|
|
24
25
|
appendPromptHistory,
|
|
@@ -212,9 +213,17 @@ function handleAgentEvent(event: AgentEvent, state: AppState): void {
|
|
|
212
213
|
case "assistant_message":
|
|
213
214
|
state.messages.push(event.message);
|
|
214
215
|
state.stats = addMessageToStats(state.stats, event.message);
|
|
216
|
+
state.contextTokens = addMessageToContextTokens(
|
|
217
|
+
state.contextTokens,
|
|
218
|
+
event.message,
|
|
219
|
+
);
|
|
215
220
|
break;
|
|
216
221
|
case "tool_result":
|
|
217
222
|
state.messages.push(event.message);
|
|
223
|
+
state.contextTokens = addMessageToContextTokens(
|
|
224
|
+
state.contextTokens,
|
|
225
|
+
event.message,
|
|
226
|
+
);
|
|
218
227
|
break;
|
|
219
228
|
case "text_delta":
|
|
220
229
|
case "thinking_delta":
|
|
@@ -277,6 +286,10 @@ export async function submitResolvedInput(
|
|
|
277
286
|
|
|
278
287
|
const turn = appendMessage(state.db, session.id, userMessage);
|
|
279
288
|
state.messages.push(userMessage);
|
|
289
|
+
state.contextTokens = addMessageToContextTokens(
|
|
290
|
+
state.contextTokens,
|
|
291
|
+
userMessage,
|
|
292
|
+
);
|
|
280
293
|
hooks?.onUserMessage?.(state);
|
|
281
294
|
|
|
282
295
|
state.git = await loadGitState(state.cwd);
|
package/src/ui/commands.test.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { AppState } from "../index.ts";
|
|
|
8
8
|
import { COMMANDS } from "../input.ts";
|
|
9
9
|
import {
|
|
10
10
|
appendPromptHistory,
|
|
11
|
+
computeContextTokens,
|
|
11
12
|
createSession,
|
|
12
13
|
openDatabase,
|
|
13
14
|
} from "../session.ts";
|
|
@@ -75,6 +76,7 @@ function createTestState(): AppState {
|
|
|
75
76
|
effort: "medium",
|
|
76
77
|
messages: [],
|
|
77
78
|
stats: { totalInput: 0, totalOutput: 0, totalCost: 0 },
|
|
79
|
+
contextTokens: 0,
|
|
78
80
|
agentsMd: [],
|
|
79
81
|
skills: [],
|
|
80
82
|
plugins: [],
|
|
@@ -377,6 +379,7 @@ describe("ui/commands", () => {
|
|
|
377
379
|
totalOutput: 0,
|
|
378
380
|
totalCost: 0,
|
|
379
381
|
});
|
|
382
|
+
expect(state.contextTokens).toBe(computeContextTokens(state.messages));
|
|
380
383
|
} finally {
|
|
381
384
|
state.db.close();
|
|
382
385
|
}
|
|
@@ -436,6 +439,7 @@ describe("ui/commands", () => {
|
|
|
436
439
|
{ role: "ui", kind: "info", content: "old", timestamp: 1 },
|
|
437
440
|
];
|
|
438
441
|
state.stats = { totalInput: 10, totalOutput: 20, totalCost: 0.5 };
|
|
442
|
+
state.contextTokens = 123;
|
|
439
443
|
|
|
440
444
|
try {
|
|
441
445
|
expect(controller.handleCommand("new", state)).toBe(true);
|
|
@@ -449,6 +453,7 @@ describe("ui/commands", () => {
|
|
|
449
453
|
totalOutput: 0,
|
|
450
454
|
totalCost: 0,
|
|
451
455
|
});
|
|
456
|
+
expect(state.contextTokens).toBe(0);
|
|
452
457
|
expect(state.agentsMd).toEqual([
|
|
453
458
|
{ path: "/tmp/reloaded/AGENTS.md", content: "Reloaded context" },
|
|
454
459
|
]);
|
package/src/ui/commands.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type { AppState } from "../index.ts";
|
|
|
19
19
|
import { getAvailableModels, saveOAuthCredentials } from "../index.ts";
|
|
20
20
|
import { COMMANDS } from "../input.ts";
|
|
21
21
|
import {
|
|
22
|
+
computeContextTokens,
|
|
22
23
|
computeStats,
|
|
23
24
|
forkSession,
|
|
24
25
|
listPromptHistory,
|
|
@@ -387,6 +388,7 @@ export function createCommandController(
|
|
|
387
388
|
state.session = picked;
|
|
388
389
|
state.messages = loadMessages(state.db, picked.id);
|
|
389
390
|
state.stats = computeStats(state.messages);
|
|
391
|
+
state.contextTokens = computeContextTokens(state.messages);
|
|
390
392
|
runtime.scrollConversationToBottom();
|
|
391
393
|
}
|
|
392
394
|
}
|
|
@@ -402,6 +404,7 @@ export function createCommandController(
|
|
|
402
404
|
state.session = null;
|
|
403
405
|
state.messages = [];
|
|
404
406
|
state.stats = { totalInput: 0, totalOutput: 0, totalCost: 0 };
|
|
407
|
+
state.contextTokens = 0;
|
|
405
408
|
await runtime.reloadPromptContext(state);
|
|
406
409
|
runtime.scrollConversationToBottom();
|
|
407
410
|
runtime.render();
|
|
@@ -415,6 +418,7 @@ export function createCommandController(
|
|
|
415
418
|
state.session = forked;
|
|
416
419
|
state.messages = loadMessages(state.db, forked.id);
|
|
417
420
|
state.stats = computeStats(state.messages);
|
|
421
|
+
state.contextTokens = computeContextTokens(state.messages);
|
|
418
422
|
runtime.appendInfoMessage("Forked session.", state);
|
|
419
423
|
};
|
|
420
424
|
|
|
@@ -434,6 +438,7 @@ export function createCommandController(
|
|
|
434
438
|
if (removed) {
|
|
435
439
|
state.messages = loadMessages(state.db, state.session.id);
|
|
436
440
|
state.stats = computeStats(state.messages);
|
|
441
|
+
state.contextTokens = computeContextTokens(state.messages);
|
|
437
442
|
runtime.scrollConversationToBottom();
|
|
438
443
|
runtime.render();
|
|
439
444
|
}
|