mini-coder 0.5.8 → 0.5.9
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/package.json +1 -1
- package/src/prompt.ts +1 -0
- package/src/tools.ts +2 -1
- package/src/ui.ts +194 -18
package/package.json
CHANGED
package/src/prompt.ts
CHANGED
|
@@ -246,6 +246,7 @@ function buildCorePrompt(opts: BuildSystemPromptOpts): string {
|
|
|
246
246
|
"- Check requirements, and plan your changes before editing code.",
|
|
247
247
|
"- Implement the necessary changes, following good practices and proper error handling.",
|
|
248
248
|
"- Always verify your changes using compilation, testing, and manual verification when possible.",
|
|
249
|
+
"- When verifying with build or test commands, avoid leaving generated binaries or scratch artifacts in the requested output location; use temporary paths or remove them before finishing.",
|
|
249
250
|
"- Do not leave helpers, tests, or any other form of temporary files; clean up after yourself and leave no trace.",
|
|
250
251
|
"- Ensure you match the requested output exactly. This applies to file names, directory structure, number of files, output formats, and all other details.",
|
|
251
252
|
'- "Polish" is not optional; it counts just as much as solving the task.',
|
package/src/tools.ts
CHANGED
|
@@ -1574,7 +1574,8 @@ export const shellTool: Tool = {
|
|
|
1574
1574
|
name: "shell",
|
|
1575
1575
|
description:
|
|
1576
1576
|
"Run a command in the user's shell. Returns stdout, stderr, and exit code. " +
|
|
1577
|
-
"Use this to explore the codebase, read tests/verifiers/examples, inspect required outputs, and run targeted checks, builds, or git commands."
|
|
1577
|
+
"Use this to explore the codebase, read tests/verifiers/examples, inspect required outputs, and run targeted checks, builds, or git commands. " +
|
|
1578
|
+
"Commands mutate the real working directory, so direct verification outputs to temporary paths or clean them up before finishing.",
|
|
1578
1579
|
parameters: Type.Object({
|
|
1579
1580
|
command: Type.String({ description: "The shell command to execute" }),
|
|
1580
1581
|
}),
|
package/src/ui.ts
CHANGED
|
@@ -64,6 +64,23 @@ const DIVIDER_FRAME_MS = 60;
|
|
|
64
64
|
/** Width of the bright pulse segment in the animated divider. */
|
|
65
65
|
const PULSE_WIDTH = 5;
|
|
66
66
|
|
|
67
|
+
/** Number of trailing words shown in the idle terminal-title preview. */
|
|
68
|
+
const TERMINAL_TITLE_WORD_COUNT = 5;
|
|
69
|
+
|
|
70
|
+
/** Divider ticks spent on each animated terminal-title scanner frame. */
|
|
71
|
+
const TERMINAL_TITLE_TICKS_PER_FRAME = 4;
|
|
72
|
+
|
|
73
|
+
/** Frames used for the active terminal-title glow-scanner animation. */
|
|
74
|
+
const TERMINAL_TITLE_FRAMES = [
|
|
75
|
+
"[=o---]",
|
|
76
|
+
"[-=o--]",
|
|
77
|
+
"[--=o-]",
|
|
78
|
+
"[---=o]",
|
|
79
|
+
"[--o=-]",
|
|
80
|
+
"[-o=--]",
|
|
81
|
+
"[o=---]",
|
|
82
|
+
] as const;
|
|
83
|
+
|
|
67
84
|
/** Maximum number of committed messages rendered before older history is chunked. */
|
|
68
85
|
const CONVERSATION_CHUNK_MESSAGES = 50;
|
|
69
86
|
|
|
@@ -109,6 +126,15 @@ let dividerTimer: ReturnType<typeof setInterval> | null = null;
|
|
|
109
126
|
/** Whether stdin was already in raw mode before the TUI initialized. */
|
|
110
127
|
let stdinWasRaw = false;
|
|
111
128
|
|
|
129
|
+
/** Latest application state associated with the active terminal UI. */
|
|
130
|
+
let titleState: AppState | null = null;
|
|
131
|
+
|
|
132
|
+
/** Whether a cel viewport has rendered for the current UI session. */
|
|
133
|
+
let titleViewportActive = false;
|
|
134
|
+
|
|
135
|
+
/** Last terminal title written during the current UI session. */
|
|
136
|
+
let lastTerminalTitle: string | null = null;
|
|
137
|
+
|
|
112
138
|
// ---------------------------------------------------------------------------
|
|
113
139
|
// Overlay state
|
|
114
140
|
// ---------------------------------------------------------------------------
|
|
@@ -150,6 +176,148 @@ export function resetUiState(): void {
|
|
|
150
176
|
resetConversationRenderCache();
|
|
151
177
|
activeOverlay = null;
|
|
152
178
|
stdinWasRaw = false;
|
|
179
|
+
titleState = null;
|
|
180
|
+
titleViewportActive = false;
|
|
181
|
+
lastTerminalTitle = null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Terminal title
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
function collapseTerminalTitleText(text: string): string | null {
|
|
189
|
+
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
190
|
+
return collapsed.length > 0 ? collapsed : null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function getUserTerminalTitleText(
|
|
194
|
+
content: Extract<AppState["messages"][number], { role: "user" }>["content"],
|
|
195
|
+
): string | null {
|
|
196
|
+
if (typeof content === "string") {
|
|
197
|
+
return collapseTerminalTitleText(content);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const text = content
|
|
201
|
+
.filter(
|
|
202
|
+
(block): block is Extract<(typeof content)[number], { type: "text" }> => {
|
|
203
|
+
return block.type === "text";
|
|
204
|
+
},
|
|
205
|
+
)
|
|
206
|
+
.map((block) => block.text)
|
|
207
|
+
.join(" ");
|
|
208
|
+
|
|
209
|
+
return collapseTerminalTitleText(text);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function getAssistantTerminalTitleText(
|
|
213
|
+
content: Extract<
|
|
214
|
+
AppState["messages"][number],
|
|
215
|
+
{ role: "assistant" }
|
|
216
|
+
>["content"],
|
|
217
|
+
): string | null {
|
|
218
|
+
const text = content
|
|
219
|
+
.filter(
|
|
220
|
+
(
|
|
221
|
+
block,
|
|
222
|
+
): block is Extract<
|
|
223
|
+
Extract<
|
|
224
|
+
AppState["messages"][number],
|
|
225
|
+
{ role: "assistant" }
|
|
226
|
+
>["content"][number],
|
|
227
|
+
{ type: "text" }
|
|
228
|
+
> => {
|
|
229
|
+
return block.type === "text";
|
|
230
|
+
},
|
|
231
|
+
)
|
|
232
|
+
.map((block) => block.text)
|
|
233
|
+
.join(" ");
|
|
234
|
+
|
|
235
|
+
return collapseTerminalTitleText(text);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function truncateTerminalTitleTail(text: string): string {
|
|
239
|
+
const words = text.split(" ");
|
|
240
|
+
if (words.length <= TERMINAL_TITLE_WORD_COUNT) {
|
|
241
|
+
return text;
|
|
242
|
+
}
|
|
243
|
+
return `...${words.slice(-TERMINAL_TITLE_WORD_COUNT).join(" ")}`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function buildIdleTerminalTitle(state: Pick<AppState, "messages">): string {
|
|
247
|
+
for (let index = state.messages.length - 1; index >= 0; index -= 1) {
|
|
248
|
+
const message = state.messages[index];
|
|
249
|
+
if (!message) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
let text: string | null = null;
|
|
254
|
+
switch (message.role) {
|
|
255
|
+
case "user":
|
|
256
|
+
text = getUserTerminalTitleText(message.content);
|
|
257
|
+
break;
|
|
258
|
+
case "assistant":
|
|
259
|
+
text = getAssistantTerminalTitleText(message.content);
|
|
260
|
+
break;
|
|
261
|
+
case "toolResult":
|
|
262
|
+
case "ui":
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (text) {
|
|
267
|
+
return `mc - ${truncateTerminalTitleTail(text)}`;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return "mc";
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Build the current terminal title from UI state.
|
|
276
|
+
*
|
|
277
|
+
* Idle titles show a short tail preview from the latest conversational text
|
|
278
|
+
* message. Active turns show a stable-width glow scanner.
|
|
279
|
+
*
|
|
280
|
+
* @param state - Application state needed to derive the title.
|
|
281
|
+
* @param animationTick - Divider animation tick used to pick the scanner frame.
|
|
282
|
+
* @returns The terminal title text to write via cel-tui.
|
|
283
|
+
*/
|
|
284
|
+
export function buildTerminalTitle(
|
|
285
|
+
state: Pick<AppState, "messages" | "running">,
|
|
286
|
+
animationTick = dividerTick,
|
|
287
|
+
): string {
|
|
288
|
+
if (!state.running) {
|
|
289
|
+
return buildIdleTerminalTitle(state);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const frameIndex =
|
|
293
|
+
Math.floor(animationTick / TERMINAL_TITLE_TICKS_PER_FRAME) %
|
|
294
|
+
TERMINAL_TITLE_FRAMES.length;
|
|
295
|
+
return `mc - ${TERMINAL_TITLE_FRAMES[frameIndex]}`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function syncTerminalTitle(
|
|
299
|
+
state: Pick<AppState, "messages" | "running"> | null,
|
|
300
|
+
): void {
|
|
301
|
+
if (!state || !titleViewportActive) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const title = buildTerminalTitle(state);
|
|
306
|
+
if (title === lastTerminalTitle) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
cel.setTitle(title);
|
|
311
|
+
lastTerminalTitle = title;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function invalidateTerminalTitleCache(): void {
|
|
315
|
+
lastTerminalTitle = null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function requestRender(): void {
|
|
319
|
+
syncTerminalTitle(titleState);
|
|
320
|
+
cel.render();
|
|
153
321
|
}
|
|
154
322
|
|
|
155
323
|
// ---------------------------------------------------------------------------
|
|
@@ -162,7 +330,7 @@ function startDividerAnimation(): void {
|
|
|
162
330
|
dividerTick = 0;
|
|
163
331
|
dividerTimer = setInterval(() => {
|
|
164
332
|
dividerTick++;
|
|
165
|
-
|
|
333
|
+
requestRender();
|
|
166
334
|
}, DIVIDER_FRAME_MS);
|
|
167
335
|
}
|
|
168
336
|
|
|
@@ -286,14 +454,14 @@ function prependConversationChunk(state: AppState, width: number): void {
|
|
|
286
454
|
function openOverlay(overlay: ActiveOverlay): void {
|
|
287
455
|
activeOverlay = overlay;
|
|
288
456
|
inputFocused = false;
|
|
289
|
-
|
|
457
|
+
requestRender();
|
|
290
458
|
}
|
|
291
459
|
|
|
292
460
|
/** Dismiss the active overlay and return focus to the input. */
|
|
293
461
|
function dismissOverlay(): void {
|
|
294
462
|
activeOverlay = null;
|
|
295
463
|
inputFocused = true;
|
|
296
|
-
|
|
464
|
+
requestRender();
|
|
297
465
|
}
|
|
298
466
|
|
|
299
467
|
function openPathAutocompleteOverlay(state: AppState): void {
|
|
@@ -331,7 +499,7 @@ function handleTabKeyPress(state: AppState): void {
|
|
|
331
499
|
const completedInput = autocompleteInputPath(inputValue, state.cwd);
|
|
332
500
|
if (completedInput) {
|
|
333
501
|
inputValue = completedInput;
|
|
334
|
-
|
|
502
|
+
requestRender();
|
|
335
503
|
return;
|
|
336
504
|
}
|
|
337
505
|
|
|
@@ -366,18 +534,20 @@ export function renderActiveOverlay(state: AppState): Node | null {
|
|
|
366
534
|
* @returns Stable callbacks for the controlled TextInput.
|
|
367
535
|
*/
|
|
368
536
|
export function createInputController(state: AppState): InputController {
|
|
537
|
+
titleState = state;
|
|
538
|
+
|
|
369
539
|
return {
|
|
370
540
|
onChange: (value) => {
|
|
371
541
|
inputValue = value;
|
|
372
|
-
|
|
542
|
+
requestRender();
|
|
373
543
|
},
|
|
374
544
|
onFocus: () => {
|
|
375
545
|
inputFocused = true;
|
|
376
|
-
|
|
546
|
+
requestRender();
|
|
377
547
|
},
|
|
378
548
|
onBlur: () => {
|
|
379
549
|
inputFocused = false;
|
|
380
|
-
|
|
550
|
+
requestRender();
|
|
381
551
|
},
|
|
382
552
|
onKeyPress: (key) => {
|
|
383
553
|
if (key === "enter") {
|
|
@@ -385,13 +555,13 @@ export function createInputController(state: AppState): InputController {
|
|
|
385
555
|
|
|
386
556
|
if (isQuitInput(raw)) {
|
|
387
557
|
inputValue = "";
|
|
388
|
-
|
|
558
|
+
requestRender();
|
|
389
559
|
requestGracefulExit(state);
|
|
390
560
|
return false;
|
|
391
561
|
}
|
|
392
562
|
|
|
393
563
|
inputValue = "";
|
|
394
|
-
|
|
564
|
+
requestRender();
|
|
395
565
|
handleInput(raw, state);
|
|
396
566
|
return false;
|
|
397
567
|
}
|
|
@@ -479,7 +649,7 @@ function appendUiMessage(
|
|
|
479
649
|
}
|
|
480
650
|
state.messages.push(message);
|
|
481
651
|
scrollConversationToBottom();
|
|
482
|
-
|
|
652
|
+
requestRender();
|
|
483
653
|
}
|
|
484
654
|
|
|
485
655
|
/**
|
|
@@ -521,9 +691,7 @@ const commandController = createCommandController({
|
|
|
521
691
|
appendInfoMessage,
|
|
522
692
|
appendTodoMessage,
|
|
523
693
|
scrollConversationToBottom,
|
|
524
|
-
render:
|
|
525
|
-
cel.render();
|
|
526
|
-
},
|
|
694
|
+
render: requestRender,
|
|
527
695
|
reloadPromptContext,
|
|
528
696
|
openInBrowser,
|
|
529
697
|
});
|
|
@@ -537,9 +705,7 @@ const agentController = createUiAgentController({
|
|
|
537
705
|
appendInfoMessage,
|
|
538
706
|
handleCommand: (command, state) =>
|
|
539
707
|
commandController.handleCommand(command, state),
|
|
540
|
-
render:
|
|
541
|
-
cel.render();
|
|
542
|
-
},
|
|
708
|
+
render: requestRender,
|
|
543
709
|
scrollConversationToBottom,
|
|
544
710
|
startDividerAnimation,
|
|
545
711
|
stopDividerAnimation,
|
|
@@ -547,6 +713,7 @@ const agentController = createUiAgentController({
|
|
|
547
713
|
|
|
548
714
|
/** Route raw user input through parseInput and dispatch accordingly. */
|
|
549
715
|
export function handleInput(raw: string, state: AppState): void {
|
|
716
|
+
titleState = state;
|
|
550
717
|
agentController.handleInput(raw, state);
|
|
551
718
|
}
|
|
552
719
|
|
|
@@ -618,6 +785,7 @@ export function suspendToBackground(
|
|
|
618
785
|
stop();
|
|
619
786
|
onResume(() => {
|
|
620
787
|
clearInterval(keepAlive);
|
|
788
|
+
invalidateTerminalTitleCache();
|
|
621
789
|
resumeUi();
|
|
622
790
|
});
|
|
623
791
|
|
|
@@ -657,7 +825,7 @@ function renderConversationLog(state: AppState, width: number): Node {
|
|
|
657
825
|
prependConversationChunk(state, width);
|
|
658
826
|
}
|
|
659
827
|
|
|
660
|
-
|
|
828
|
+
requestRender();
|
|
661
829
|
},
|
|
662
830
|
},
|
|
663
831
|
buildConversationLog(state, width),
|
|
@@ -681,6 +849,12 @@ export function renderBaseLayout(
|
|
|
681
849
|
inputController: InputController,
|
|
682
850
|
onSuspend?: () => void,
|
|
683
851
|
): Node {
|
|
852
|
+
titleState = state;
|
|
853
|
+
titleViewportActive = true;
|
|
854
|
+
if (lastTerminalTitle === null) {
|
|
855
|
+
syncTerminalTitle(state);
|
|
856
|
+
}
|
|
857
|
+
|
|
684
858
|
return VStack(
|
|
685
859
|
{
|
|
686
860
|
height: "100%",
|
|
@@ -736,6 +910,7 @@ export function renderBaseLayout(
|
|
|
736
910
|
export function startUI(state: AppState): void {
|
|
737
911
|
resetUiState();
|
|
738
912
|
stdinWasRaw = process.stdin.isRaw || false;
|
|
913
|
+
titleState = state;
|
|
739
914
|
const terminal = new ProcessTerminal();
|
|
740
915
|
const inputController = createInputController(state);
|
|
741
916
|
cel.init(terminal);
|
|
@@ -749,7 +924,7 @@ export function startUI(state: AppState): void {
|
|
|
749
924
|
if (state.running) {
|
|
750
925
|
startDividerAnimation();
|
|
751
926
|
}
|
|
752
|
-
|
|
927
|
+
requestRender();
|
|
753
928
|
});
|
|
754
929
|
});
|
|
755
930
|
const overlay = renderActiveOverlay(state);
|
|
@@ -759,6 +934,7 @@ export function startUI(state: AppState): void {
|
|
|
759
934
|
}
|
|
760
935
|
return base;
|
|
761
936
|
});
|
|
937
|
+
syncTerminalTitle(state);
|
|
762
938
|
|
|
763
939
|
if (state.running) {
|
|
764
940
|
startDividerAnimation();
|