mini-coder 0.5.9 → 0.5.11
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 +1 -1
- package/package.json +1 -1
- package/src/agent.ts +5 -1
- package/src/input.ts +1 -1
- package/src/session.ts +15 -2
- package/src/ui/commands.test.ts +40 -289
- package/src/ui/commands.ts +7 -2
- package/src/ui/conversation.ts +18 -4
- package/src/ui/help.ts +47 -33
- package/src/ui.ts +8 -2
- package/src/ui/agent.test.ts +0 -49
- package/src/ui/conversation.test.ts +0 -1435
- package/src/ui/help.test.ts +0 -50
- package/src/ui/overlay.test.ts +0 -42
- package/src/ui/render-performance.test.ts +0 -444
- package/src/ui/status.test.ts +0 -489
package/README.md
CHANGED
|
@@ -103,7 +103,7 @@ $ printf '%s\n' 'fix the failing tests' | mc
|
|
|
103
103
|
- Starts when `-p/--prompt` is provided or when stdin or stdout is not a TTY.
|
|
104
104
|
- If stdout is redirected but stdin is still interactive, pass `-p`; headless mode will not fall back to an interactive prompt.
|
|
105
105
|
- Uses the same parser as the TUI for plain text, `/skill:name`, and standalone image paths.
|
|
106
|
-
- With `--json`, writes NDJSON events for completed assistant/tool-result messages plus `done` / `error` / `aborted` outcomes;
|
|
106
|
+
- With `--json`, writes NDJSON events for completed assistant/tool-result messages plus `done` / `error` / `aborted` outcomes; queued `user_message` events may also appear. Streaming deltas are omitted.
|
|
107
107
|
- Headless runs still persist like normal sessions and show up in `/session` history for that working directory.
|
|
108
108
|
- Interactive slash commands such as `/model`, `/session`, and `/help` are not available in headless mode.
|
|
109
109
|
|
package/package.json
CHANGED
package/src/agent.ts
CHANGED
|
@@ -623,6 +623,10 @@ function getTodoReminderSignature(
|
|
|
623
623
|
);
|
|
624
624
|
}
|
|
625
625
|
|
|
626
|
+
function wrapSystemReminder(content: string): string {
|
|
627
|
+
return `<system_reminder>\n${content}\n</system_reminder>`;
|
|
628
|
+
}
|
|
629
|
+
|
|
626
630
|
function createTodoReminderMessage(
|
|
627
631
|
messages: readonly Message[],
|
|
628
632
|
remindedTodoSignatures: Set<string>,
|
|
@@ -651,7 +655,7 @@ function createTodoReminderMessage(
|
|
|
651
655
|
|
|
652
656
|
return {
|
|
653
657
|
role: "user",
|
|
654
|
-
content: lines.join("\n"),
|
|
658
|
+
content: wrapSystemReminder(lines.join("\n")),
|
|
655
659
|
timestamp: Date.now(),
|
|
656
660
|
};
|
|
657
661
|
}
|
package/src/input.ts
CHANGED
|
@@ -17,7 +17,6 @@ import { extname, isAbsolute, join } from "node:path";
|
|
|
17
17
|
|
|
18
18
|
/** All recognized slash commands. */
|
|
19
19
|
export const COMMANDS = [
|
|
20
|
-
"model",
|
|
21
20
|
"session",
|
|
22
21
|
"new",
|
|
23
22
|
"fork",
|
|
@@ -28,6 +27,7 @@ export const COMMANDS = [
|
|
|
28
27
|
"login",
|
|
29
28
|
"logout",
|
|
30
29
|
"help",
|
|
30
|
+
"model",
|
|
31
31
|
"effort",
|
|
32
32
|
] as const;
|
|
33
33
|
|
package/src/session.ts
CHANGED
|
@@ -95,6 +95,9 @@ interface AppendPromptHistoryOpts {
|
|
|
95
95
|
sessionId?: string;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
/** Rich-text format hints supported by persisted UI info messages. */
|
|
99
|
+
export type UiInfoFormat = "markdown";
|
|
100
|
+
|
|
98
101
|
/** A persisted UI-only info message shown in the conversation log. */
|
|
99
102
|
export interface UiInfoMessage {
|
|
100
103
|
/** Identifies this as an internal UI message. */
|
|
@@ -103,6 +106,8 @@ export interface UiInfoMessage {
|
|
|
103
106
|
kind: "info";
|
|
104
107
|
/** Display text shown in the conversation log. */
|
|
105
108
|
content: string;
|
|
109
|
+
/** Optional rich-text format hint for the content. */
|
|
110
|
+
format?: UiInfoFormat;
|
|
106
111
|
/** Unix timestamp in milliseconds. */
|
|
107
112
|
timestamp: number;
|
|
108
113
|
}
|
|
@@ -513,7 +518,10 @@ function isUiMessageRecord(value: unknown): value is UiMessage {
|
|
|
513
518
|
}
|
|
514
519
|
|
|
515
520
|
if (record.kind === "info") {
|
|
516
|
-
return
|
|
521
|
+
return (
|
|
522
|
+
typeof record.content === "string" &&
|
|
523
|
+
(record.format === undefined || record.format === "markdown")
|
|
524
|
+
);
|
|
517
525
|
}
|
|
518
526
|
|
|
519
527
|
return (
|
|
@@ -629,13 +637,18 @@ export function truncateSessions(
|
|
|
629
637
|
* Create a persisted UI info message.
|
|
630
638
|
*
|
|
631
639
|
* @param content - Display text shown in the conversation log.
|
|
640
|
+
* @param format - Optional rich-text format hint for the content.
|
|
632
641
|
* @returns A new {@link UiInfoMessage}.
|
|
633
642
|
*/
|
|
634
|
-
export function createUiMessage(
|
|
643
|
+
export function createUiMessage(
|
|
644
|
+
content: string,
|
|
645
|
+
format?: UiInfoFormat,
|
|
646
|
+
): UiInfoMessage {
|
|
635
647
|
return {
|
|
636
648
|
role: "ui",
|
|
637
649
|
kind: "info",
|
|
638
650
|
content,
|
|
651
|
+
...(format ? { format } : {}),
|
|
639
652
|
timestamp: Date.now(),
|
|
640
653
|
};
|
|
641
654
|
}
|
package/src/ui/commands.test.ts
CHANGED
|
@@ -2,40 +2,19 @@ import { afterEach, describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import type { ContainerNode
|
|
5
|
+
import type { ContainerNode } from "@cel-tui/types";
|
|
6
6
|
import { registerFauxProvider } from "@mariozechner/pi-ai";
|
|
7
7
|
import type { AppState } from "../index.ts";
|
|
8
|
-
import { COMMANDS } from "../input.ts";
|
|
9
8
|
import {
|
|
10
|
-
appendPromptHistory,
|
|
11
9
|
computeContextTokens,
|
|
12
10
|
createSession,
|
|
13
11
|
openDatabase,
|
|
14
12
|
} from "../session.ts";
|
|
15
13
|
import { loadSettings } from "../settings.ts";
|
|
16
14
|
import { DEFAULT_THEME } from "../theme.ts";
|
|
17
|
-
import {
|
|
18
|
-
createCommandController,
|
|
19
|
-
formatPromptHistoryLabel,
|
|
20
|
-
formatPromptHistoryPreview,
|
|
21
|
-
formatRelativeDate,
|
|
22
|
-
formatSessionLabel,
|
|
23
|
-
} from "./commands.ts";
|
|
15
|
+
import { createCommandController } from "./commands.ts";
|
|
24
16
|
import type { ActiveOverlay } from "./overlay.ts";
|
|
25
17
|
|
|
26
|
-
function collectText(node: Node | null): string[] {
|
|
27
|
-
if (!node) {
|
|
28
|
-
return [];
|
|
29
|
-
}
|
|
30
|
-
if (node.type === "text") {
|
|
31
|
-
return [node.content];
|
|
32
|
-
}
|
|
33
|
-
if (node.type === "textinput") {
|
|
34
|
-
return [];
|
|
35
|
-
}
|
|
36
|
-
return node.children.flatMap((child) => collectText(child));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
18
|
function expectOverlay(overlay: ActiveOverlay | null): ActiveOverlay {
|
|
40
19
|
if (!overlay) {
|
|
41
20
|
throw new Error("Expected an active overlay");
|
|
@@ -107,200 +86,6 @@ afterEach(() => {
|
|
|
107
86
|
});
|
|
108
87
|
|
|
109
88
|
describe("ui/commands", () => {
|
|
110
|
-
test("showCommandAutocomplete clears the current draft and opens the commands overlay", () => {
|
|
111
|
-
const state = createTestState();
|
|
112
|
-
const runtimeState = { overlay: null as ActiveOverlay | null };
|
|
113
|
-
let inputValue = "draft";
|
|
114
|
-
const controller = createCommandController({
|
|
115
|
-
openOverlay: (nextOverlay) => {
|
|
116
|
-
runtimeState.overlay = nextOverlay;
|
|
117
|
-
},
|
|
118
|
-
dismissOverlay: () => {
|
|
119
|
-
runtimeState.overlay = null;
|
|
120
|
-
},
|
|
121
|
-
setInputValue: (value) => {
|
|
122
|
-
inputValue = value;
|
|
123
|
-
},
|
|
124
|
-
appendInfoMessage: () => {},
|
|
125
|
-
appendTodoMessage: () => {},
|
|
126
|
-
scrollConversationToBottom: () => {},
|
|
127
|
-
render: () => {},
|
|
128
|
-
reloadPromptContext: async () => {},
|
|
129
|
-
openInBrowser: () => {},
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
try {
|
|
133
|
-
controller.showCommandAutocomplete(state);
|
|
134
|
-
|
|
135
|
-
const overlay = expectOverlay(runtimeState.overlay);
|
|
136
|
-
|
|
137
|
-
expect(inputValue).toBe("");
|
|
138
|
-
expect(overlay.title).toBe("Commands");
|
|
139
|
-
|
|
140
|
-
const text = collectText(overlay.select());
|
|
141
|
-
for (const command of COMMANDS) {
|
|
142
|
-
expect(text.some((line) => line.includes(`/${command}`))).toBe(true);
|
|
143
|
-
}
|
|
144
|
-
} finally {
|
|
145
|
-
state.db.close();
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
test("showInputHistoryOverlay restores the selected raw prompt", () => {
|
|
150
|
-
const state = createTestState();
|
|
151
|
-
const runtimeState = { overlay: null as ActiveOverlay | null };
|
|
152
|
-
let inputValue = "draft";
|
|
153
|
-
const rawPrompt = "first line\nsecond line";
|
|
154
|
-
const controller = createCommandController({
|
|
155
|
-
openOverlay: (nextOverlay) => {
|
|
156
|
-
runtimeState.overlay = nextOverlay;
|
|
157
|
-
},
|
|
158
|
-
dismissOverlay: () => {
|
|
159
|
-
runtimeState.overlay = null;
|
|
160
|
-
},
|
|
161
|
-
setInputValue: (value) => {
|
|
162
|
-
inputValue = value;
|
|
163
|
-
},
|
|
164
|
-
appendInfoMessage: () => {},
|
|
165
|
-
appendTodoMessage: () => {},
|
|
166
|
-
scrollConversationToBottom: () => {},
|
|
167
|
-
render: () => {},
|
|
168
|
-
reloadPromptContext: async () => {},
|
|
169
|
-
openInBrowser: () => {},
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
try {
|
|
173
|
-
appendPromptHistory(state.db, {
|
|
174
|
-
text: "older prompt",
|
|
175
|
-
cwd: "/tmp/older",
|
|
176
|
-
});
|
|
177
|
-
appendPromptHistory(state.db, { text: rawPrompt, cwd: state.cwd });
|
|
178
|
-
|
|
179
|
-
controller.showInputHistoryOverlay(state);
|
|
180
|
-
|
|
181
|
-
const overlay = expectOverlay(runtimeState.overlay);
|
|
182
|
-
|
|
183
|
-
expect(overlay.title).toBe("Input history");
|
|
184
|
-
|
|
185
|
-
const selectNode = renderSelect(overlay);
|
|
186
|
-
|
|
187
|
-
selectNode.props.onKeyPress?.("enter");
|
|
188
|
-
|
|
189
|
-
expect(runtimeState.overlay).toBeNull();
|
|
190
|
-
expect(inputValue).toBe(rawPrompt);
|
|
191
|
-
} finally {
|
|
192
|
-
state.db.close();
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
test("showInputHistoryOverlay dismissal leaves the current draft unchanged", () => {
|
|
197
|
-
const state = createTestState();
|
|
198
|
-
const runtimeState = { overlay: null as ActiveOverlay | null };
|
|
199
|
-
let inputValue = "draft prompt";
|
|
200
|
-
const controller = createCommandController({
|
|
201
|
-
openOverlay: (nextOverlay) => {
|
|
202
|
-
runtimeState.overlay = nextOverlay;
|
|
203
|
-
},
|
|
204
|
-
dismissOverlay: () => {
|
|
205
|
-
runtimeState.overlay = null;
|
|
206
|
-
},
|
|
207
|
-
setInputValue: (value) => {
|
|
208
|
-
inputValue = value;
|
|
209
|
-
},
|
|
210
|
-
appendInfoMessage: () => {},
|
|
211
|
-
appendTodoMessage: () => {},
|
|
212
|
-
scrollConversationToBottom: () => {},
|
|
213
|
-
render: () => {},
|
|
214
|
-
reloadPromptContext: async () => {},
|
|
215
|
-
openInBrowser: () => {},
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
try {
|
|
219
|
-
appendPromptHistory(state.db, {
|
|
220
|
-
text: "saved prompt",
|
|
221
|
-
cwd: state.cwd,
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
controller.showInputHistoryOverlay(state);
|
|
225
|
-
|
|
226
|
-
const overlay = expectOverlay(runtimeState.overlay);
|
|
227
|
-
const selectNode = renderSelect(overlay);
|
|
228
|
-
|
|
229
|
-
selectNode.props.onBlur?.();
|
|
230
|
-
|
|
231
|
-
expect(runtimeState.overlay).toBeNull();
|
|
232
|
-
expect(inputValue).toBe("draft prompt");
|
|
233
|
-
} finally {
|
|
234
|
-
state.db.close();
|
|
235
|
-
}
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
test("handleCommand('session') overlay includes the first user preview for disambiguation", () => {
|
|
239
|
-
const state = createTestState();
|
|
240
|
-
const runtimeState = { overlay: null as ActiveOverlay | null };
|
|
241
|
-
const controller = createCommandController({
|
|
242
|
-
openOverlay: (nextOverlay) => {
|
|
243
|
-
runtimeState.overlay = nextOverlay;
|
|
244
|
-
},
|
|
245
|
-
dismissOverlay: () => {
|
|
246
|
-
runtimeState.overlay = null;
|
|
247
|
-
},
|
|
248
|
-
setInputValue: () => {},
|
|
249
|
-
appendInfoMessage: () => {},
|
|
250
|
-
appendTodoMessage: () => {},
|
|
251
|
-
scrollConversationToBottom: () => {},
|
|
252
|
-
render: () => {},
|
|
253
|
-
reloadPromptContext: async () => {},
|
|
254
|
-
openInBrowser: () => {},
|
|
255
|
-
});
|
|
256
|
-
const session = createSession(state.db, {
|
|
257
|
-
cwd: state.canonicalCwd,
|
|
258
|
-
model: "test/beta",
|
|
259
|
-
effort: "high",
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
state.db.run(
|
|
263
|
-
"INSERT INTO messages (session_id, turn, data, created_at) VALUES (?, ?, ?, ?)",
|
|
264
|
-
[
|
|
265
|
-
session.id,
|
|
266
|
-
null,
|
|
267
|
-
JSON.stringify({
|
|
268
|
-
role: "ui",
|
|
269
|
-
kind: "info",
|
|
270
|
-
content: "Help output",
|
|
271
|
-
timestamp: 1,
|
|
272
|
-
}),
|
|
273
|
-
1,
|
|
274
|
-
],
|
|
275
|
-
);
|
|
276
|
-
state.db.run(
|
|
277
|
-
"INSERT INTO messages (session_id, turn, data, created_at) VALUES (?, ?, ?, ?)",
|
|
278
|
-
[
|
|
279
|
-
session.id,
|
|
280
|
-
1,
|
|
281
|
-
JSON.stringify({
|
|
282
|
-
role: "user",
|
|
283
|
-
content: "Investigate\nthis session label",
|
|
284
|
-
timestamp: 2,
|
|
285
|
-
}),
|
|
286
|
-
2,
|
|
287
|
-
],
|
|
288
|
-
);
|
|
289
|
-
|
|
290
|
-
try {
|
|
291
|
-
expect(controller.handleCommand("session", state)).toBe(true);
|
|
292
|
-
|
|
293
|
-
const overlay = expectOverlay(runtimeState.overlay);
|
|
294
|
-
const text = collectText(renderSelect(overlay));
|
|
295
|
-
|
|
296
|
-
expect(
|
|
297
|
-
text.some((line) => line.includes("Investigate this session")),
|
|
298
|
-
).toBe(true);
|
|
299
|
-
} finally {
|
|
300
|
-
state.db.close();
|
|
301
|
-
}
|
|
302
|
-
});
|
|
303
|
-
|
|
304
89
|
test("handleCommand('session') restores the selected session and recomputes stats", () => {
|
|
305
90
|
const state = createTestState();
|
|
306
91
|
const runtimeState = { overlay: null as ActiveOverlay | null };
|
|
@@ -391,27 +176,6 @@ describe("ui/commands", () => {
|
|
|
391
176
|
}
|
|
392
177
|
});
|
|
393
178
|
|
|
394
|
-
test("handleCommand returns false for unknown commands", () => {
|
|
395
|
-
const state = createTestState();
|
|
396
|
-
const controller = createCommandController({
|
|
397
|
-
openOverlay: () => {},
|
|
398
|
-
dismissOverlay: () => {},
|
|
399
|
-
setInputValue: () => {},
|
|
400
|
-
appendInfoMessage: () => {},
|
|
401
|
-
appendTodoMessage: () => {},
|
|
402
|
-
scrollConversationToBottom: () => {},
|
|
403
|
-
render: () => {},
|
|
404
|
-
reloadPromptContext: async () => {},
|
|
405
|
-
openInBrowser: () => {},
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
try {
|
|
409
|
-
expect(controller.handleCommand("unknown", state)).toBe(false);
|
|
410
|
-
} finally {
|
|
411
|
-
state.db.close();
|
|
412
|
-
}
|
|
413
|
-
});
|
|
414
|
-
|
|
415
179
|
test("/new clears the active session state and reloads prompt context", async () => {
|
|
416
180
|
const state = createTestState();
|
|
417
181
|
let reloadCount = 0;
|
|
@@ -520,6 +284,44 @@ describe("ui/commands", () => {
|
|
|
520
284
|
}
|
|
521
285
|
});
|
|
522
286
|
|
|
287
|
+
test("/help appends markdown help without creating a session", () => {
|
|
288
|
+
const state = createTestState();
|
|
289
|
+
const appended: Array<{
|
|
290
|
+
text: string;
|
|
291
|
+
format: string | undefined;
|
|
292
|
+
sessionId: string | null;
|
|
293
|
+
}> = [];
|
|
294
|
+
const controller = createCommandController({
|
|
295
|
+
openOverlay: () => {},
|
|
296
|
+
dismissOverlay: () => {},
|
|
297
|
+
setInputValue: () => {},
|
|
298
|
+
appendInfoMessage: (text, nextState, format) => {
|
|
299
|
+
appended.push({
|
|
300
|
+
text,
|
|
301
|
+
format,
|
|
302
|
+
sessionId: nextState.session?.id ?? null,
|
|
303
|
+
});
|
|
304
|
+
},
|
|
305
|
+
appendTodoMessage: () => {},
|
|
306
|
+
scrollConversationToBottom: () => {},
|
|
307
|
+
render: () => {},
|
|
308
|
+
reloadPromptContext: async () => {},
|
|
309
|
+
openInBrowser: () => {},
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
expect(controller.handleCommand("help", state)).toBe(true);
|
|
314
|
+
expect(appended).toHaveLength(1);
|
|
315
|
+
expect(appended[0]?.text).toContain("# Help");
|
|
316
|
+
expect(appended[0]?.text).toContain("## Commands");
|
|
317
|
+
expect(appended[0]?.format).toBe("markdown");
|
|
318
|
+
expect(appended[0]?.sessionId).toBeNull();
|
|
319
|
+
expect(state.session).toBeNull();
|
|
320
|
+
} finally {
|
|
321
|
+
state.db.close();
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
523
325
|
test("/todo appends the current todo list without creating a session", () => {
|
|
524
326
|
const state = createTestState();
|
|
525
327
|
state.messages = [
|
|
@@ -636,55 +438,4 @@ describe("ui/commands", () => {
|
|
|
636
438
|
state.db.close();
|
|
637
439
|
}
|
|
638
440
|
});
|
|
639
|
-
|
|
640
|
-
test("formatRelativeDate accepts an explicit clock for deterministic output", () => {
|
|
641
|
-
const now = new Date("2026-04-07T12:00:00Z");
|
|
642
|
-
|
|
643
|
-
expect(formatRelativeDate(new Date("2026-04-07T11:59:45Z"), now)).toBe(
|
|
644
|
-
"just now",
|
|
645
|
-
);
|
|
646
|
-
expect(formatRelativeDate(new Date("2026-04-07T11:50:00Z"), now)).toBe(
|
|
647
|
-
"10m ago",
|
|
648
|
-
);
|
|
649
|
-
expect(formatRelativeDate(new Date("2026-04-07T10:00:00Z"), now)).toBe(
|
|
650
|
-
"2h ago",
|
|
651
|
-
);
|
|
652
|
-
expect(formatRelativeDate(new Date("2026-04-04T12:00:00Z"), now)).toBe(
|
|
653
|
-
"3d ago",
|
|
654
|
-
);
|
|
655
|
-
});
|
|
656
|
-
|
|
657
|
-
test("formatPromptHistoryPreview collapses whitespace into one line", () => {
|
|
658
|
-
expect(formatPromptHistoryPreview(" first\n\n second\tthird ")).toBe(
|
|
659
|
-
"first second third",
|
|
660
|
-
);
|
|
661
|
-
});
|
|
662
|
-
|
|
663
|
-
test("formatPromptHistoryLabel_longPromptAndCwd_returnsATruncatedSingleLineLabel", () => {
|
|
664
|
-
expect(
|
|
665
|
-
formatPromptHistoryLabel(
|
|
666
|
-
" Investigate\n\n this prompt history row because it is much too wide for the overlay ",
|
|
667
|
-
"/tmp/projects/very/deeply/nested/mini-coder-audit",
|
|
668
|
-
"5m ago",
|
|
669
|
-
),
|
|
670
|
-
).toBe(
|
|
671
|
-
"Investigate this prompt history… · …/mini-coder-audit · 5m ago",
|
|
672
|
-
);
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
test("formatSessionLabel_longPreviewAndModel_returnsATruncatedReadableLabel", () => {
|
|
676
|
-
expect(
|
|
677
|
-
formatSessionLabel(
|
|
678
|
-
{
|
|
679
|
-
model: "openai-codex/gpt-5.4-super-long-variant",
|
|
680
|
-
firstUserPreview:
|
|
681
|
-
"Audit the session selector because every entry looks identical in real usage",
|
|
682
|
-
},
|
|
683
|
-
"just now",
|
|
684
|
-
true,
|
|
685
|
-
),
|
|
686
|
-
).toBe(
|
|
687
|
-
"Audit the session selector… · openai-codex/gpt… · just now · current",
|
|
688
|
-
);
|
|
689
|
-
});
|
|
690
441
|
});
|
package/src/ui/commands.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
listSessions,
|
|
27
27
|
loadMessages,
|
|
28
28
|
type SessionListEntry,
|
|
29
|
+
type UiInfoFormat,
|
|
29
30
|
undoLastTurn,
|
|
30
31
|
} from "../session.ts";
|
|
31
32
|
import { updateSettings } from "../settings.ts";
|
|
@@ -51,7 +52,11 @@ interface UiCommandRuntime {
|
|
|
51
52
|
/** Update the current input draft. */
|
|
52
53
|
setInputValue: (value: string) => void;
|
|
53
54
|
/** Append a UI-only info message to the conversation log. */
|
|
54
|
-
appendInfoMessage: (
|
|
55
|
+
appendInfoMessage: (
|
|
56
|
+
text: string,
|
|
57
|
+
state: AppState,
|
|
58
|
+
format?: UiInfoFormat,
|
|
59
|
+
) => void;
|
|
55
60
|
/** Append a UI-only todo snapshot to the conversation log. */
|
|
56
61
|
appendTodoMessage: (
|
|
57
62
|
todos: ReturnType<typeof getTodoItems>,
|
|
@@ -583,7 +588,7 @@ export function createCommandController(
|
|
|
583
588
|
};
|
|
584
589
|
|
|
585
590
|
const handleHelpCommand = (state: AppState): void => {
|
|
586
|
-
runtime.appendInfoMessage(buildHelpText(state), state);
|
|
591
|
+
runtime.appendInfoMessage(buildHelpText(state), state, "markdown");
|
|
587
592
|
};
|
|
588
593
|
|
|
589
594
|
const handleTodoCommand = (state: AppState): void => {
|
package/src/ui/conversation.ts
CHANGED
|
@@ -1161,14 +1161,28 @@ function renderUiTodoMessage(
|
|
|
1161
1161
|
}
|
|
1162
1162
|
|
|
1163
1163
|
/** Render an internal UI message in the conversation log. */
|
|
1164
|
-
function renderUiMessage(
|
|
1164
|
+
function renderUiMessage(
|
|
1165
|
+
msg: UiMessage,
|
|
1166
|
+
opts: Pick<ConversationRenderOpts, "previewWidth" | "theme">,
|
|
1167
|
+
): Node {
|
|
1165
1168
|
if (msg.kind === "todo") {
|
|
1166
|
-
return renderUiTodoMessage(msg, theme);
|
|
1169
|
+
return renderUiTodoMessage(msg, opts.theme);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
if (msg.format === "markdown") {
|
|
1173
|
+
const markdown = renderMarkdownTextBlock(
|
|
1174
|
+
msg.content,
|
|
1175
|
+
opts.theme,
|
|
1176
|
+
opts.previewWidth,
|
|
1177
|
+
);
|
|
1178
|
+
if (markdown) {
|
|
1179
|
+
return markdown;
|
|
1180
|
+
}
|
|
1167
1181
|
}
|
|
1168
1182
|
|
|
1169
1183
|
return VStack({ padding: { x: 1 } }, [
|
|
1170
1184
|
Text(msg.content, {
|
|
1171
|
-
fgColor: theme.mutedText,
|
|
1185
|
+
fgColor: opts.theme.mutedText,
|
|
1172
1186
|
italic: true,
|
|
1173
1187
|
wrap: "word",
|
|
1174
1188
|
}),
|
|
@@ -1231,7 +1245,7 @@ function renderConversationMessage(
|
|
|
1231
1245
|
theme: Theme,
|
|
1232
1246
|
): Node | null {
|
|
1233
1247
|
if (message.role === "ui") {
|
|
1234
|
-
return renderUiMessage(message,
|
|
1248
|
+
return renderUiMessage(message, renderOpts);
|
|
1235
1249
|
}
|
|
1236
1250
|
if (message.role === "user") {
|
|
1237
1251
|
return renderUserMessage(message, theme);
|
package/src/ui/help.ts
CHANGED
|
@@ -47,7 +47,7 @@ export const COMMAND_DESCRIPTIONS: Record<string, string> = {
|
|
|
47
47
|
*
|
|
48
48
|
* @param command - Command name.
|
|
49
49
|
* @param state - Help-relevant application state.
|
|
50
|
-
* @returns
|
|
50
|
+
* @returns Markdown-ready command description.
|
|
51
51
|
*/
|
|
52
52
|
function getHelpCommandDescription(
|
|
53
53
|
command: (typeof COMMANDS)[number],
|
|
@@ -55,72 +55,86 @@ function getHelpCommandDescription(
|
|
|
55
55
|
): string {
|
|
56
56
|
const description = COMMAND_DESCRIPTIONS[command] ?? "";
|
|
57
57
|
if (command === "reasoning") {
|
|
58
|
-
return `${description} (currently ${state.showReasoning ? "on" : "off"})`;
|
|
58
|
+
return `${description} _(currently ${state.showReasoning ? "on" : "off"})_`;
|
|
59
59
|
}
|
|
60
60
|
if (command === "verbose") {
|
|
61
|
-
return `${description} (currently ${state.verbose ? "on" : "off"})`;
|
|
61
|
+
return `${description} _(currently ${state.verbose ? "on" : "off"})_`;
|
|
62
62
|
}
|
|
63
63
|
return description;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
function formatInlineCode(text: string): string {
|
|
67
|
+
return `\`${text}\``;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function formatInlineCodeList(items: readonly string[]): string {
|
|
71
|
+
return items.map((item) => formatInlineCode(item)).join(", ");
|
|
72
|
+
}
|
|
73
|
+
|
|
66
74
|
/**
|
|
67
75
|
* Build the `/help` text shown in the conversation log.
|
|
68
76
|
*
|
|
69
77
|
* @param state - Help-relevant application state.
|
|
70
|
-
* @returns Multi-line help text for display.
|
|
78
|
+
* @returns Multi-line markdown help text for display.
|
|
71
79
|
*/
|
|
72
80
|
export function buildHelpText(state: HelpRenderState): string {
|
|
73
|
-
const lines: string[] = [];
|
|
81
|
+
const lines: string[] = ["# Help", "", "## Commands", ""];
|
|
74
82
|
|
|
75
|
-
lines.push("Commands:");
|
|
76
83
|
for (const command of COMMANDS) {
|
|
77
|
-
lines.push(
|
|
84
|
+
lines.push(
|
|
85
|
+
`- ${formatInlineCode(`/${command}`)} — ${getHelpCommandDescription(command, state)}`,
|
|
86
|
+
);
|
|
78
87
|
}
|
|
79
88
|
|
|
80
89
|
const providerNames = Array.from(state.providers.keys());
|
|
81
|
-
lines.push("");
|
|
82
|
-
lines.push("Note:");
|
|
83
|
-
lines.push(
|
|
84
|
-
" Escape closes the current overlay and returns focus to the input.",
|
|
85
|
-
);
|
|
86
|
-
lines.push(" With no overlay open, Escape interrupts the current turn.");
|
|
87
|
-
lines.push(" Otherwise Escape does nothing.");
|
|
88
|
-
|
|
89
|
-
lines.push("");
|
|
90
90
|
lines.push(
|
|
91
|
+
"",
|
|
92
|
+
"## Keyboard",
|
|
93
|
+
"",
|
|
94
|
+
"- `Enter` submits the current draft.",
|
|
95
|
+
"- `Shift+Enter` inserts a newline.",
|
|
96
|
+
"- `Tab` opens command autocomplete when the draft starts with `/`.",
|
|
97
|
+
"- Otherwise, `Tab` autocompletes file paths and can open a path picker when there are multiple matches.",
|
|
98
|
+
"- `Ctrl+R` opens global input history search.",
|
|
99
|
+
"- `Escape` closes the current overlay and returns focus to the input.",
|
|
100
|
+
"- With no overlay open, `Escape` interrupts the current turn.",
|
|
101
|
+
"- Otherwise, `Escape` does nothing.",
|
|
102
|
+
"- `Ctrl+C` exits gracefully.",
|
|
103
|
+
"- `Ctrl+D` exits when the input is empty.",
|
|
104
|
+
"- `Ctrl+Z` suspends the app to the background.",
|
|
105
|
+
"",
|
|
106
|
+
"## Current state",
|
|
107
|
+
"",
|
|
91
108
|
providerNames.length > 0
|
|
92
|
-
?
|
|
93
|
-
: "Providers: none
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
lines.push(
|
|
109
|
+
? `- Providers: ${formatInlineCodeList(providerNames)}`
|
|
110
|
+
: "- Providers: none — use `/login`",
|
|
97
111
|
state.model
|
|
98
|
-
?
|
|
99
|
-
: "Model: none
|
|
112
|
+
? `- Model: ${formatInlineCode(`${state.model.provider}/${state.model.id}`)}`
|
|
113
|
+
: "- Model: none — use `/model`",
|
|
100
114
|
);
|
|
101
115
|
|
|
102
116
|
if (state.agentsMd.length > 0) {
|
|
103
|
-
lines.push("");
|
|
104
|
-
lines.push("AGENTS.md files:");
|
|
117
|
+
lines.push("", "## Loaded `AGENTS.md` files", "");
|
|
105
118
|
for (const agentFile of state.agentsMd) {
|
|
106
|
-
lines.push(
|
|
119
|
+
lines.push(`- ${formatInlineCode(abbreviatePath(agentFile.path))}`);
|
|
107
120
|
}
|
|
108
121
|
}
|
|
109
122
|
|
|
110
123
|
if (state.skills.length > 0) {
|
|
111
|
-
lines.push("");
|
|
112
|
-
lines.push("Skills:");
|
|
124
|
+
lines.push("", "## Skills", "");
|
|
113
125
|
for (const skill of state.skills) {
|
|
114
|
-
|
|
115
|
-
|
|
126
|
+
lines.push(
|
|
127
|
+
skill.description
|
|
128
|
+
? `- ${formatInlineCode(skill.name)} — ${skill.description}`
|
|
129
|
+
: `- ${formatInlineCode(skill.name)}`,
|
|
130
|
+
);
|
|
116
131
|
}
|
|
117
132
|
}
|
|
118
133
|
|
|
119
134
|
if (state.plugins.length > 0) {
|
|
120
|
-
lines.push("");
|
|
121
|
-
lines.push("Plugins:");
|
|
135
|
+
lines.push("", "## Plugins", "");
|
|
122
136
|
for (const plugin of state.plugins) {
|
|
123
|
-
lines.push(
|
|
137
|
+
lines.push(`- ${formatInlineCode(plugin.entry.name)}`);
|
|
124
138
|
}
|
|
125
139
|
}
|
|
126
140
|
|