@sean.holung/minicode 0.3.8 → 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/serve/agent-bridge.js +4 -1
- package/dist/src/serve/server.js +14 -0
- package/dist/src/session/session-store.js +11 -1
- package/dist/src/web/app.js +140 -23
- package/dist/src/web/index.html +5 -1
- package/dist/src/web/style.css +57 -3
- package/dist/tests/context-indicator.test.js +9 -0
- package/dist/tests/serve.integration.test.js +31 -0
- package/dist/tests/session-store.test.js +18 -1
- package/dist/tests/session-ui.test.js +6 -0
- package/package.json +1 -1
|
@@ -8,7 +8,7 @@ import { computeFileHashes, getWorkspaceCacheDir, loadIndex, saveIndex, } from "
|
|
|
8
8
|
import { buildProjectIndex } from "../indexer/project-index.js";
|
|
9
9
|
import { sortModelsAlphabetically } from "../model-utils.js";
|
|
10
10
|
import { createToolRegistry } from "../tools/registry.js";
|
|
11
|
-
import { listSessions, loadSession, loadSessionByLabel, saveSession, } from "../session/session-store.js";
|
|
11
|
+
import { deleteSession, listSessions, loadSession, loadSessionByLabel, saveSession, } from "../session/session-store.js";
|
|
12
12
|
import { getSymbolDisplayName } from "../indexer/symbol-names.js";
|
|
13
13
|
export class AgentBridge {
|
|
14
14
|
agent;
|
|
@@ -349,6 +349,9 @@ export class AgentBridge {
|
|
|
349
349
|
async listSess() {
|
|
350
350
|
return listSessions();
|
|
351
351
|
}
|
|
352
|
+
async deleteSess(sessionId) {
|
|
353
|
+
return deleteSession(sessionId);
|
|
354
|
+
}
|
|
352
355
|
// ── Project index queries ──
|
|
353
356
|
hasIndex() {
|
|
354
357
|
return this.projectIndex !== undefined;
|
package/dist/src/serve/server.js
CHANGED
|
@@ -473,6 +473,20 @@ export function createRequestHandler(bridge, emit, options = {}) {
|
|
|
473
473
|
});
|
|
474
474
|
return;
|
|
475
475
|
}
|
|
476
|
+
if (pathname.startsWith("/api/sessions/") && method === "DELETE") {
|
|
477
|
+
const sessionId = decodeURIComponent(pathname.slice("/api/sessions/".length));
|
|
478
|
+
if (!sessionId) {
|
|
479
|
+
sendJson(res, 400, { error: "Session id is required" });
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const deleted = await bridge.deleteSess(sessionId);
|
|
483
|
+
if (!deleted) {
|
|
484
|
+
sendJson(res, 404, { error: "Session not found" });
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
sendJson(res, 200, { ok: true, deleted: true, id: sessionId });
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
476
490
|
// ── Graph / Index API ──
|
|
477
491
|
if (pathname === "/api/symbols" && method === "GET") {
|
|
478
492
|
if (!bridge.hasIndex()) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { Session } from "@minicode/agent-sdk";
|
|
@@ -100,3 +100,13 @@ export async function loadSessionByLabel(label) {
|
|
|
100
100
|
return undefined;
|
|
101
101
|
return loadSession(match.id);
|
|
102
102
|
}
|
|
103
|
+
export async function deleteSession(sessionId) {
|
|
104
|
+
const filePath = path.join(sessionsDir, `${sessionId}.json`);
|
|
105
|
+
try {
|
|
106
|
+
await unlink(filePath);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
package/dist/src/web/app.js
CHANGED
|
@@ -2950,6 +2950,7 @@ var sessionDropdown = document.getElementById("session-dropdown");
|
|
|
2950
2950
|
var sessionList = document.getElementById("session-list");
|
|
2951
2951
|
var sessionUpdateRow = document.getElementById("session-update-row");
|
|
2952
2952
|
var sessionUpdateBtn = document.getElementById("session-update-btn");
|
|
2953
|
+
var sessionAutoSaveToggle = document.getElementById("session-autosave-toggle");
|
|
2953
2954
|
var saveBtn = document.getElementById("save-btn");
|
|
2954
2955
|
var saveLabelInput = document.getElementById("save-label");
|
|
2955
2956
|
var contextFill = document.getElementById("context-fill");
|
|
@@ -3010,6 +3011,8 @@ var sessionRefreshTracker = createLatestRequestTracker();
|
|
|
3010
3011
|
var TOOL_RESULT_MAX = 500;
|
|
3011
3012
|
var OPENROUTER_PKCE_VERIFIER_KEY = "minicode:openrouter:pkce-verifier";
|
|
3012
3013
|
var OPENROUTER_PERSIST_TO_ENV_KEY = "minicode:openrouter:persist-to-env";
|
|
3014
|
+
var SESSION_AUTOSAVE_KEY = "minicode:session:auto-save";
|
|
3015
|
+
var SESSION_AUTOSAVE_LABEL_PREFIX = "Autosave";
|
|
3013
3016
|
var OPENAI_COMPATIBLE_PRESETS = {
|
|
3014
3017
|
lmstudio: {
|
|
3015
3018
|
baseUrl: "http://localhost:1234/v1",
|
|
@@ -3032,6 +3035,11 @@ var OPENAI_COMPATIBLE_PRESETS = {
|
|
|
3032
3035
|
apiKeyPlaceholder: "Add an API key only if this endpoint requires auth"
|
|
3033
3036
|
}
|
|
3034
3037
|
};
|
|
3038
|
+
var sessionAutoSaveEnabled = loadSessionAutoSavePreference();
|
|
3039
|
+
var pendingAutoSaveLabel = null;
|
|
3040
|
+
var autoSaveInFlight = null;
|
|
3041
|
+
var autoSaveQueued = false;
|
|
3042
|
+
sessionAutoSaveToggle.checked = sessionAutoSaveEnabled;
|
|
3035
3043
|
function connect() {
|
|
3036
3044
|
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
|
3037
3045
|
ws = new WebSocket(`${protocol}//${location.host}`);
|
|
@@ -3422,6 +3430,7 @@ function handleServerMessage(msg) {
|
|
|
3422
3430
|
if (msg.usage) {
|
|
3423
3431
|
addUsageInfo(msg.usage);
|
|
3424
3432
|
}
|
|
3433
|
+
queueSessionAutoSave();
|
|
3425
3434
|
break;
|
|
3426
3435
|
case "error":
|
|
3427
3436
|
addMessage(`Error: ${msg.message || ""}`, "error");
|
|
@@ -3568,7 +3577,8 @@ function updateContextIndicator(contextTokens, maxContextTokens) {
|
|
|
3568
3577
|
}
|
|
3569
3578
|
contextLabel.textContent = pct + "%";
|
|
3570
3579
|
const indicator = document.getElementById("context-indicator");
|
|
3571
|
-
indicator.title = `Context: ~${contextTokens.toLocaleString()} / ${maxContextTokens.toLocaleString()} tokens (${pct}%)
|
|
3580
|
+
indicator.title = `Context: ~${contextTokens.toLocaleString()} / ${maxContextTokens.toLocaleString()} tokens (${pct}%)
|
|
3581
|
+
Adjust context size in Settings if you want it larger or smaller.`;
|
|
3572
3582
|
}
|
|
3573
3583
|
async function fetchContext() {
|
|
3574
3584
|
try {
|
|
@@ -4040,23 +4050,122 @@ document.addEventListener("click", (e) => {
|
|
|
4040
4050
|
sessionDropdown.classList.add("hidden");
|
|
4041
4051
|
}
|
|
4042
4052
|
});
|
|
4053
|
+
function loadSessionAutoSavePreference() {
|
|
4054
|
+
try {
|
|
4055
|
+
return localStorage.getItem(SESSION_AUTOSAVE_KEY) === "1";
|
|
4056
|
+
} catch {
|
|
4057
|
+
return false;
|
|
4058
|
+
}
|
|
4059
|
+
}
|
|
4060
|
+
function persistSessionAutoSavePreference(enabled) {
|
|
4061
|
+
try {
|
|
4062
|
+
if (enabled) {
|
|
4063
|
+
localStorage.setItem(SESSION_AUTOSAVE_KEY, "1");
|
|
4064
|
+
} else {
|
|
4065
|
+
localStorage.removeItem(SESSION_AUTOSAVE_KEY);
|
|
4066
|
+
}
|
|
4067
|
+
} catch {
|
|
4068
|
+
}
|
|
4069
|
+
}
|
|
4070
|
+
function buildAutoSaveLabel() {
|
|
4071
|
+
return `${SESSION_AUTOSAVE_LABEL_PREFIX} ${(/* @__PURE__ */ new Date()).toLocaleString()}`;
|
|
4072
|
+
}
|
|
4073
|
+
async function persistCurrentSession(label) {
|
|
4074
|
+
const res = await fetch("/api/sessions/save", {
|
|
4075
|
+
method: "POST",
|
|
4076
|
+
headers: { "Content-Type": "application/json" },
|
|
4077
|
+
body: JSON.stringify({ label })
|
|
4078
|
+
});
|
|
4079
|
+
const body = await res.json();
|
|
4080
|
+
if (!res.ok) {
|
|
4081
|
+
throw new Error("error" in body ? body.error : `Failed to save session (${res.status})`);
|
|
4082
|
+
}
|
|
4083
|
+
return body;
|
|
4084
|
+
}
|
|
4085
|
+
async function deleteSavedSession(session) {
|
|
4086
|
+
const isCurrentSavedSession = activeSavedSession?.id === session.id;
|
|
4087
|
+
const confirmed = window.confirm(`Delete saved session "${session.label}"?`);
|
|
4088
|
+
if (!confirmed) {
|
|
4089
|
+
return;
|
|
4090
|
+
}
|
|
4091
|
+
try {
|
|
4092
|
+
const res = await fetch(`/api/sessions/${encodeURIComponent(session.id)}`, {
|
|
4093
|
+
method: "DELETE"
|
|
4094
|
+
});
|
|
4095
|
+
const body = await res.json();
|
|
4096
|
+
if (!res.ok) {
|
|
4097
|
+
throw new Error("error" in body ? body.error : `Failed to delete session (${res.status})`);
|
|
4098
|
+
}
|
|
4099
|
+
if (isCurrentSavedSession) {
|
|
4100
|
+
activeSavedSession = null;
|
|
4101
|
+
}
|
|
4102
|
+
if (pendingAutoSaveLabel === session.label) {
|
|
4103
|
+
pendingAutoSaveLabel = null;
|
|
4104
|
+
}
|
|
4105
|
+
addMessage(
|
|
4106
|
+
isCurrentSavedSession ? `Deleted saved session "${session.label}". The current chat stays open until you load another session or refresh.` : `Session deleted: "${session.label}"`,
|
|
4107
|
+
"thinking"
|
|
4108
|
+
);
|
|
4109
|
+
await refreshSessionList();
|
|
4110
|
+
} catch (error) {
|
|
4111
|
+
const message = error instanceof Error ? error.message : "Failed to delete session";
|
|
4112
|
+
addMessage(message, "error");
|
|
4113
|
+
}
|
|
4114
|
+
}
|
|
4115
|
+
async function maybeAutoSaveSession() {
|
|
4116
|
+
if (!sessionAutoSaveEnabled) {
|
|
4117
|
+
return;
|
|
4118
|
+
}
|
|
4119
|
+
const label = activeSavedSession?.label ?? pendingAutoSaveLabel ?? buildAutoSaveLabel();
|
|
4120
|
+
try {
|
|
4121
|
+
const data = await persistCurrentSession(label);
|
|
4122
|
+
pendingAutoSaveLabel = data.label;
|
|
4123
|
+
await refreshSessionList();
|
|
4124
|
+
} catch (error) {
|
|
4125
|
+
const message = error instanceof Error ? error.message : "Failed to auto-save session";
|
|
4126
|
+
addMessage(message, "error");
|
|
4127
|
+
}
|
|
4128
|
+
}
|
|
4129
|
+
function queueSessionAutoSave() {
|
|
4130
|
+
if (!sessionAutoSaveEnabled) {
|
|
4131
|
+
return;
|
|
4132
|
+
}
|
|
4133
|
+
if (autoSaveInFlight) {
|
|
4134
|
+
autoSaveQueued = true;
|
|
4135
|
+
return;
|
|
4136
|
+
}
|
|
4137
|
+
autoSaveInFlight = (async () => {
|
|
4138
|
+
await maybeAutoSaveSession();
|
|
4139
|
+
})();
|
|
4140
|
+
void autoSaveInFlight.finally(() => {
|
|
4141
|
+
autoSaveInFlight = null;
|
|
4142
|
+
if (autoSaveQueued) {
|
|
4143
|
+
autoSaveQueued = false;
|
|
4144
|
+
queueSessionAutoSave();
|
|
4145
|
+
}
|
|
4146
|
+
});
|
|
4147
|
+
}
|
|
4148
|
+
sessionAutoSaveToggle.addEventListener("change", () => {
|
|
4149
|
+
sessionAutoSaveEnabled = sessionAutoSaveToggle.checked;
|
|
4150
|
+
persistSessionAutoSavePreference(sessionAutoSaveEnabled);
|
|
4151
|
+
if (sessionAutoSaveEnabled) {
|
|
4152
|
+
addMessage(
|
|
4153
|
+
activeSavedSession ? `Auto-save enabled. minicode will update "${activeSavedSession.label}" after each completed turn.` : "Auto-save enabled. minicode will save this chat after the next completed turn.",
|
|
4154
|
+
"thinking"
|
|
4155
|
+
);
|
|
4156
|
+
} else {
|
|
4157
|
+
addMessage("Auto-save disabled.", "thinking");
|
|
4158
|
+
}
|
|
4159
|
+
});
|
|
4043
4160
|
saveBtn.addEventListener("click", async () => {
|
|
4044
4161
|
const requestedLabel = saveLabelInput.value.trim();
|
|
4045
4162
|
const label = requestedLabel || activeSavedSession?.label || void 0;
|
|
4046
4163
|
const isUpdatingCurrentSession = !!activeSavedSession && (requestedLabel.length === 0 || requestedLabel === activeSavedSession.label);
|
|
4047
4164
|
try {
|
|
4048
4165
|
saveBtn.setAttribute("disabled", "true");
|
|
4049
|
-
const
|
|
4050
|
-
method: "POST",
|
|
4051
|
-
headers: { "Content-Type": "application/json" },
|
|
4052
|
-
body: JSON.stringify({ label })
|
|
4053
|
-
});
|
|
4054
|
-
const body = await res.json();
|
|
4055
|
-
if (!res.ok) {
|
|
4056
|
-
throw new Error("error" in body ? body.error : `Failed to save session (${res.status})`);
|
|
4057
|
-
}
|
|
4058
|
-
const data = body;
|
|
4166
|
+
const data = await persistCurrentSession(label);
|
|
4059
4167
|
saveLabelInput.value = "";
|
|
4168
|
+
pendingAutoSaveLabel = data.label;
|
|
4060
4169
|
addMessage(
|
|
4061
4170
|
`${isUpdatingCurrentSession ? "Session updated" : "Session saved"}: "${data.label}"`,
|
|
4062
4171
|
"thinking"
|
|
@@ -4086,6 +4195,7 @@ async function refreshSessionList() {
|
|
|
4086
4195
|
const sessions = data.sessions;
|
|
4087
4196
|
activeSavedSession = sessions.find((session) => session.id === data.currentSessionId) ?? null;
|
|
4088
4197
|
if (activeSavedSession) {
|
|
4198
|
+
pendingAutoSaveLabel = activeSavedSession.label;
|
|
4089
4199
|
sessionUpdateRow.classList.remove("hidden");
|
|
4090
4200
|
sessionUpdateBtn.textContent = `Update "${activeSavedSession.label}"`;
|
|
4091
4201
|
sessionUpdateBtn.title = `Save changes back to "${activeSavedSession.label}"`;
|
|
@@ -4103,8 +4213,22 @@ async function refreshSessionList() {
|
|
|
4103
4213
|
const el = document.createElement("div");
|
|
4104
4214
|
const isActive = activeSavedSession?.id === s.id;
|
|
4105
4215
|
el.className = "session-item" + (isActive ? " active" : "");
|
|
4106
|
-
|
|
4107
|
-
|
|
4216
|
+
const loadBtn = document.createElement("button");
|
|
4217
|
+
loadBtn.type = "button";
|
|
4218
|
+
loadBtn.className = "session-load-btn";
|
|
4219
|
+
loadBtn.innerHTML = `<span class="session-label">${escapeHtml(s.label)}</span><span class="session-meta">${s.messageCount} msgs${isActive ? ' <span class="session-active-badge">\u2022 active</span>' : ""}</span>`;
|
|
4220
|
+
loadBtn.addEventListener("click", () => loadSession(s.label));
|
|
4221
|
+
const deleteBtn = document.createElement("button");
|
|
4222
|
+
deleteBtn.type = "button";
|
|
4223
|
+
deleteBtn.className = "session-delete-btn";
|
|
4224
|
+
deleteBtn.textContent = "Delete";
|
|
4225
|
+
deleteBtn.title = `Delete "${s.label}"`;
|
|
4226
|
+
deleteBtn.addEventListener("click", (event) => {
|
|
4227
|
+
event.stopPropagation();
|
|
4228
|
+
void deleteSavedSession(s);
|
|
4229
|
+
});
|
|
4230
|
+
el.appendChild(loadBtn);
|
|
4231
|
+
el.appendChild(deleteBtn);
|
|
4108
4232
|
sessionList.appendChild(el);
|
|
4109
4233
|
}
|
|
4110
4234
|
} catch {
|
|
@@ -4126,6 +4250,7 @@ async function loadSession(label) {
|
|
|
4126
4250
|
if (res.ok) {
|
|
4127
4251
|
const body = await res.json();
|
|
4128
4252
|
sessionDropdown.classList.add("hidden");
|
|
4253
|
+
pendingAutoSaveLabel = body.label;
|
|
4129
4254
|
renderLoadedSessionMessages(body.messages);
|
|
4130
4255
|
if (body.messages.length === 0) {
|
|
4131
4256
|
addMessage(`Session "${body.label}" restored`, "thinking");
|
|
@@ -4141,16 +4266,8 @@ sessionUpdateBtn.addEventListener("click", async () => {
|
|
|
4141
4266
|
}
|
|
4142
4267
|
try {
|
|
4143
4268
|
sessionUpdateBtn.disabled = true;
|
|
4144
|
-
const
|
|
4145
|
-
|
|
4146
|
-
headers: { "Content-Type": "application/json" },
|
|
4147
|
-
body: JSON.stringify({ label: activeSavedSession.label })
|
|
4148
|
-
});
|
|
4149
|
-
const body = await res.json();
|
|
4150
|
-
if (!res.ok) {
|
|
4151
|
-
throw new Error("error" in body ? body.error : `Failed to update session (${res.status})`);
|
|
4152
|
-
}
|
|
4153
|
-
const data = body;
|
|
4269
|
+
const data = await persistCurrentSession(activeSavedSession.label);
|
|
4270
|
+
pendingAutoSaveLabel = data.label;
|
|
4154
4271
|
addMessage(`Session updated: "${data.label}"`, "thinking");
|
|
4155
4272
|
await refreshSessionList();
|
|
4156
4273
|
} catch (error) {
|
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>
|
|
@@ -45,6 +45,10 @@
|
|
|
45
45
|
<div id="session-update-row" class="session-update-row hidden">
|
|
46
46
|
<button id="session-update-btn" class="dropdown-action" type="button">Update current saved session</button>
|
|
47
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>
|
|
48
52
|
<div class="dropdown-row">
|
|
49
53
|
<input id="save-label" type="text" placeholder="Label (optional)" />
|
|
50
54
|
<button id="save-btn" class="dropdown-action">Save</button>
|
package/dist/src/web/style.css
CHANGED
|
@@ -222,6 +222,20 @@ h1 {
|
|
|
222
222
|
margin-bottom: 8px;
|
|
223
223
|
}
|
|
224
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
|
+
|
|
225
239
|
.dropdown-divider {
|
|
226
240
|
height: 1px;
|
|
227
241
|
background: var(--border);
|
|
@@ -275,10 +289,8 @@ h1 {
|
|
|
275
289
|
.session-item {
|
|
276
290
|
display: flex;
|
|
277
291
|
align-items: center;
|
|
278
|
-
|
|
279
|
-
padding: 6px 8px;
|
|
292
|
+
gap: 8px;
|
|
280
293
|
border-radius: 4px;
|
|
281
|
-
cursor: pointer;
|
|
282
294
|
font-size: 12px;
|
|
283
295
|
transition: background 0.15s;
|
|
284
296
|
}
|
|
@@ -292,6 +304,28 @@ h1 {
|
|
|
292
304
|
outline: 1px solid rgba(122, 162, 247, 0.35);
|
|
293
305
|
}
|
|
294
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
|
+
|
|
295
329
|
.session-label {
|
|
296
330
|
color: var(--text);
|
|
297
331
|
overflow: hidden;
|
|
@@ -310,6 +344,26 @@ h1 {
|
|
|
310
344
|
color: var(--accent);
|
|
311
345
|
}
|
|
312
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
|
+
|
|
313
367
|
.badge {
|
|
314
368
|
font-size: 11px;
|
|
315
369
|
padding: 2px 8px;
|
|
@@ -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");
|
|
@@ -24,6 +24,7 @@ class MockBridge extends AgentBridge {
|
|
|
24
24
|
openAiCompatibleApiKey;
|
|
25
25
|
openAiCompatibleSessionActive = false;
|
|
26
26
|
refreshIndexCount = 0;
|
|
27
|
+
deletedSessionIds = [];
|
|
27
28
|
constructor() {
|
|
28
29
|
super(() => { }, false);
|
|
29
30
|
}
|
|
@@ -89,6 +90,13 @@ class MockBridge extends AgentBridge {
|
|
|
89
90
|
}
|
|
90
91
|
return { session, label };
|
|
91
92
|
}
|
|
93
|
+
async deleteSess(sessionId) {
|
|
94
|
+
if (sessionId !== "sess-1") {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
this.deletedSessionIds.push(sessionId);
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
92
100
|
getCurrentSessionId() {
|
|
93
101
|
return this._currentSessionId;
|
|
94
102
|
}
|
|
@@ -536,6 +544,29 @@ test("POST /api/sessions/load returns success for known session", async () => {
|
|
|
536
544
|
assert.equal(body.messages[9]?.content, "message-11");
|
|
537
545
|
assert.ok(body.messages.every((message) => !message.content.startsWith("[Conversation Summary")));
|
|
538
546
|
});
|
|
547
|
+
test("DELETE /api/sessions/:id deletes a saved session", async () => {
|
|
548
|
+
const bridge = new MockBridge();
|
|
549
|
+
const base = await startTestServer(bridge);
|
|
550
|
+
const res = await fetch(`${base}/api/sessions/sess-1`, {
|
|
551
|
+
method: "DELETE",
|
|
552
|
+
});
|
|
553
|
+
assert.equal(res.status, 200);
|
|
554
|
+
const body = (await res.json());
|
|
555
|
+
assert.equal(body.ok, true);
|
|
556
|
+
assert.equal(body.deleted, true);
|
|
557
|
+
assert.equal(body.id, "sess-1");
|
|
558
|
+
assert.deepEqual(bridge.deletedSessionIds, ["sess-1"]);
|
|
559
|
+
});
|
|
560
|
+
test("DELETE /api/sessions/:id returns 404 for unknown session", async () => {
|
|
561
|
+
const bridge = new MockBridge();
|
|
562
|
+
const base = await startTestServer(bridge);
|
|
563
|
+
const res = await fetch(`${base}/api/sessions/missing`, {
|
|
564
|
+
method: "DELETE",
|
|
565
|
+
});
|
|
566
|
+
assert.equal(res.status, 404);
|
|
567
|
+
const body = (await res.json());
|
|
568
|
+
assert.match(body.error, /Session not found/);
|
|
569
|
+
});
|
|
539
570
|
test("POST /api/chat returns agent response", async () => {
|
|
540
571
|
const bridge = new MockBridge();
|
|
541
572
|
const base = await startTestServer(bridge);
|
|
@@ -4,7 +4,7 @@ import { mkdtemp, readdir, rm } from "node:fs/promises";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import { Session } from "@minicode/agent-sdk";
|
|
7
|
-
import { DuplicateSessionLabelError, listSessions, loadSession, loadSessionByLabel, saveSession, setSessionsDir, } from "../src/session/session-store.js";
|
|
7
|
+
import { deleteSession, DuplicateSessionLabelError, listSessions, loadSession, loadSessionByLabel, saveSession, setSessionsDir, } from "../src/session/session-store.js";
|
|
8
8
|
async function withTmpDir(fn) {
|
|
9
9
|
const dir = await mkdtemp(path.join(os.tmpdir(), "minicode-test-"));
|
|
10
10
|
setSessionsDir(dir);
|
|
@@ -127,3 +127,20 @@ test("saving same session twice overwrites the file", async () => {
|
|
|
127
127
|
assert.equal(result.session.getMessages().length, 2);
|
|
128
128
|
});
|
|
129
129
|
});
|
|
130
|
+
test("deleteSession removes the saved session file", async () => {
|
|
131
|
+
await withTmpDir(async (dir) => {
|
|
132
|
+
const session = new Session("test-id");
|
|
133
|
+
session.addMessage({ role: "user", content: "hello" });
|
|
134
|
+
await saveSession(session, "delete me");
|
|
135
|
+
const deleted = await deleteSession("test-id");
|
|
136
|
+
assert.equal(deleted, true);
|
|
137
|
+
const files = await readdir(dir);
|
|
138
|
+
assert.equal(files.length, 0);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
test("deleteSession returns false for a missing session", async () => {
|
|
142
|
+
await withTmpDir(async () => {
|
|
143
|
+
const deleted = await deleteSession("missing-session");
|
|
144
|
+
assert.equal(deleted, false);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -7,11 +7,14 @@ test("built HTML contains update action for the current saved session", () => {
|
|
|
7
7
|
const html = readFileSync(join(distWeb, "index.html"), "utf8");
|
|
8
8
|
assert.ok(html.includes('id="session-update-row"'), "HTML should contain the session update row");
|
|
9
9
|
assert.ok(html.includes('id="session-update-btn"'), "HTML should contain the session update button");
|
|
10
|
+
assert.ok(html.includes('id="session-autosave-toggle"'), "HTML should contain the auto-save sessions toggle");
|
|
10
11
|
});
|
|
11
12
|
test("built CSS contains active-session styling", () => {
|
|
12
13
|
const css = readFileSync(join(distWeb, "style.css"), "utf8");
|
|
13
14
|
assert.ok(css.includes(".session-item.active"), "CSS should style the active saved session row");
|
|
14
15
|
assert.ok(css.includes(".session-active-badge"), "CSS should style the active session badge");
|
|
16
|
+
assert.ok(css.includes(".session-delete-btn"), "CSS should style the session delete button");
|
|
17
|
+
assert.ok(css.includes(".session-autosave-row"), "CSS should style the auto-save toggle row");
|
|
15
18
|
});
|
|
16
19
|
test("built JS contains active saved session update logic", () => {
|
|
17
20
|
const js = readFileSync(join(distWeb, "app.js"), "utf8");
|
|
@@ -22,4 +25,7 @@ test("built JS contains active saved session update logic", () => {
|
|
|
22
25
|
assert.ok(js.includes('saveBtn.setAttribute("disabled", "true")'), "JS should disable saving while the first save is in flight");
|
|
23
26
|
assert.ok(js.includes("renderLoadedSessionMessages"), "JS should render session previews after load");
|
|
24
27
|
assert.ok(js.includes("body.messages"), "JS should read preview messages from the load session response");
|
|
28
|
+
assert.ok(js.includes("SESSION_AUTOSAVE_KEY"), "JS should persist the auto-save preference");
|
|
29
|
+
assert.ok(js.includes("window.confirm"), "JS should confirm before deleting a saved session");
|
|
30
|
+
assert.ok(js.includes('method: "DELETE"'), "JS should call the delete session API");
|
|
25
31
|
});
|