@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.
- package/dist/src/agent/config.js +25 -0
- package/dist/src/model-utils.js +18 -1
- package/dist/src/serve/agent-bridge.js +89 -15
- package/dist/src/serve/server.js +151 -3
- package/dist/src/session/session-store.js +29 -1
- package/dist/src/web/app.js +691 -105
- package/dist/src/web/index.html +117 -9
- package/dist/src/web/style.css +198 -10
- package/dist/tests/agent.test.js +16 -0
- package/dist/tests/config-integration.test.js +91 -1
- package/dist/tests/context-indicator.test.js +9 -0
- package/dist/tests/file-tools.test.js +12 -0
- package/dist/tests/graph-onboarding.test.js +8 -0
- package/dist/tests/model-client-openai.test.js +41 -0
- package/dist/tests/model-dropdown-ui.test.js +23 -0
- package/dist/tests/model-utils.test.js +26 -1
- package/dist/tests/serve.integration.test.js +194 -0
- package/dist/tests/session-store.test.js +32 -1
- package/dist/tests/session-ui.test.js +6 -0
- package/dist/tests/settings-ui.test.js +11 -0
- package/dist/tests/setup-overlay-state.test.js +49 -0
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js +10 -1
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/model/client.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/model/client.js +21 -0
- package/node_modules/@minicode/agent-sdk/dist/src/model/client.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.js +60 -6
- package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
package/dist/src/web/index.html
CHANGED
|
@@ -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-
|
|
65
|
-
<div class="config-overlay-spotlight
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
<
|
|
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">
|
package/dist/src/web/style.css
CHANGED
|
@@ -90,24 +90,51 @@ h1 {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
#model-dropdown {
|
|
93
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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,
|
package/dist/tests/agent.test.js
CHANGED
|
@@ -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");
|