@sean.holung/minicode 0.3.7 → 0.3.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.
Files changed (32) hide show
  1. package/dist/src/agent/config.js +25 -0
  2. package/dist/src/model-utils.js +18 -1
  3. package/dist/src/serve/agent-bridge.js +89 -15
  4. package/dist/src/serve/server.js +151 -3
  5. package/dist/src/session/session-store.js +29 -1
  6. package/dist/src/web/app.js +691 -105
  7. package/dist/src/web/index.html +117 -9
  8. package/dist/src/web/style.css +198 -10
  9. package/dist/tests/agent.test.js +16 -0
  10. package/dist/tests/config-integration.test.js +91 -1
  11. package/dist/tests/context-indicator.test.js +9 -0
  12. package/dist/tests/file-tools.test.js +12 -0
  13. package/dist/tests/graph-onboarding.test.js +8 -0
  14. package/dist/tests/model-client-openai.test.js +41 -0
  15. package/dist/tests/model-dropdown-ui.test.js +23 -0
  16. package/dist/tests/model-utils.test.js +26 -1
  17. package/dist/tests/serve.integration.test.js +194 -0
  18. package/dist/tests/session-store.test.js +32 -1
  19. package/dist/tests/session-ui.test.js +6 -0
  20. package/dist/tests/settings-ui.test.js +11 -0
  21. package/dist/tests/setup-overlay-state.test.js +49 -0
  22. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts.map +1 -1
  23. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js +10 -1
  24. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js.map +1 -1
  25. package/node_modules/@minicode/agent-sdk/dist/src/model/client.d.ts.map +1 -1
  26. package/node_modules/@minicode/agent-sdk/dist/src/model/client.js +21 -0
  27. package/node_modules/@minicode/agent-sdk/dist/src/model/client.js.map +1 -1
  28. package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.d.ts.map +1 -1
  29. package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.js +60 -6
  30. package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.js.map +1 -1
  31. package/node_modules/@minicode/agent-sdk/dist/tsconfig.tsbuildinfo +1 -1
  32. package/package.json +1 -1
@@ -16,7 +16,7 @@
16
16
  <div class="header-left">
17
17
  <h1>minicode</h1>
18
18
  <span id="status-badge" class="badge ready">ready</span>
19
- <div id="context-indicator" title="Context window usage">
19
+ <div id="context-indicator" title="Context window usage. Adjust context size in Settings if you want it larger or smaller.">
20
20
  <div id="context-bar">
21
21
  <div id="context-fill"></div>
22
22
  </div>
@@ -27,6 +27,10 @@
27
27
  <div class="model-menu">
28
28
  <button id="model-btn" class="header-btn" title="Switch model"><span id="model-info"></span> ▾</button>
29
29
  <div id="model-dropdown" class="dropdown hidden">
30
+ <div class="dropdown-section dropdown-search-section">
31
+ <input id="model-search" type="text" placeholder="Search models..." autocomplete="off" />
32
+ </div>
33
+ <div class="dropdown-divider"></div>
30
34
  <div id="model-list" class="dropdown-section">
31
35
  <div class="dropdown-empty">Loading models...</div>
32
36
  </div>
@@ -41,6 +45,10 @@
41
45
  <div id="session-update-row" class="session-update-row hidden">
42
46
  <button id="session-update-btn" class="dropdown-action" type="button">Update current saved session</button>
43
47
  </div>
48
+ <label id="session-autosave-row" class="session-autosave-row" title="Automatically save or update this chat after each completed turn.">
49
+ <input id="session-autosave-toggle" type="checkbox" />
50
+ <span>Auto-save after each turn</span>
51
+ </label>
44
52
  <div class="dropdown-row">
45
53
  <input id="save-label" type="text" placeholder="Label (optional)" />
46
54
  <button id="save-btn" class="dropdown-action">Save</button>
@@ -61,15 +69,28 @@
61
69
  <div class="config-overlay-content">
62
70
  <h2>Agent not connected</h2>
63
71
  <p id="config-missing" class="config-missing hidden"></p>
64
- <div id="config-overlay-spotlight" class="config-overlay-spotlight">
65
- <div class="config-overlay-spotlight-copy">
66
- <span class="config-overlay-spotlight-badge">Fastest way to start</span>
67
- <strong>Try minicode for free with OpenRouter</strong>
68
- <p>Create a free OpenRouter account, connect it to this <code>minicode serve</code> session, and use free models without editing config files first.</p>
72
+ <div id="config-overlay-quick-connects" class="config-overlay-shortcuts">
73
+ <div id="config-overlay-spotlight" class="config-overlay-spotlight">
74
+ <div class="config-overlay-spotlight-copy">
75
+ <span class="config-overlay-spotlight-badge">Fastest way to start</span>
76
+ <strong>Try minicode for free with OpenRouter</strong>
77
+ <p>Create a free OpenRouter account, connect it to this <code>minicode serve</code> session, and use free models without editing config files first.</p>
78
+ </div>
79
+ <div class="config-overlay-actions config-overlay-spotlight-actions">
80
+ <button id="connect-openrouter-btn" class="dropdown-action" type="button" data-openrouter-connect="true">Connect OpenRouter</button>
81
+ </div>
69
82
  </div>
70
- <div class="config-overlay-actions config-overlay-spotlight-actions">
71
- <button id="connect-openrouter-btn" class="dropdown-action" type="button" data-openrouter-connect="true">Connect OpenRouter</button>
72
- <span class="config-overlay-action-note">Session-only. Nothing is written to disk.</span>
83
+
84
+ <div id="config-overlay-openai-compatible" class="config-overlay-spotlight config-overlay-spotlight-secondary">
85
+ <div class="config-overlay-spotlight-copy">
86
+ <span class="config-overlay-spotlight-badge">Local quick connect</span>
87
+ <strong>Connect OpenAI-compatible</strong>
88
+ <p>Choose a preset like LM Studio, OpenAI, Ollama, or Custom, then adjust the endpoint if needed before connecting this <code>minicode serve</code> session.</p>
89
+ </div>
90
+ <div class="config-overlay-actions config-overlay-spotlight-actions">
91
+ <button id="connect-openai-compatible-btn" class="dropdown-action" type="button" data-openai-compatible-connect="true">Connect OpenAI-compatible</button>
92
+ <span class="config-overlay-action-note">Starts with common presets, but the endpoint stays editable.</span>
93
+ </div>
73
94
  </div>
74
95
  </div>
75
96
  <p id="config-overlay-intro">minicode needs a model provider to run. Configure one of the following:</p>
@@ -115,6 +136,7 @@ MODEL=your-model-name</pre>
115
136
  <div id="graph-toolbar">
116
137
  <input id="graph-search" type="text" placeholder="Search symbols or files..." autocomplete="off" />
117
138
  <button id="graph-analyze" class="header-btn">Analyze</button>
139
+ <button id="graph-refresh" class="header-btn" title="Refresh the dependency graph and symbol search">Refresh</button>
118
140
  <button id="graph-fit" class="header-btn">Fit</button>
119
141
  <button id="graph-relayout" class="header-btn">Re-layout</button>
120
142
  <button id="graph-clear" class="header-btn">Clear</button>
@@ -176,6 +198,23 @@ MODEL=your-model-name</pre>
176
198
  </div>
177
199
  <button id="disconnect-openrouter-btn" class="header-btn" type="button">Disconnect OpenRouter</button>
178
200
  </div>
201
+ <div id="settings-openai-compatible-session" class="settings-session-banner hidden" role="status" aria-live="polite">
202
+ <div class="settings-session-copy">
203
+ <div class="settings-session-title">OpenAI-compatible provider is connected for this serve session</div>
204
+ <div id="settings-openai-compatible-session-meta" class="settings-session-meta"></div>
205
+ </div>
206
+ <button id="disconnect-openai-compatible-btn" class="header-btn" type="button">Disconnect OpenAI-compatible</button>
207
+ </div>
208
+ <div class="settings-provider-shortcuts">
209
+ <div class="settings-provider-shortcuts-copy">
210
+ <div class="settings-session-title">Quick connect</div>
211
+ <div class="settings-session-meta">Temporarily connect a provider for this serve session, or optionally save it to <code>~/.minicode/.env</code>.</div>
212
+ </div>
213
+ <div class="settings-provider-shortcuts-actions">
214
+ <button class="header-btn" type="button" data-openrouter-connect="true">Connect OpenRouter</button>
215
+ <button class="header-btn" type="button" data-openai-compatible-connect="true">Connect OpenAI-compatible</button>
216
+ </div>
217
+ </div>
179
218
  <div id="settings-list" class="settings-list">
180
219
  <div class="dropdown-empty">Loading settings...</div>
181
220
  </div>
@@ -235,6 +274,75 @@ MODEL=your-model-name</pre>
235
274
  </section>
236
275
  </div>
237
276
 
277
+ <div id="openai-compatible-connect-modal" class="modal hidden" aria-hidden="true">
278
+ <div id="openai-compatible-connect-backdrop" class="modal-backdrop"></div>
279
+ <section class="modal-panel modal-panel-compact" role="dialog" aria-modal="true" aria-labelledby="openai-compatible-connect-title">
280
+ <div class="modal-header">
281
+ <div>
282
+ <h2 id="openai-compatible-connect-title">Connect OpenAI-compatible</h2>
283
+ <p class="modal-subtitle">Configure an OpenAI-compatible endpoint for this <code>minicode serve</code> session and optionally save it for future runs.</p>
284
+ </div>
285
+ <button id="openai-compatible-connect-close" class="header-btn" type="button">Close</button>
286
+ </div>
287
+
288
+ <div class="openrouter-connect-body">
289
+ <p class="openrouter-connect-lead">
290
+ Pick a preset to prefill a common endpoint, then adjust it if needed. minicode will point this serve session at the OpenAI-compatible API endpoint you provide.
291
+ </p>
292
+
293
+ <div class="openrouter-connect-note">
294
+ <strong>Connection details</strong>
295
+ <p>Local runtimes like LM Studio or Ollama usually work without an API key, while hosted providers like OpenAI typically require one. The endpoint stays editable no matter which preset you choose.</p>
296
+ </div>
297
+
298
+ <label class="provider-input-group" for="openai-compatible-preset">
299
+ <span class="provider-input-label">Preset</span>
300
+ <select id="openai-compatible-preset" class="provider-input">
301
+ <option value="lmstudio">LM Studio</option>
302
+ <option value="openai">OpenAI</option>
303
+ <option value="ollama">Ollama</option>
304
+ <option value="custom">Custom</option>
305
+ </select>
306
+ </label>
307
+
308
+ <p id="openai-compatible-preset-help" class="openrouter-connect-help">
309
+ LM Studio pre-fills the default local server endpoint at <code>http://localhost:1234/v1</code>.
310
+ </p>
311
+
312
+ <label class="provider-input-group" for="openai-compatible-base-url">
313
+ <span class="provider-input-label">Endpoint</span>
314
+ <input id="openai-compatible-base-url" class="provider-input" type="text" value="http://localhost:1234/v1" spellcheck="false" autocomplete="off" />
315
+ </label>
316
+
317
+ <label class="provider-input-group" for="openai-compatible-api-key">
318
+ <span class="provider-input-label">API key (optional)</span>
319
+ <input id="openai-compatible-api-key" class="provider-input" type="password" placeholder="Leave blank for local providers that do not require auth" spellcheck="false" autocomplete="off" />
320
+ </label>
321
+
322
+ <label class="openrouter-persist-toggle" for="openai-compatible-persist-checkbox">
323
+ <input id="openai-compatible-persist-checkbox" type="checkbox" />
324
+ <span>Save the OpenAI-compatible provider defaults to <code>~/.minicode/.env</code> after connecting.</span>
325
+ </label>
326
+
327
+ <p class="openrouter-connect-help">
328
+ When enabled, minicode will update <code>MODEL_PROVIDER</code> and <code>OPENAI_BASE_URL</code>, and it will also write <code>OPENAI_API_KEY</code> if you provide one. Nothing is written unless you check the box.
329
+ </p>
330
+
331
+ <p id="openai-compatible-connect-status" class="config-connect-status hidden"></p>
332
+ </div>
333
+
334
+ <div class="modal-footer">
335
+ <p class="settings-footnote">
336
+ This uses the same OpenAI-compatible runtime path minicode already uses for local and hosted providers.
337
+ </p>
338
+ <div class="settings-actions">
339
+ <button id="openai-compatible-connect-cancel" class="header-btn" type="button">Cancel</button>
340
+ <button id="openai-compatible-connect-continue" class="dropdown-action" type="button">Continue</button>
341
+ </div>
342
+ </div>
343
+ </section>
344
+ </div>
345
+
238
346
  <div id="file-preview-modal" class="modal hidden" aria-hidden="true">
239
347
  <div id="file-preview-backdrop" class="modal-backdrop"></div>
240
348
  <section class="modal-panel modal-panel-file-preview" role="dialog" aria-modal="true" aria-labelledby="file-preview-title">
@@ -90,24 +90,51 @@ h1 {
90
90
  }
91
91
 
92
92
  #model-dropdown {
93
- max-height: 320px;
93
+ width: min(420px, calc(100vw - 32px));
94
+ max-height: 360px;
95
+ }
96
+
97
+ #model-search {
98
+ width: 100%;
99
+ background: var(--bg);
100
+ border: 1px solid var(--border);
101
+ border-radius: 4px;
102
+ color: var(--text);
103
+ font-family: var(--font-mono);
104
+ font-size: 12px;
105
+ padding: 6px 8px;
106
+ }
107
+
108
+ #model-search:focus {
109
+ outline: none;
110
+ border-color: var(--accent);
94
111
  }
95
112
 
96
113
  #model-list {
97
- max-height: 280px;
114
+ max-height: 300px;
98
115
  overflow-y: auto;
99
116
  }
100
117
 
118
+ .dropdown-search-section {
119
+ padding-bottom: 10px;
120
+ }
121
+
101
122
  .model-item {
102
- padding: 6px 8px;
123
+ display: flex;
124
+ align-items: center;
125
+ justify-content: space-between;
126
+ gap: 12px;
127
+ width: 100%;
128
+ padding: 8px;
103
129
  border-radius: 4px;
104
130
  cursor: pointer;
131
+ border: none;
132
+ background: transparent;
133
+ text-align: left;
105
134
  font-size: 12px;
106
135
  transition: background 0.15s;
107
136
  color: var(--text);
108
- overflow: hidden;
109
- text-overflow: ellipsis;
110
- white-space: nowrap;
137
+ font-family: var(--font-mono);
111
138
  }
112
139
 
113
140
  .model-item:hover {
@@ -115,8 +142,40 @@ h1 {
115
142
  }
116
143
 
117
144
  .model-item.active {
145
+ background: rgba(122, 162, 247, 0.12);
146
+ outline: 1px solid rgba(122, 162, 247, 0.35);
147
+ }
148
+
149
+ .model-item-body {
150
+ display: flex;
151
+ flex: 1;
152
+ flex-direction: column;
153
+ min-width: 0;
154
+ }
155
+
156
+ .model-item-name {
157
+ color: var(--text);
158
+ font-weight: 500;
159
+ overflow: hidden;
160
+ text-overflow: ellipsis;
161
+ white-space: nowrap;
162
+ }
163
+
164
+ .model-item-subtitle {
165
+ color: var(--text-dim);
166
+ font-size: 11px;
167
+ overflow: hidden;
168
+ text-overflow: ellipsis;
169
+ white-space: nowrap;
170
+ }
171
+
172
+ .model-item-badge {
118
173
  color: var(--accent);
174
+ font-size: 11px;
119
175
  font-weight: 600;
176
+ flex-shrink: 0;
177
+ letter-spacing: 0.04em;
178
+ text-transform: uppercase;
120
179
  }
121
180
 
122
181
  /* Session menu */
@@ -163,6 +222,20 @@ h1 {
163
222
  margin-bottom: 8px;
164
223
  }
165
224
 
225
+ .session-autosave-row {
226
+ display: flex;
227
+ align-items: center;
228
+ gap: 8px;
229
+ margin-bottom: 8px;
230
+ font-size: 12px;
231
+ color: var(--text-dim);
232
+ cursor: pointer;
233
+ }
234
+
235
+ .session-autosave-row input {
236
+ accent-color: var(--accent);
237
+ }
238
+
166
239
  .dropdown-divider {
167
240
  height: 1px;
168
241
  background: var(--border);
@@ -216,10 +289,8 @@ h1 {
216
289
  .session-item {
217
290
  display: flex;
218
291
  align-items: center;
219
- justify-content: space-between;
220
- padding: 6px 8px;
292
+ gap: 8px;
221
293
  border-radius: 4px;
222
- cursor: pointer;
223
294
  font-size: 12px;
224
295
  transition: background 0.15s;
225
296
  }
@@ -233,6 +304,28 @@ h1 {
233
304
  outline: 1px solid rgba(122, 162, 247, 0.35);
234
305
  }
235
306
 
307
+ .session-load-btn {
308
+ flex: 1;
309
+ display: flex;
310
+ align-items: center;
311
+ justify-content: space-between;
312
+ gap: 8px;
313
+ width: 100%;
314
+ padding: 6px 8px;
315
+ background: transparent;
316
+ border: none;
317
+ color: inherit;
318
+ font: inherit;
319
+ text-align: left;
320
+ cursor: pointer;
321
+ }
322
+
323
+ .session-load-btn:focus-visible,
324
+ .session-delete-btn:focus-visible {
325
+ outline: 1px solid var(--accent);
326
+ outline-offset: 1px;
327
+ }
328
+
236
329
  .session-label {
237
330
  color: var(--text);
238
331
  overflow: hidden;
@@ -251,6 +344,26 @@ h1 {
251
344
  color: var(--accent);
252
345
  }
253
346
 
347
+ .session-delete-btn {
348
+ flex-shrink: 0;
349
+ margin: 4px 6px 4px 0;
350
+ padding: 4px 8px;
351
+ background: transparent;
352
+ border: 1px solid var(--border);
353
+ border-radius: 4px;
354
+ color: var(--text-dim);
355
+ font-family: var(--font-mono);
356
+ font-size: 11px;
357
+ cursor: pointer;
358
+ transition: color 0.15s, border-color 0.15s, background 0.15s;
359
+ }
360
+
361
+ .session-delete-btn:hover {
362
+ color: var(--red);
363
+ border-color: rgba(247, 118, 142, 0.55);
364
+ background: rgba(247, 118, 142, 0.08);
365
+ }
366
+
254
367
  .badge {
255
368
  font-size: 11px;
256
369
  padding: 2px 8px;
@@ -363,11 +476,17 @@ h1 {
363
476
  margin-bottom: 16px;
364
477
  }
365
478
 
366
- .config-overlay-spotlight {
479
+ .config-overlay-shortcuts {
367
480
  display: flex;
368
481
  flex-direction: column;
369
482
  gap: 12px;
370
483
  margin-bottom: 16px;
484
+ }
485
+
486
+ .config-overlay-spotlight {
487
+ display: flex;
488
+ flex-direction: column;
489
+ gap: 12px;
371
490
  padding: 14px 16px;
372
491
  border-radius: 8px;
373
492
  border: 1px solid rgba(122, 162, 247, 0.35);
@@ -377,6 +496,13 @@ h1 {
377
496
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
378
497
  }
379
498
 
499
+ .config-overlay-spotlight-secondary {
500
+ border-color: rgba(158, 206, 106, 0.24);
501
+ background:
502
+ linear-gradient(135deg, rgba(158, 206, 106, 0.14), rgba(122, 162, 247, 0.06)),
503
+ var(--bg-surface);
504
+ }
505
+
380
506
  .config-overlay-spotlight-copy {
381
507
  display: flex;
382
508
  flex-direction: column;
@@ -1188,6 +1314,31 @@ footer {
1188
1314
  flex-shrink: 0;
1189
1315
  }
1190
1316
 
1317
+ .settings-provider-shortcuts {
1318
+ display: flex;
1319
+ align-items: flex-start;
1320
+ justify-content: space-between;
1321
+ gap: 16px;
1322
+ margin-bottom: 16px;
1323
+ padding: 14px 16px;
1324
+ border-radius: 10px;
1325
+ border: 1px solid var(--border);
1326
+ background: rgba(26, 27, 38, 0.72);
1327
+ }
1328
+
1329
+ .settings-provider-shortcuts-copy {
1330
+ display: flex;
1331
+ flex-direction: column;
1332
+ gap: 4px;
1333
+ }
1334
+
1335
+ .settings-provider-shortcuts-actions {
1336
+ display: flex;
1337
+ gap: 8px;
1338
+ flex-wrap: wrap;
1339
+ justify-content: flex-end;
1340
+ }
1341
+
1191
1342
  .openrouter-connect-body {
1192
1343
  padding: 20px;
1193
1344
  display: flex;
@@ -1240,11 +1391,48 @@ footer {
1240
1391
  color: var(--text);
1241
1392
  }
1242
1393
 
1394
+ .provider-input-group {
1395
+ display: flex;
1396
+ flex-direction: column;
1397
+ gap: 6px;
1398
+ }
1399
+
1400
+ .provider-input-label {
1401
+ color: var(--text);
1402
+ font-size: 12px;
1403
+ font-weight: 500;
1404
+ }
1405
+
1406
+ .provider-input {
1407
+ width: 100%;
1408
+ background: var(--bg);
1409
+ border: 1px solid var(--border);
1410
+ border-radius: 6px;
1411
+ color: var(--text);
1412
+ font-family: var(--font-mono);
1413
+ font-size: 12px;
1414
+ padding: 9px 10px;
1415
+ }
1416
+
1417
+ .provider-input:focus {
1418
+ outline: none;
1419
+ border-color: var(--accent);
1420
+ }
1421
+
1243
1422
  @media (max-width: 760px) {
1244
1423
  .settings-session-banner {
1245
1424
  flex-direction: column;
1246
1425
  align-items: flex-start;
1247
1426
  }
1427
+
1428
+ .settings-provider-shortcuts {
1429
+ flex-direction: column;
1430
+ }
1431
+
1432
+ .settings-provider-shortcuts-actions {
1433
+ width: 100%;
1434
+ justify-content: flex-start;
1435
+ }
1248
1436
  }
1249
1437
 
1250
1438
  .settings-list::-webkit-scrollbar,
@@ -59,6 +59,21 @@ function createEchoTool() {
59
59
  execute: async (input) => `echo:${String(input.value)}`,
60
60
  };
61
61
  }
62
+ function assertToolCallTranscriptIsComplete(messages) {
63
+ for (let i = 0; i < messages.length; i += 1) {
64
+ const message = messages[i];
65
+ if (message?.role !== "assistant" || !message.toolCalls?.length) {
66
+ continue;
67
+ }
68
+ const toolCalls = message.toolCalls;
69
+ for (let offset = 0; offset < toolCalls.length; offset += 1) {
70
+ const toolCall = toolCalls[offset];
71
+ const toolResult = messages[i + offset + 1];
72
+ assert.equal(toolResult?.role, "tool");
73
+ assert.equal(toolResult?.role === "tool" ? toolResult.toolCallId : undefined, toolCall.id);
74
+ }
75
+ }
76
+ }
62
77
  test("agent executes tool calls and returns final assistant text", async () => {
63
78
  const responses = [
64
79
  {
@@ -96,6 +111,7 @@ test("agent stops on repeated identical tool calls", async () => {
96
111
  });
97
112
  const { text } = await agent.runTurn("Do something");
98
113
  assert.match(text, /repeated identical tool calls/);
114
+ assertToolCallTranscriptIsComplete(agent.getSession().getMessages());
99
115
  });
100
116
  test("agent tells the user how to continue when the turn call limit is reached", async () => {
101
117
  const config = createTestAgentConfig("/tmp");
@@ -11,7 +11,7 @@ import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
11
11
  import os from "node:os";
12
12
  import path from "node:path";
13
13
  import { afterEach, describe, test } from "node:test";
14
- import { loadAgentConfig, resolveConfigEnv, getConfigSetupMessage, getConfigMissing, } from "../src/agent/config.js";
14
+ import { loadAgentConfig, resolveConfigEnv, getConfigSetupMessage, getConfiguredProvider, getConfigMissing, } from "../src/agent/config.js";
15
15
  import { buildStructuredConfigPayload } from "../src/agent/editable-config.js";
16
16
  import { createRequestHandler } from "../src/serve/server.js";
17
17
  import { AgentBridge } from "../src/serve/agent-bridge.js";
@@ -637,6 +637,40 @@ describe("/api/status needsSetup", () => {
637
637
  assert.equal(body.model, "my-test-model");
638
638
  assert.equal(body.provider, "openai-compatible");
639
639
  });
640
+ test("status endpoint reports no configured provider for a fresh install using defaults", async () => {
641
+ const baseDir = await mkdtemp(path.join(os.tmpdir(), "minicode-integ-"));
642
+ tempDirs.push(baseDir);
643
+ const minicodeHome = path.join(baseDir, "new-home");
644
+ const config = {
645
+ ...createTestAgentConfig("/tmp"),
646
+ modelProvider: "openai-compatible",
647
+ model: "",
648
+ openAiBaseUrl: "http://localhost:1234/v1",
649
+ };
650
+ const bridge = new ConfigurableBridge(config);
651
+ const base = await startServer(bridge, { minicodeHome });
652
+ const res = await fetch(`${base}/api/status`);
653
+ const body = await res.json();
654
+ assert.equal(body.configuredProvider, null);
655
+ assert.equal(body.needsSetup, true);
656
+ assert.ok(body.missing.some((m) => m.includes("MODEL")));
657
+ });
658
+ test("status endpoint reports openai-compatible when localhost is explicitly configured", async () => {
659
+ const minicodeHome = await createTestHome({
660
+ config: {
661
+ modelProvider: "openai-compatible",
662
+ openAiBaseUrl: "http://localhost:1234/v1",
663
+ },
664
+ });
665
+ const config = await loadAgentConfig("/tmp", { minicodeHome });
666
+ const bridge = new ConfigurableBridge(config);
667
+ const base = await startServer(bridge, { minicodeHome });
668
+ const res = await fetch(`${base}/api/status`);
669
+ const body = await res.json();
670
+ assert.equal(body.configuredProvider, "openai-compatible");
671
+ assert.equal(body.needsSetup, true);
672
+ assert.ok(body.missing.some((m) => m.includes("MODEL")));
673
+ });
640
674
  });
641
675
  // ═══════════════════════════════════════════════════════════════════
642
676
  // /api/context — graceful degradation when agent not ready
@@ -823,3 +857,59 @@ describe("realistic user scenarios", () => {
823
857
  });
824
858
  });
825
859
  });
860
+ describe("getConfiguredProvider", () => {
861
+ test("returns null for a fresh install using only fallback localhost defaults", async () => {
862
+ const base = await mkdtemp(path.join(os.tmpdir(), "minicode-integ-"));
863
+ tempDirs.push(base);
864
+ const minicodeHome = path.join(base, "new-home");
865
+ await withEnv({
866
+ MODEL: undefined,
867
+ MODEL_PROVIDER: undefined,
868
+ OPENAI_BASE_URL: undefined,
869
+ OPENAI_API_KEY: undefined,
870
+ OPENROUTER_API_KEY: undefined,
871
+ ANTHROPIC_API_KEY: undefined,
872
+ }, async () => {
873
+ const config = await loadAgentConfig("/tmp", { minicodeHome });
874
+ const resolvedEnv = await resolveConfigEnv({ minicodeHome });
875
+ assert.equal(getConfiguredProvider(config, resolvedEnv.values), null);
876
+ });
877
+ });
878
+ test("returns openrouter when OpenRouter is explicitly configured", async () => {
879
+ const home = await createTestHome({
880
+ config: {
881
+ modelProvider: "openai-compatible",
882
+ openAiBaseUrl: "https://openrouter.ai/api/v1",
883
+ },
884
+ dotenv: "OPENROUTER_API_KEY=sk-or-test-key\n",
885
+ });
886
+ await withEnv({
887
+ MODEL_PROVIDER: undefined,
888
+ OPENAI_BASE_URL: undefined,
889
+ OPENAI_API_KEY: undefined,
890
+ OPENROUTER_API_KEY: undefined,
891
+ }, async () => {
892
+ const config = await loadAgentConfig("/tmp", { minicodeHome: home });
893
+ const resolvedEnv = await resolveConfigEnv({ minicodeHome: home });
894
+ assert.equal(getConfiguredProvider(config, resolvedEnv.values), "openrouter");
895
+ });
896
+ });
897
+ test("returns openai-compatible when a local endpoint is explicitly configured", async () => {
898
+ const home = await createTestHome({
899
+ config: {
900
+ modelProvider: "openai-compatible",
901
+ openAiBaseUrl: "http://localhost:1234/v1",
902
+ },
903
+ });
904
+ await withEnv({
905
+ MODEL_PROVIDER: undefined,
906
+ OPENAI_BASE_URL: undefined,
907
+ OPENAI_API_KEY: undefined,
908
+ OPENROUTER_API_KEY: undefined,
909
+ }, async () => {
910
+ const config = await loadAgentConfig("/tmp", { minicodeHome: home });
911
+ const resolvedEnv = await resolveConfigEnv({ minicodeHome: home });
912
+ assert.equal(getConfiguredProvider(config, resolvedEnv.values), "openai-compatible");
913
+ });
914
+ });
915
+ });
@@ -1,11 +1,14 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { test, afterEach } from "node:test";
3
3
  import { createServer } from "node:http";
4
+ import { readFileSync } from "node:fs";
5
+ import { join } from "node:path";
4
6
  import { createRequestHandler } from "../src/serve/server.js";
5
7
  import { AgentBridge } from "../src/serve/agent-bridge.js";
6
8
  import { CodingAgent, ToolRegistry, } from "@minicode/agent-sdk";
7
9
  import { UiStore } from "../src/ui/state/ui-store.js";
8
10
  import { createTestAgentConfig } from "./test-utils.js";
11
+ const distWeb = join(import.meta.dirname, "..", "dist", "src", "web");
9
12
  // ── Mock model client ──
10
13
  class SequenceModelClient {
11
14
  responses;
@@ -111,6 +114,12 @@ test("GET /api/context reflects updated context state", async () => {
111
114
  assert.equal(body.contextTokens, 8000);
112
115
  assert.equal(body.maxContextTokens, 16000);
113
116
  });
117
+ test("built web UI explains that context size can be adjusted in Settings", () => {
118
+ const html = readFileSync(join(distWeb, "index.html"), "utf8");
119
+ const js = readFileSync(join(distWeb, "app.js"), "utf8");
120
+ assert.ok(html.includes("Context window usage. Adjust context size in Settings"), "HTML should provide a helpful default tooltip for the context indicator");
121
+ assert.ok(js.includes("Adjust context size in Settings if you want it larger or smaller."), "JS should include guidance about adjusting context size in Settings");
122
+ });
114
123
  // ── Agent emits context_status UiUpdate ──
115
124
  test("agent emits context_status UiUpdate during turn", async () => {
116
125
  const config = createTestAgentConfig("/tmp/test-workspace");
@@ -91,6 +91,18 @@ test("run_command refresh removes deleted files from the index", async () => {
91
91
  await runTool.execute({ command: "rm src/util.ts" });
92
92
  assert.equal(index.getSymbol("add"), undefined, "deleted file should be removed from the index");
93
93
  });
94
+ test("run_command times out commands that ignore SIGTERM", async () => {
95
+ const workspaceRoot = await createTempWorkspace();
96
+ const config = createTestAgentConfig(workspaceRoot);
97
+ config.commandTimeoutMs = 100;
98
+ const runTool = createRunCommandTool(config);
99
+ const start = Date.now();
100
+ const output = await runTool.execute({
101
+ command: `node -e 'process.on("SIGTERM", () => {}); setInterval(() => {}, 1000)'`,
102
+ });
103
+ assert.match(output, /timed_out: true/);
104
+ assert.ok(Date.now() - start < 5_000, "run_command should return after the timeout and kill grace period");
105
+ });
94
106
  test("read_file supports negative offset and line limits", async () => {
95
107
  const workspaceRoot = await createTempWorkspace();
96
108
  const filePath = path.join(workspaceRoot, "lines.txt");