@sean.holung/minicode 0.3.4 → 0.3.6

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 (72) hide show
  1. package/README.md +25 -47
  2. package/dist/scripts/run-benchmarks.js +73 -28
  3. package/dist/src/agent/config.js +51 -66
  4. package/dist/src/agent/editable-config.js +50 -58
  5. package/dist/src/agent/home-env.js +74 -0
  6. package/dist/src/benchmark/runner.js +142 -59
  7. package/dist/src/cli/config-slash-command.js +15 -13
  8. package/dist/src/indexer/project-index.js +49 -13
  9. package/dist/src/serve/agent-bridge.js +99 -31
  10. package/dist/src/serve/mcp-server.js +70 -21
  11. package/dist/src/serve/server.js +198 -8
  12. package/dist/src/session/session-preview.js +14 -0
  13. package/dist/src/shared/graph-search.js +80 -0
  14. package/dist/src/shared/graph-selection.js +40 -0
  15. package/dist/src/shared/graph-symbols.js +82 -0
  16. package/dist/src/shared/symbol-resolution.js +33 -0
  17. package/dist/src/tools/find-path.js +15 -6
  18. package/dist/src/tools/find-references.js +7 -2
  19. package/dist/src/tools/get-dependencies.js +8 -3
  20. package/dist/src/tools/read-symbol.js +9 -3
  21. package/dist/src/tools/registry.js +4 -1
  22. package/dist/src/tools/search-code-map.js +18 -3
  23. package/dist/src/web/app.js +646 -87
  24. package/dist/src/web/index.html +68 -6
  25. package/dist/src/web/style.css +208 -1
  26. package/dist/tests/benchmark-harness.test.js +100 -0
  27. package/dist/tests/config-api.test.js +5 -5
  28. package/dist/tests/config-integration.test.js +130 -56
  29. package/dist/tests/config-slash-command.test.js +12 -11
  30. package/dist/tests/config.test.js +12 -4
  31. package/dist/tests/editable-config.test.js +15 -12
  32. package/dist/tests/file-tools.test.js +34 -1
  33. package/dist/tests/find-path.test.js +43 -2
  34. package/dist/tests/find-references.test.js +49 -0
  35. package/dist/tests/get-dependencies.test.js +23 -0
  36. package/dist/tests/graph-onboarding.test.js +10 -1
  37. package/dist/tests/graph-search.test.js +66 -0
  38. package/dist/tests/graph-selection.test.js +58 -0
  39. package/dist/tests/graph-symbols.test.js +45 -0
  40. package/dist/tests/home-env.test.js +56 -0
  41. package/dist/tests/indexer.test.js +6 -0
  42. package/dist/tests/read-symbol.test.js +35 -0
  43. package/dist/tests/request-tracker.test.js +15 -0
  44. package/dist/tests/run-benchmarks.test.js +117 -33
  45. package/dist/tests/search-code-map.test.js +2 -0
  46. package/dist/tests/serve.integration.test.js +338 -9
  47. package/dist/tests/session-preview.test.js +56 -0
  48. package/dist/tests/session-ui.test.js +4 -0
  49. package/dist/tests/settings-ui.test.js +18 -0
  50. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts.map +1 -1
  51. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js +2 -1
  52. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js.map +1 -1
  53. package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts +1 -1
  54. package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts.map +1 -1
  55. package/node_modules/@minicode/agent-sdk/dist/src/index.js.map +1 -1
  56. package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.d.ts +3 -0
  57. package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.d.ts.map +1 -1
  58. package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.d.ts +3 -0
  59. package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.d.ts.map +1 -1
  60. package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.js +4 -1
  61. package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.js.map +1 -1
  62. package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.d.ts +11 -1
  63. package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.d.ts.map +1 -1
  64. package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.js +4 -1
  65. package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.js.map +1 -1
  66. package/node_modules/@minicode/agent-sdk/dist/src/tools/search.d.ts.map +1 -1
  67. package/node_modules/@minicode/agent-sdk/dist/src/tools/search.js +16 -8
  68. package/node_modules/@minicode/agent-sdk/dist/src/tools/search.js.map +1 -1
  69. package/node_modules/@minicode/agent-sdk/dist/tests/file-tools.test.js +19 -2
  70. package/node_modules/@minicode/agent-sdk/dist/tests/file-tools.test.js.map +1 -1
  71. package/node_modules/@minicode/agent-sdk/dist/tsconfig.tsbuildinfo +1 -1
  72. package/package.json +1 -1
@@ -1,9 +1,12 @@
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 { mkdirSync, writeFileSync, rmSync } from "node:fs";
4
+ import { mkdirSync, mkdtempSync, readFileSync, writeFileSync, rmSync } from "node:fs";
5
+ import os from "node:os";
6
+ import path from "node:path";
5
7
  import { createRequestHandler, shutdownServe } from "../src/serve/server.js";
6
8
  import { AgentBridge } from "../src/serve/agent-bridge.js";
9
+ import { Session } from "@minicode/agent-sdk";
7
10
  import { createTestAgentConfig } from "./test-utils.js";
8
11
  /**
9
12
  * Lightweight AgentBridge subclass for testing.
@@ -12,7 +15,11 @@ import { createTestAgentConfig } from "./test-utils.js";
12
15
  class MockBridge extends AgentBridge {
13
16
  _busy = false;
14
17
  _currentSessionId = "sess-1";
18
+ _baseConfig = createTestAgentConfig("/tmp/test-workspace");
19
+ _config = createTestAgentConfig("/tmp/test-workspace");
15
20
  turnHistory = [];
21
+ openRouterKey;
22
+ openRouterSessionActive = false;
16
23
  constructor() {
17
24
  super(() => { }, false);
18
25
  }
@@ -20,7 +27,7 @@ class MockBridge extends AgentBridge {
20
27
  return this._busy;
21
28
  }
22
29
  getConfig() {
23
- return createTestAgentConfig("/tmp/test-workspace");
30
+ return this._config;
24
31
  }
25
32
  async runTurn(message) {
26
33
  if (this._busy)
@@ -65,11 +72,47 @@ class MockBridge extends AgentBridge {
65
72
  async loadSess(label) {
66
73
  if (label === "nonexistent")
67
74
  return null;
68
- return { session: {}, label };
75
+ const session = new Session("sess-1");
76
+ session.addMessage({
77
+ role: "user",
78
+ content: "[Conversation Summary — earlier messages were compacted to save context]\nOlder summary",
79
+ });
80
+ for (let i = 1; i <= 11; i += 1) {
81
+ session.addMessage({
82
+ role: i % 2 === 0 ? "assistant" : "user",
83
+ content: `message-${i}`,
84
+ });
85
+ }
86
+ return { session, label };
69
87
  }
70
88
  getCurrentSessionId() {
71
89
  return this._currentSessionId;
72
90
  }
91
+ connectOpenRouter(apiKey) {
92
+ this.openRouterKey = apiKey;
93
+ this.openRouterSessionActive = true;
94
+ this._config.modelProvider = "openai-compatible";
95
+ this._config.openAiBaseUrl = "https://openrouter.ai/api/v1";
96
+ this._config.openAiApiKey = apiKey;
97
+ }
98
+ disconnectOpenRouter() {
99
+ if (!this.openRouterSessionActive) {
100
+ return false;
101
+ }
102
+ this.openRouterSessionActive = false;
103
+ this.openRouterKey = undefined;
104
+ this._config.modelProvider = this._baseConfig.modelProvider;
105
+ this._config.model = this._baseConfig.model;
106
+ this._config.openAiBaseUrl = this._baseConfig.openAiBaseUrl;
107
+ delete this._config.openAiApiKey;
108
+ return true;
109
+ }
110
+ isOpenRouterSessionConnected() {
111
+ return this.openRouterSessionActive;
112
+ }
113
+ switchModel(modelId) {
114
+ this._config.model = modelId;
115
+ }
73
116
  setBusy(busy) {
74
117
  this._busy = busy;
75
118
  }
@@ -84,11 +127,19 @@ class MockBridge extends AgentBridge {
84
127
  ];
85
128
  }
86
129
  getSymbol(name) {
87
- const syms = this.getSymbols();
88
- const match = syms.find((s) => s.qualifiedName === name || s.name === name);
130
+ const [match] = this.getSymbolMatches(name);
89
131
  if (!match)
90
132
  return undefined;
91
- return { ...match, dependencies: [], kind: match.kind };
133
+ return match;
134
+ }
135
+ getSymbolMatches(name) {
136
+ return this.getSymbols()
137
+ .filter((s) => s.qualifiedName === name || s.name === name)
138
+ .map((match) => ({
139
+ ...match,
140
+ dependencies: [],
141
+ kind: match.kind,
142
+ }));
92
143
  }
93
144
  getDependencies(symbolName) {
94
145
  if (symbolName === "foo") {
@@ -247,10 +298,63 @@ class MockBridge extends AgentBridge {
247
298
  return `Interpreting ${findingId}...`;
248
299
  }
249
300
  }
301
+ class AmbiguousMockBridge extends MockBridge {
302
+ getSymbols() {
303
+ return [
304
+ ...super.getSymbols(),
305
+ {
306
+ name: "Review (type)",
307
+ qualifiedName: "Review#type",
308
+ kind: "type",
309
+ filePath: "src/review.ts",
310
+ startLine: 1,
311
+ endLine: 1,
312
+ signature: "type Review = { id: string }",
313
+ exported: true,
314
+ },
315
+ {
316
+ name: "Review (class)",
317
+ qualifiedName: "Review#class",
318
+ kind: "class",
319
+ filePath: "src/review.ts",
320
+ startLine: 3,
321
+ endLine: 7,
322
+ signature: "class Review",
323
+ exported: true,
324
+ },
325
+ ];
326
+ }
327
+ getSymbolMatches(name) {
328
+ if (name === "Review") {
329
+ return this.getSymbols()
330
+ .filter((symbol) => symbol.qualifiedName.startsWith("Review#"))
331
+ .map((match) => ({
332
+ ...match,
333
+ dependencies: [],
334
+ kind: match.kind,
335
+ }));
336
+ }
337
+ return super.getSymbolMatches(name);
338
+ }
339
+ }
340
+ class EmptySessionsBridge extends MockBridge {
341
+ async listSess() {
342
+ return [];
343
+ }
344
+ async saveSess(label) {
345
+ return {
346
+ id: "sess-1",
347
+ label: label ?? "auto-label",
348
+ createdAt: "2026-01-01T00:00:00.000Z",
349
+ savedAt: "2026-01-01T00:00:00.000Z",
350
+ messageCount: 1,
351
+ };
352
+ }
353
+ }
250
354
  // ── Test harness ──
251
355
  let activeServer;
252
- function startTestServer(bridge) {
253
- const handler = createRequestHandler(bridge);
356
+ function startTestServer(bridge, options = {}) {
357
+ const handler = createRequestHandler(bridge, undefined, options);
254
358
  const server = createServer(handler);
255
359
  activeServer = server;
256
360
  return new Promise((resolve) => {
@@ -306,7 +410,7 @@ test("GET /api/config returns formatted config plus structured settings", async
306
410
  assert.ok(body.config.includes("test-model"));
307
411
  assert.equal(body.restartRequired, true);
308
412
  assert.equal(body.secretsUiSupported, false);
309
- assert.ok(body.settings.configPath.endsWith("/.minicode/agent.config.json"));
413
+ assert.ok(body.settings.configPath.endsWith("/.minicode/.env"));
310
414
  assert.ok(body.settings.entries.some((entry) => entry.key === "maxSteps"));
311
415
  });
312
416
  test("GET /api/sessions returns session list", async () => {
@@ -332,6 +436,24 @@ test("POST /api/sessions/save saves a session", async () => {
332
436
  const body = (await res.json());
333
437
  assert.equal(body.label, "my-save");
334
438
  });
439
+ test("session APIs support the first save when no sessions exist yet", async () => {
440
+ const bridge = new EmptySessionsBridge();
441
+ const base = await startTestServer(bridge);
442
+ const listRes = await fetch(`${base}/api/sessions`);
443
+ assert.equal(listRes.status, 200);
444
+ const listBody = (await listRes.json());
445
+ assert.equal(listBody.sessions.length, 0);
446
+ assert.equal(listBody.currentSessionId, "sess-1");
447
+ const saveRes = await fetch(`${base}/api/sessions/save`, {
448
+ method: "POST",
449
+ headers: { "Content-Type": "application/json" },
450
+ body: JSON.stringify({}),
451
+ });
452
+ assert.equal(saveRes.status, 200);
453
+ const saveBody = (await saveRes.json());
454
+ assert.equal(saveBody.id, "sess-1");
455
+ assert.equal(saveBody.label, "auto-label");
456
+ });
335
457
  test("POST /api/sessions/load returns 404 for unknown session", async () => {
336
458
  const bridge = new MockBridge();
337
459
  const base = await startTestServer(bridge);
@@ -353,6 +475,10 @@ test("POST /api/sessions/load returns success for known session", async () => {
353
475
  assert.equal(res.status, 200);
354
476
  const body = (await res.json());
355
477
  assert.equal(body.label, "test-session");
478
+ assert.equal(body.messages.length, 10);
479
+ assert.equal(body.messages[0]?.content, "message-2");
480
+ assert.equal(body.messages[9]?.content, "message-11");
481
+ assert.ok(body.messages.every((message) => !message.content.startsWith("[Conversation Summary")));
356
482
  });
357
483
  test("POST /api/chat returns agent response", async () => {
358
484
  const bridge = new MockBridge();
@@ -389,6 +515,179 @@ test("POST /api/chat returns 429 when agent is busy", async () => {
389
515
  });
390
516
  assert.equal(res.status, 429);
391
517
  });
518
+ test("POST /api/openrouter/connect exchanges code and stores a session-only key", async () => {
519
+ const bridge = new MockBridge();
520
+ const base = await startTestServer(bridge);
521
+ const originalFetch = globalThis.fetch;
522
+ globalThis.fetch = async (input, init) => {
523
+ if (String(input) !== "https://openrouter.ai/api/v1/auth/keys") {
524
+ return originalFetch(input, init);
525
+ }
526
+ assert.equal(init?.method, "POST");
527
+ assert.equal((init?.headers)["Content-Type"], "application/json");
528
+ const body = JSON.parse(String(init?.body));
529
+ assert.equal(body.code, "oauth-code");
530
+ assert.equal(body.code_verifier, "pkce-verifier");
531
+ assert.equal(body.code_challenge_method, "S256");
532
+ return new Response(JSON.stringify({ key: "sk-or-v1-session-key" }), { status: 200, headers: { "content-type": "application/json" } });
533
+ };
534
+ try {
535
+ const res = await originalFetch(`${base}/api/openrouter/connect`, {
536
+ method: "POST",
537
+ headers: { "Content-Type": "application/json" },
538
+ body: JSON.stringify({ code: "oauth-code", codeVerifier: "pkce-verifier" }),
539
+ });
540
+ assert.equal(res.status, 200);
541
+ const body = await res.json();
542
+ assert.equal(body.ok, true);
543
+ assert.equal(body.sessionOnly, true);
544
+ assert.equal(body.persistedToEnv, false);
545
+ assert.equal(body.persistedEnvPath, null);
546
+ assert.equal(body.persistWarning, null);
547
+ assert.equal(body.baseUrl, "https://openrouter.ai/api/v1");
548
+ assert.equal(body.needsSetup, false);
549
+ assert.deepEqual(body.missing, []);
550
+ assert.equal(body.message, "OpenRouter connected for this serve session.");
551
+ assert.equal(bridge.openRouterKey, "sk-or-v1-session-key");
552
+ assert.equal(bridge.getConfig().modelProvider, "openai-compatible");
553
+ assert.equal(bridge.getConfig().openAiBaseUrl, "https://openrouter.ai/api/v1");
554
+ assert.equal(bridge.getConfig().openAiApiKey, "sk-or-v1-session-key");
555
+ }
556
+ finally {
557
+ globalThis.fetch = originalFetch;
558
+ }
559
+ });
560
+ test("POST /api/openrouter/connect can persist OpenRouter setup to ~/.minicode/.env", async () => {
561
+ const bridge = new MockBridge();
562
+ const minicodeHome = mkdtempSync(path.join(os.tmpdir(), "minicode-openrouter-home-"));
563
+ const base = await startTestServer(bridge, { minicodeHome });
564
+ const originalFetch = globalThis.fetch;
565
+ globalThis.fetch = async (input, init) => {
566
+ if (String(input) !== "https://openrouter.ai/api/v1/auth/keys") {
567
+ return originalFetch(input, init);
568
+ }
569
+ return new Response(JSON.stringify({ key: "sk-or-v1-session-key" }), { status: 200, headers: { "content-type": "application/json" } });
570
+ };
571
+ try {
572
+ const res = await originalFetch(`${base}/api/openrouter/connect`, {
573
+ method: "POST",
574
+ headers: { "Content-Type": "application/json" },
575
+ body: JSON.stringify({ code: "oauth-code", codeVerifier: "pkce-verifier", persistToEnv: true }),
576
+ });
577
+ assert.equal(res.status, 200);
578
+ const body = await res.json();
579
+ assert.equal(body.persistedToEnv, true);
580
+ assert.equal(body.persistWarning, null);
581
+ assert.equal(body.persistedEnvPath, path.join(minicodeHome, ".env"));
582
+ assert.match(body.message, /saved to ~\/\.minicode\/\.env/);
583
+ const envContents = readFileSync(path.join(minicodeHome, ".env"), "utf8");
584
+ assert.match(envContents, /^MODEL_PROVIDER=openai-compatible$/m);
585
+ assert.match(envContents, /^OPENAI_BASE_URL=https:\/\/openrouter\.ai\/api\/v1$/m);
586
+ assert.match(envContents, /^OPENROUTER_API_KEY=sk-or-v1-session-key$/m);
587
+ }
588
+ finally {
589
+ globalThis.fetch = originalFetch;
590
+ rmSync(minicodeHome, { recursive: true, force: true });
591
+ }
592
+ });
593
+ test("POST /api/model persists the selected model to ~/.minicode/.env", async () => {
594
+ const bridge = new MockBridge();
595
+ const minicodeHome = mkdtempSync(path.join(os.tmpdir(), "minicode-model-home-"));
596
+ const envPath = path.join(minicodeHome, ".env");
597
+ writeFileSync(envPath, [
598
+ "MODEL_PROVIDER=openai-compatible",
599
+ "OPENAI_BASE_URL=https://openrouter.ai/api/v1",
600
+ "OPENROUTER_API_KEY=sk-or-v1-session-key",
601
+ "",
602
+ ].join("\n"), "utf8");
603
+ const base = await startTestServer(bridge, { minicodeHome });
604
+ try {
605
+ const res = await fetch(`${base}/api/model`, {
606
+ method: "POST",
607
+ headers: { "Content-Type": "application/json" },
608
+ body: JSON.stringify({ model: "openrouter/test-model" }),
609
+ });
610
+ assert.equal(res.status, 200);
611
+ const body = await res.json();
612
+ assert.equal(body.model, "openrouter/test-model");
613
+ assert.equal(body.persistedToEnv, true);
614
+ assert.equal(body.persistedEnvPath, envPath);
615
+ const envContents = readFileSync(envPath, "utf8");
616
+ assert.match(envContents, /^MODEL=openrouter\/test-model$/m);
617
+ assert.match(envContents, /^OPENROUTER_API_KEY=sk-or-v1-session-key$/m);
618
+ assert.equal(bridge.getConfig().model, "openrouter/test-model");
619
+ }
620
+ finally {
621
+ rmSync(minicodeHome, { recursive: true, force: true });
622
+ }
623
+ });
624
+ test("POST /api/openrouter/disconnect removes the session-only OpenRouter connection", async () => {
625
+ const bridge = new MockBridge();
626
+ bridge.connectOpenRouter("sk-or-v1-session-key");
627
+ const base = await startTestServer(bridge);
628
+ const res = await fetch(`${base}/api/openrouter/disconnect`, {
629
+ method: "POST",
630
+ headers: { "Content-Type": "application/json" },
631
+ });
632
+ assert.equal(res.status, 200);
633
+ const body = await res.json();
634
+ assert.equal(body.ok, true);
635
+ assert.equal(body.disconnected, true);
636
+ assert.equal(body.sessionOnly, true);
637
+ assert.equal(body.provider, "anthropic");
638
+ assert.equal(body.baseUrl, "http://localhost:1234/v1");
639
+ assert.equal(body.message, "Removed the session-only OpenRouter connection and restored your original provider settings.");
640
+ assert.equal(bridge.openRouterSessionActive, false);
641
+ assert.equal(bridge.getConfig().modelProvider, "anthropic");
642
+ assert.equal(bridge.getConfig().openAiBaseUrl, "http://localhost:1234/v1");
643
+ assert.equal(bridge.getConfig().openAiApiKey, undefined);
644
+ });
645
+ test("GET /api/status exposes OpenRouter session state and base URL", async () => {
646
+ const bridge = new MockBridge();
647
+ bridge.connectOpenRouter("sk-or-v1-session-key");
648
+ const base = await startTestServer(bridge);
649
+ const res = await fetch(`${base}/api/status`);
650
+ assert.equal(res.status, 200);
651
+ const body = await res.json();
652
+ assert.equal(body.provider, "openai-compatible");
653
+ assert.equal(body.baseUrl, "https://openrouter.ai/api/v1");
654
+ assert.equal(body.sessionOpenRouterConnected, true);
655
+ });
656
+ test("POST /api/openrouter/connect returns 400 when code is missing", async () => {
657
+ const bridge = new MockBridge();
658
+ const base = await startTestServer(bridge);
659
+ const res = await fetch(`${base}/api/openrouter/connect`, {
660
+ method: "POST",
661
+ headers: { "Content-Type": "application/json" },
662
+ body: JSON.stringify({ codeVerifier: "pkce-verifier" }),
663
+ });
664
+ assert.equal(res.status, 400);
665
+ });
666
+ test("POST /api/openrouter/connect surfaces exchange failures", async () => {
667
+ const bridge = new MockBridge();
668
+ const base = await startTestServer(bridge);
669
+ const originalFetch = globalThis.fetch;
670
+ globalThis.fetch = async (input, init) => {
671
+ if (String(input) !== "https://openrouter.ai/api/v1/auth/keys") {
672
+ return originalFetch(input, init);
673
+ }
674
+ return new Response("Invalid code", { status: 403 });
675
+ };
676
+ try {
677
+ const res = await originalFetch(`${base}/api/openrouter/connect`, {
678
+ method: "POST",
679
+ headers: { "Content-Type": "application/json" },
680
+ body: JSON.stringify({ code: "bad-code", codeVerifier: "pkce-verifier" }),
681
+ });
682
+ assert.equal(res.status, 403);
683
+ const body = await res.json();
684
+ assert.ok(body.error.includes("OpenRouter OAuth exchange failed"));
685
+ assert.ok(body.error.includes("Invalid code"));
686
+ }
687
+ finally {
688
+ globalThis.fetch = originalFetch;
689
+ }
690
+ });
392
691
  // ── OpenAI-compatible API tests ──
393
692
  test("GET /v1/models returns minicode-agent model", async () => {
394
693
  const bridge = new MockBridge();
@@ -497,6 +796,7 @@ test("GET / serves index.html", async () => {
497
796
  const res = await fetch(`${base}/`);
498
797
  assert.equal(res.status, 200);
499
798
  assert.equal(res.headers.get("content-type"), "text/html");
799
+ assert.equal(res.headers.get("cache-control"), "no-store");
500
800
  const html = await res.text();
501
801
  assert.ok(html.includes("minicode"));
502
802
  });
@@ -506,6 +806,7 @@ test("GET /style.css serves CSS file", async () => {
506
806
  const res = await fetch(`${base}/style.css`);
507
807
  assert.equal(res.status, 200);
508
808
  assert.equal(res.headers.get("content-type"), "text/css");
809
+ assert.equal(res.headers.get("cache-control"), "no-store");
509
810
  });
510
811
  test("GET /app.js serves JS file", async () => {
511
812
  const bridge = new MockBridge();
@@ -513,6 +814,7 @@ test("GET /app.js serves JS file", async () => {
513
814
  const res = await fetch(`${base}/app.js`);
514
815
  assert.equal(res.status, 200);
515
816
  assert.equal(res.headers.get("content-type"), "application/javascript");
817
+ assert.equal(res.headers.get("cache-control"), "no-store");
516
818
  });
517
819
  test("GET /nonexistent returns 404", async () => {
518
820
  const bridge = new MockBridge();
@@ -570,6 +872,15 @@ test("GET /api/symbols/:name/dependencies returns 404 for unknown symbol", async
570
872
  const res = await fetch(`${base}/api/symbols/nonexistent/dependencies`);
571
873
  assert.equal(res.status, 404);
572
874
  });
875
+ test("GET /api/symbols/:name/dependencies returns 409 for ambiguous symbol", async () => {
876
+ const bridge = new AmbiguousMockBridge();
877
+ const base = await startTestServer(bridge);
878
+ const res = await fetch(`${base}/api/symbols/Review/dependencies`);
879
+ assert.equal(res.status, 409);
880
+ const body = (await res.json());
881
+ assert.equal(body.error, 'Symbol "Review" is ambiguous');
882
+ assert.deepEqual(body.candidates.map((candidate) => candidate.qualifiedName).sort(), ["Review#class", "Review#type"]);
883
+ });
573
884
  test("GET /api/symbols/:name/references returns references", async () => {
574
885
  const bridge = new MockBridge();
575
886
  const base = await startTestServer(bridge);
@@ -586,6 +897,15 @@ test("GET /api/symbols/:name/references returns 404 for unknown symbol", async (
586
897
  const res = await fetch(`${base}/api/symbols/nonexistent/references`);
587
898
  assert.equal(res.status, 404);
588
899
  });
900
+ test("GET /api/symbols/:name/references returns 409 for ambiguous symbol", async () => {
901
+ const bridge = new AmbiguousMockBridge();
902
+ const base = await startTestServer(bridge);
903
+ const res = await fetch(`${base}/api/symbols/Review/references`);
904
+ assert.equal(res.status, 409);
905
+ const body = (await res.json());
906
+ assert.equal(body.error, 'Symbol "Review" is ambiguous');
907
+ assert.deepEqual(body.candidates.map((candidate) => candidate.qualifiedName).sort(), ["Review#class", "Review#type"]);
908
+ });
589
909
  test("GET /api/code-map returns code map", async () => {
590
910
  const bridge = new MockBridge();
591
911
  const base = await startTestServer(bridge);
@@ -741,6 +1061,15 @@ test("GET /api/symbols/:name/source returns 404 for unknown symbol", async () =>
741
1061
  const body = (await res.json());
742
1062
  assert.ok(body.error.includes("nonexistent"));
743
1063
  });
1064
+ test("GET /api/symbols/:name/source returns 409 for ambiguous symbol", async () => {
1065
+ const bridge = new AmbiguousMockBridge();
1066
+ const base = await startTestServer(bridge);
1067
+ const res = await fetch(`${base}/api/symbols/Review/source`);
1068
+ assert.equal(res.status, 409);
1069
+ const body = (await res.json());
1070
+ assert.equal(body.error, 'Symbol "Review" is ambiguous');
1071
+ assert.deepEqual(body.candidates.map((candidate) => candidate.qualifiedName).sort(), ["Review#class", "Review#type"]);
1072
+ });
744
1073
  test("GET /api/symbols/:name/source returns 500 when file is missing", async () => {
745
1074
  const bridge = new MockBridge();
746
1075
  const base = await startTestServer(bridge);
@@ -0,0 +1,56 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { buildSessionPreview, isCompactionSummaryMessage, } from "../src/session/session-preview.js";
4
+ test("isCompactionSummaryMessage detects compacted summary stubs", () => {
5
+ assert.equal(isCompactionSummaryMessage({
6
+ role: "user",
7
+ content: "[Conversation Summary — earlier messages were compacted to save context]\nSummary text",
8
+ }), true);
9
+ assert.equal(isCompactionSummaryMessage({
10
+ role: "assistant",
11
+ content: "[Conversation Summary — earlier messages were compacted to save context]\nSummary text",
12
+ }), false);
13
+ });
14
+ test("buildSessionPreview filters compaction summaries and keeps the last ten messages", () => {
15
+ const preview = buildSessionPreview([
16
+ {
17
+ role: "user",
18
+ content: "[Conversation Summary — earlier messages were compacted using LLM summarization]\nSummary text",
19
+ },
20
+ { role: "user", content: "message-1" },
21
+ { role: "assistant", content: "message-2" },
22
+ { role: "user", content: "message-3" },
23
+ { role: "assistant", content: "message-4" },
24
+ { role: "user", content: "message-5" },
25
+ { role: "assistant", content: "message-6" },
26
+ { role: "user", content: "message-7" },
27
+ { role: "assistant", content: "message-8" },
28
+ { role: "user", content: "message-9" },
29
+ { role: "assistant", content: "message-10" },
30
+ { role: "user", content: "message-11" },
31
+ ]);
32
+ assert.equal(preview.length, 10);
33
+ assert.equal(preview[0]?.content, "message-2");
34
+ assert.equal(preview[9]?.content, "message-11");
35
+ assert.ok(preview.every((message) => !message.content.startsWith("[Conversation Summary")));
36
+ });
37
+ test("buildSessionPreview preserves tool messages in order", () => {
38
+ const preview = buildSessionPreview([
39
+ {
40
+ role: "assistant",
41
+ content: "Let me check that",
42
+ toolCalls: [{ id: "tool-1", name: "search", input: { query: "foo" } }],
43
+ },
44
+ {
45
+ role: "tool",
46
+ toolCallId: "tool-1",
47
+ toolName: "search",
48
+ content: "search output",
49
+ },
50
+ {
51
+ role: "assistant",
52
+ content: "Found it",
53
+ },
54
+ ]);
55
+ assert.deepEqual(preview.map((message) => message.role), ["assistant", "tool", "assistant"]);
56
+ });
@@ -18,4 +18,8 @@ test("built JS contains active saved session update logic", () => {
18
18
  assert.ok(js.includes("activeSavedSession"), "JS should track the active saved session");
19
19
  assert.ok(js.includes("currentSessionId"), "JS should read the current session id from the sessions API");
20
20
  assert.ok(js.includes("Session updated:"), "JS should emit the update confirmation message");
21
+ assert.ok(js.includes("sessionRefreshTracker"), "JS should guard session list refreshes against stale responses");
22
+ assert.ok(js.includes('saveBtn.setAttribute("disabled", "true")'), "JS should disable saving while the first save is in flight");
23
+ assert.ok(js.includes("renderLoadedSessionMessages"), "JS should render session previews after load");
24
+ assert.ok(js.includes("body.messages"), "JS should read preview messages from the load session response");
21
25
  });
@@ -7,6 +7,12 @@ test("built HTML contains settings entry point and modal shell", () => {
7
7
  const html = readFileSync(join(distWeb, "index.html"), "utf8");
8
8
  assert.ok(html.includes('id="settings-btn"'), "HTML should contain the settings button");
9
9
  assert.ok(html.includes('id="settings-modal"'), "HTML should contain the settings modal");
10
+ assert.ok(html.includes('id="connect-openrouter-btn"'), "HTML should contain the OpenRouter connect button");
11
+ assert.ok(html.includes('id="config-overlay-intro"'), "HTML should contain the setup overlay intro copy");
12
+ assert.ok(html.includes("Try minicode for free with OpenRouter"), "HTML should promote the free OpenRouter quick start");
13
+ assert.ok(html.includes('id="openrouter-connect-modal"'), "HTML should contain the OpenRouter consent modal");
14
+ assert.ok(html.includes('id="openrouter-persist-checkbox"'), "HTML should contain the OpenRouter persistence checkbox");
15
+ assert.ok(html.includes('id="disconnect-openrouter-btn"'), "HTML should contain the OpenRouter disconnect button");
10
16
  assert.ok(!html.includes('id="settings-scope"'), "HTML should no longer contain the settings scope selector");
11
17
  assert.ok(html.includes('id="settings-save"'), "HTML should contain the settings save action");
12
18
  });
@@ -16,11 +22,23 @@ test("built CSS contains modal and settings layout styles", () => {
16
22
  assert.ok(css.includes(".settings-list"), "CSS should contain settings list styles");
17
23
  assert.ok(css.includes(".settings-item-meta"), "CSS should contain settings metadata grid styles");
18
24
  assert.ok(css.includes(".settings-help-warning"), "CSS should contain warning styling for env overrides");
25
+ assert.ok(css.includes("align-items: flex-start;"), "CSS should top-align scrollable setup overlay content");
26
+ assert.ok(css.includes(".config-overlay-spotlight"), "CSS should style the OpenRouter quick-start spotlight");
27
+ assert.ok(css.includes(".openrouter-connect-body"), "CSS should style the OpenRouter consent modal body");
28
+ assert.ok(css.includes(".config-connect-status.success"), "CSS should style OpenRouter connect success state");
29
+ assert.ok(css.includes(".settings-session-banner"), "CSS should style the OpenRouter session banner");
19
30
  assert.ok(css.includes("body.modal-open"), "CSS should lock scroll while the settings modal is open");
20
31
  });
21
32
  test("built JS contains config loading and saving logic for settings", () => {
22
33
  const js = readFileSync(join(distWeb, "app.js"), "utf8");
23
34
  assert.ok(js.includes("/api/config"), "JS should fetch the config API");
35
+ assert.ok(js.includes("/api/openrouter/connect"), "JS should call the OpenRouter connect API");
36
+ assert.ok(js.includes("/api/openrouter/disconnect"), "JS should call the OpenRouter disconnect API");
37
+ assert.ok(js.includes("persistToHomeEnv"), "JS should support persisting the selected model after OpenRouter setup");
38
+ assert.ok(js.includes("code_challenge_method"), "JS should generate an OpenRouter PKCE auth request");
39
+ assert.ok(js.includes("sessionStorage"), "JS should persist the PKCE verifier for the OAuth callback");
40
+ assert.ok(js.includes("minicode:openrouter:persist-to-env"), "JS should persist the optional OpenRouter env-write choice across OAuth");
41
+ assert.ok(js.includes("sessionOpenRouterConnected"), "JS should track session-only OpenRouter state");
24
42
  assert.ok(js.includes("Save settings"), "JS should contain the settings save action text");
25
43
  assert.ok(js.includes("settingsPayload"), "JS should track settings payload state");
26
44
  assert.ok(js.includes("persistedValue"), "JS should wire persisted settings behavior");
@@ -1 +1 @@
1
- {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../../src/agent/agent.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../../src/agent/agent.ts"],"names":[],"mappings":"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"}
@@ -157,7 +157,8 @@ export class CodingAgent {
157
157
  };
158
158
  }
159
159
  setReasoningEffort(effort) {
160
- const { reasoningEffort: _, ...rest } = this.config;
160
+ const rest = { ...this.config };
161
+ delete rest.reasoningEffort;
161
162
  this.config = effort ? { ...rest, reasoningEffort: effort } : { ...rest };
162
163
  }
163
164
  /**