create-walle 0.9.10 → 0.9.12

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 (168) hide show
  1. package/README.md +3 -3
  2. package/package.json +2 -2
  3. package/template/bin/dev.sh +7 -1
  4. package/template/bin/setup.js +53 -9
  5. package/template/bin/sync-images.js +53 -0
  6. package/template/builder-journal.md +17 -0
  7. package/template/claude-task-manager/api-prompts.js +98 -13
  8. package/template/claude-task-manager/api-reviews.js +82 -5
  9. package/template/claude-task-manager/db.js +32 -5
  10. package/template/claude-task-manager/docs/session-capture-foundation-design.md +1273 -0
  11. package/template/claude-task-manager/lib/claude-desktop-sessions.js +696 -0
  12. package/template/claude-task-manager/lib/coding-agent-models.js +49 -1
  13. package/template/claude-task-manager/lib/session-capture.js +421 -0
  14. package/template/claude-task-manager/lib/session-history.js +135 -15
  15. package/template/claude-task-manager/lib/session-jobs.js +10 -5
  16. package/template/claude-task-manager/lib/session-stream.js +87 -19
  17. package/template/claude-task-manager/lib/setup-provider-config.js +115 -0
  18. package/template/claude-task-manager/lib/walle-ctm-history.js +72 -0
  19. package/template/claude-task-manager/lib/walle-session-context.js +61 -0
  20. package/template/claude-task-manager/lib/walle-transcript.js +176 -0
  21. package/template/claude-task-manager/public/css/setup.css +35 -8
  22. package/template/claude-task-manager/public/css/walle-session.css +56 -0
  23. package/template/claude-task-manager/public/css/walle.css +120 -0
  24. package/template/claude-task-manager/public/index.html +814 -181
  25. package/template/claude-task-manager/public/js/message-renderer.js +148 -19
  26. package/template/claude-task-manager/public/js/reviews.js +120 -62
  27. package/template/claude-task-manager/public/js/setup.js +75 -31
  28. package/template/claude-task-manager/public/js/stream-view.js +115 -55
  29. package/template/claude-task-manager/public/js/walle-session.js +84 -2
  30. package/template/claude-task-manager/public/js/walle.js +308 -54
  31. package/template/claude-task-manager/server.js +1092 -146
  32. package/template/claude-task-manager/session-integrity.js +181 -54
  33. package/template/claude-task-manager/session-utils.js +123 -41
  34. package/template/claude-task-manager/workers/state-detectors/codex.js +5 -2
  35. package/template/package.json +1 -1
  36. package/template/wall-e/adapters/ctm.js +39 -18
  37. package/template/wall-e/agent-runners/contract.js +17 -0
  38. package/template/wall-e/agent-runners/index.js +22 -0
  39. package/template/wall-e/agent-runtime/harness.js +212 -0
  40. package/template/wall-e/agent-runtime/index.js +8 -0
  41. package/template/wall-e/agent-runtime/registry.js +67 -0
  42. package/template/wall-e/agent-runtime/session-store.js +179 -0
  43. package/template/wall-e/agent-runtime/spawn.js +208 -0
  44. package/template/wall-e/api-walle.js +174 -7
  45. package/template/wall-e/brain.js +266 -28
  46. package/template/wall-e/channels/policy.js +88 -0
  47. package/template/wall-e/channels/registry.js +15 -1
  48. package/template/wall-e/channels/reply-dispatcher.js +70 -0
  49. package/template/wall-e/channels/session-bindings.js +51 -0
  50. package/template/wall-e/chat/code-review-context.js +29 -0
  51. package/template/wall-e/chat.js +188 -42
  52. package/template/wall-e/coding/acp-adapter.js +188 -0
  53. package/template/wall-e/coding/agent-catalog.js +129 -0
  54. package/template/wall-e/coding/compaction-service.js +247 -0
  55. package/template/wall-e/coding/execution-trace.js +3 -0
  56. package/template/wall-e/coding/instruction-service.js +224 -0
  57. package/template/wall-e/coding/model-message.js +67 -0
  58. package/template/wall-e/coding/permission-rules-store.js +111 -0
  59. package/template/wall-e/coding/permission-service.js +266 -0
  60. package/template/wall-e/coding/prompt-bundle.js +67 -0
  61. package/template/wall-e/coding/prompt-runtime.js +243 -0
  62. package/template/wall-e/coding/provider-transform.js +188 -0
  63. package/template/wall-e/coding/runtime-mode.js +132 -0
  64. package/template/wall-e/coding/snapshot-service.js +155 -0
  65. package/template/wall-e/coding/stream-processor.js +268 -0
  66. package/template/wall-e/coding/task-tool.js +255 -0
  67. package/template/wall-e/coding/tool-registry.js +361 -0
  68. package/template/wall-e/coding/transcript-writer.js +143 -0
  69. package/template/wall-e/coding/workspace-replay.js +324 -0
  70. package/template/wall-e/coding-context.js +4 -22
  71. package/template/wall-e/coding-orchestrator.js +307 -18
  72. package/template/wall-e/coding-prompts.js +44 -3
  73. package/template/wall-e/context/context-builder.js +43 -1
  74. package/template/wall-e/context/topic-matcher.js +1 -1
  75. package/template/wall-e/eval/agent-runner.js +59 -13
  76. package/template/wall-e/eval/benchmarks/memory-retrieval.json +155 -57
  77. package/template/wall-e/eval/benchmarks.js +100 -16
  78. package/template/wall-e/eval/eval-orchestrator.js +218 -8
  79. package/template/wall-e/eval/harvester.js +49 -5
  80. package/template/wall-e/eval/head-to-head.js +23 -2
  81. package/template/wall-e/eval/humaneval-adapter.js +30 -5
  82. package/template/wall-e/eval/livecodebench-adapter.js +29 -5
  83. package/template/wall-e/eval/manifest.js +186 -0
  84. package/template/wall-e/eval/run-agent-benchmarks.js +66 -2
  85. package/template/wall-e/eval/session-retrieval-benchmark.js +150 -0
  86. package/template/wall-e/eval/session-transcripts.js +57 -4
  87. package/template/wall-e/eval/swebench-adapter.js +109 -3
  88. package/template/wall-e/evaluation/agent-router.js +53 -1
  89. package/template/wall-e/evaluation/coding-quorum.js +48 -1
  90. package/template/wall-e/evaluation/router.js +4 -2
  91. package/template/wall-e/evaluation/tier-selector.js +11 -1
  92. package/template/wall-e/extraction/contradiction.js +2 -2
  93. package/template/wall-e/extraction/indexer.js +2 -1
  94. package/template/wall-e/extraction/knowledge-extractor.js +2 -2
  95. package/template/wall-e/fly.toml +1 -0
  96. package/template/wall-e/hooks/cli.js +92 -0
  97. package/template/wall-e/hooks/discovery.js +119 -0
  98. package/template/wall-e/hooks/index.js +7 -0
  99. package/template/wall-e/hooks/manifest.js +55 -0
  100. package/template/wall-e/hooks/runtime.js +84 -0
  101. package/template/wall-e/hooks/session-memory.js +225 -0
  102. package/template/wall-e/http/auth.js +7 -2
  103. package/template/wall-e/http/chat-api.js +54 -8
  104. package/template/wall-e/integrations/claude-plugin/hooks/hooks.json +27 -0
  105. package/template/wall-e/integrations/claude-plugin/hooks/walle-precompact-hook.sh +5 -0
  106. package/template/wall-e/integrations/claude-plugin/hooks/walle-stop-hook.sh +5 -0
  107. package/template/wall-e/integrations/codex-plugin/hooks/walle-hook.sh +7 -0
  108. package/template/wall-e/integrations/codex-plugin/hooks.json +37 -0
  109. package/template/wall-e/listening/calendar.js +3 -1
  110. package/template/wall-e/llm/client.js +64 -10
  111. package/template/wall-e/llm/google.js +39 -5
  112. package/template/wall-e/llm/ollama.js +1 -1
  113. package/template/wall-e/llm/ollama.plugin.json +1 -1
  114. package/template/wall-e/llm/provider-availability.js +10 -0
  115. package/template/wall-e/llm/provider-error.js +269 -0
  116. package/template/wall-e/llm/tool-adapter.js +48 -12
  117. package/template/wall-e/loops/boot.js +2 -1
  118. package/template/wall-e/loops/initiative.js +2 -2
  119. package/template/wall-e/loops/tasks.js +8 -47
  120. package/template/wall-e/loops/workspace-prompts.js +20 -0
  121. package/template/wall-e/mcp-server.js +442 -1
  122. package/template/wall-e/memory/session-ingest-service.js +159 -0
  123. package/template/wall-e/memory/source-indexer.js +289 -0
  124. package/template/wall-e/plugins/discovery.js +83 -0
  125. package/template/wall-e/plugins/manifest-loader.js +50 -10
  126. package/template/wall-e/plugins/manifest-schema.js +69 -0
  127. package/template/wall-e/plugins/model-catalog.js +55 -0
  128. package/template/wall-e/prompts/coding/base.txt +2 -0
  129. package/template/wall-e/prompts/coding/deepseek.txt +1 -0
  130. package/template/wall-e/prompts/coding/memory-protocol.md +9 -0
  131. package/template/wall-e/prompts/coding/plan.txt +1 -0
  132. package/template/wall-e/runtime/execution-trace.js +220 -0
  133. package/template/wall-e/security/audit.js +266 -0
  134. package/template/wall-e/security/ssrf.js +236 -0
  135. package/template/wall-e/session-files.js +303 -0
  136. package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +3 -0
  137. package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +3 -0
  138. package/template/wall-e/skills/internal-skill-registry.js +2 -2
  139. package/template/wall-e/skills/script-skill-runner.js +143 -0
  140. package/template/wall-e/skills/skill-executor.js +5 -6
  141. package/template/wall-e/skills/skill-fallback.js +3 -1
  142. package/template/wall-e/skills/skill-harness-registry.js +7 -8
  143. package/template/wall-e/skills/skill-planner.js +52 -4
  144. package/template/wall-e/skills/slack-ingest.js +11 -3
  145. package/template/wall-e/sources/base.js +90 -0
  146. package/template/wall-e/sources/builtin.js +33 -0
  147. package/template/wall-e/sources/claude-code-jsonl.js +78 -0
  148. package/template/wall-e/sources/codex-jsonl.js +125 -0
  149. package/template/wall-e/sources/coding-session-utils.js +117 -0
  150. package/template/wall-e/sources/contract-suite.js +59 -0
  151. package/template/wall-e/sources/gemini-jsonl.js +85 -0
  152. package/template/wall-e/sources/index.js +9 -0
  153. package/template/wall-e/sources/jsonl-utils.js +181 -0
  154. package/template/wall-e/sources/record-types.js +252 -0
  155. package/template/wall-e/sources/registry.js +92 -0
  156. package/template/wall-e/sources/transforms.js +100 -0
  157. package/template/wall-e/sources/walle-jsonl.js +108 -0
  158. package/template/wall-e/tools/coding-middleware.js +31 -1
  159. package/template/wall-e/tools/file-tracker.js +25 -1
  160. package/template/wall-e/tools/local-tools.js +75 -47
  161. package/template/wall-e/tools/session-sharing.js +68 -1
  162. package/template/wall-e/tools/shell-analyzer.js +1 -1
  163. package/template/wall-e/tools/shell-policy.js +47 -0
  164. package/template/wall-e/tools/snapshot.js +42 -0
  165. package/template/wall-e/training/harvester.js +49 -5
  166. package/template/wall-e/utils/repair.js +253 -1
  167. package/template/website/index.html +3 -3
  168. package/template/wall-e/skills/_bundled/slack-mentions/.watched-threads.json +0 -18
package/README.md CHANGED
@@ -8,12 +8,12 @@ Set up **CTM + Wall-E** in one command — a browser-based dashboard for AI codi
8
8
 
9
9
  A web dashboard for running and managing AI coding sessions across multiple providers.
10
10
 
11
- - **Terminal Multiplexer** — Run Claude Code, Codex, Gemini CLI, and Aider sessions side by side with live status, persistent scrollback, model switching, and AI-generated titles
11
+ - **Terminal Multiplexer** — Run Claude Code, Codex, Gemini CLI, Aider, OpenCode, Cursor Agent, and more side by side with live status, persistent scrollback, model switching, and AI-generated titles
12
12
  - **Prompt Editor** — Save, version, and organize prompts with folders, tags, chains, templates, and AI search
13
13
  - **Task Queue** — Queue prompts for sequential execution with auto-advance when the agent finishes, or step through manually
14
14
  - **Approval Workflows** — Auto-approve tool-use requests based on learned rules; uncertain cases escalate to you
15
15
  - **Code Review** — View git diffs from any project, staged or unstaged, with line-level detail
16
- - **Model Registry** — Manage providers (Anthropic, OpenAI, Google, Ollama), compare pricing, switch models per session
16
+ - **Model Registry** — Manage providers (Anthropic, OpenAI, Google, DeepSeek, Ollama, LM Studio, MLX, and CLI subscription providers), compare pricing, switch models per session
17
17
  - **Session Insights** — Analyze patterns across sessions to optimize prompts and workflows
18
18
 
19
19
  ### Wall-E (Personal Digital Twin)
@@ -24,7 +24,7 @@ An always-on AI agent that learns from your Slack, email, calendar, and coding s
24
24
  - **Proactive Intelligence** — Surfaces time-sensitive items, suggests actions, and delivers morning briefings and weekly reflections without being asked
25
25
  - **Chat with Tools** — Talk to Wall-E in the browser — it can search your memories, look up people, run skills, and call external tools via MCP (Slack, Glean, etc.)
26
26
  - **19 Bundled Skills** — Morning briefing, weekly reflection, proactive alerts, Slack monitoring, email sync, calendar integration, coding agent, model training, model pricing sync, and more
27
- - **Multi-Model** — Works with Claude, GPT, Gemini, and local models via Ollama with smart routing
27
+ - **Multi-Model** — Works with Claude, GPT, Gemini, DeepSeek, and local models via Ollama, LM Studio, or MLX with smart routing
28
28
  - **Skill Management GUI** — Search, filter, create, edit, and monitor skills from the browser with rich cards, config forms, execution history, export/import, and pre-flight validation
29
29
  - **Multi-Device** — Share your brain across machines via Dropbox or iCloud
30
30
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-walle",
3
- "version": "0.9.10",
4
- "description": "CTM + Wall-E \u2014 AI coding dashboard and personal digital twin agent. Multi-agent terminal for Claude Code, Codex, Gemini & Aider, plus prompt editor, task queue, and an agent that learns from Slack, email & calendar.",
3
+ "version": "0.9.12",
4
+ "description": "CTM + Wall-E \u2014 AI coding dashboard and personal digital twin agent. Multi-agent terminal for Claude Code, Codex, Gemini, Aider, OpenCode, and more, plus prompt editor, task queue, and an agent that learns from Slack, email & calendar.",
5
5
  "bin": {
6
6
  "create-walle": "bin/create-walle.js"
7
7
  },
@@ -53,6 +53,10 @@ elif [[ "$1" == "--refresh" ]]; then
53
53
  echo "[dev] Snapshotting production databases to $DEV_DIR ..."
54
54
  node "$ROOT/bin/sqlite-snapshot.js" "$PROD_CTM_DIR/task-manager.db" "$DEV_DIR/task-manager.db" "CTM"
55
55
  node "$ROOT/bin/sqlite-snapshot.js" "$PROD_WALLE_DIR/wall-e-brain.db" "$DEV_DIR/wall-e-brain.db" "Brain"
56
+ if [[ -d "$PROD_CTM_DIR/images" ]]; then
57
+ echo " Images: syncing $PROD_CTM_DIR/images -> $DEV_DIR/images"
58
+ node "$ROOT/bin/sync-images.js" "$PROD_CTM_DIR/images" "$DEV_DIR/images"
59
+ fi
56
60
  # Ensure the dev instance owns its WAL files from first open.
57
61
  rm -f "$DEV_DIR"/*.db-wal "$DEV_DIR"/*.db-shm
58
62
  fi
@@ -72,6 +76,8 @@ export CTM_PORT="$DEV_CTM_PORT"
72
76
  export WALL_E_PORT="$DEV_WALLE_PORT"
73
77
  export CTM_DATA_DIR="$DEV_DIR"
74
78
  export WALL_E_DATA_DIR="$DEV_DIR"
79
+ export WALLE_SESSIONS_DIR="$DEV_DIR/sessions"
80
+ export WALL_E_SESSIONS_DIR="$DEV_DIR/sessions"
75
81
  export CTM_HOST="127.0.0.1"
76
82
  export CTM_INSTANCE_TAG="dev-$DEV_CTM_PORT"
77
83
  export OAUTH_PROXY_PORT="$DEV_OAUTH_PROXY_PORT"
@@ -79,7 +85,7 @@ export OAUTH_PROXY_PORT="$DEV_OAUTH_PROXY_PORT"
79
85
  # Source the rest of .env (API keys, owner name, etc.)
80
86
  if [[ -f "$ROOT/.env" ]]; then
81
87
  set -a
82
- source <(grep -v '^#' "$ROOT/.env" | grep -vE '^(CTM_PORT|WALL_E_PORT|CTM_DATA_DIR|WALL_E_DATA_DIR|CTM_HOST|CTM_INSTANCE_TAG|OAUTH_PROXY_PORT)=' | grep '=')
88
+ source <(grep -v '^#' "$ROOT/.env" | grep -vE '^(CTM_PORT|WALL_E_PORT|CTM_DATA_DIR|WALL_E_DATA_DIR|WALLE_SESSIONS_DIR|WALL_E_SESSIONS_DIR|CTM_HOST|CTM_INSTANCE_TAG|OAUTH_PROXY_PORT)=' | grep '=')
83
89
  set +a
84
90
  fi
85
91
 
@@ -8,6 +8,9 @@ const { execFileSync } = require('child_process');
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
10
  const { atomicWriteFileSync } = require('../claude-task-manager/atomic-write');
11
+ const {
12
+ setupProviderHasRuntimeAccess,
13
+ } = require('../claude-task-manager/lib/setup-provider-config');
11
14
 
12
15
  const ROOT = path.resolve(__dirname, '..');
13
16
  const ENV_PATH = path.join(ROOT, '.env');
@@ -74,19 +77,60 @@ function runIfNeeded() {
74
77
 
75
78
  /** Returns true if setup is needed (no API key configured anywhere). Cached after first check. */
76
79
  let _needsSetupCache = undefined;
80
+
81
+ function parseEnvFile(content) {
82
+ const out = {};
83
+ for (const line of String(content || '').split('\n')) {
84
+ const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
85
+ if (!m) continue;
86
+ out[m[1]] = m[2].replace(/^['"]|['"]$/g, '');
87
+ }
88
+ return out;
89
+ }
90
+
91
+ function readEnvFile() {
92
+ if (!fs.existsSync(ENV_PATH)) return {};
93
+ try { return parseEnvFile(fs.readFileSync(ENV_PATH, 'utf8')); }
94
+ catch { return {}; }
95
+ }
96
+
97
+ function readBrainProviderState(env) {
98
+ try {
99
+ const brain = require('../wall-e/brain');
100
+ if (typeof brain.getDb === 'function') brain.getDb();
101
+ const provider = brain.getKv?.('walle_provider') || env.WALLE_PROVIDER || 'anthropic';
102
+ const row = brain.getDb().prepare(
103
+ 'SELECT api_key_encrypted, auth_method FROM model_providers WHERE type = ? AND enabled = 1 ORDER BY updated_at DESC LIMIT 1'
104
+ ).get(provider);
105
+ return {
106
+ provider,
107
+ authMethod: row?.auth_method || brain.getProviderAuthMethod?.(provider) || env.WALLE_AUTH_METHOD || '',
108
+ hasStoredKey: !!row?.api_key_encrypted,
109
+ };
110
+ } catch {
111
+ return null;
112
+ }
113
+ }
114
+
77
115
  function needsSetup() {
78
116
  if (_needsSetupCache !== undefined) return _needsSetupCache;
79
- // Check process.env first (may be set via Portkey, environment, etc.)
80
- if (process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_BASE_URL) {
81
- _needsSetupCache = false;
82
- return false;
83
- }
84
117
  if (!fs.existsSync(ENV_PATH)) {
85
- _needsSetupCache = true;
86
- return true;
118
+ const provider = process.env.WALLE_PROVIDER || 'anthropic';
119
+ _needsSetupCache = !setupProviderHasRuntimeAccess({
120
+ type: provider,
121
+ env: process.env,
122
+ authMethod: process.env.WALLE_AUTH_METHOD || '',
123
+ hasStoredKey: false,
124
+ });
125
+ return _needsSetupCache;
87
126
  }
88
- const content = fs.readFileSync(ENV_PATH, 'utf8');
89
- _needsSetupCache = !content.match(/^ANTHROPIC_API_KEY=\S+/m);
127
+ const fileEnv = readEnvFile();
128
+ const env = { ...fileEnv, ...process.env };
129
+ const dbState = readBrainProviderState(env);
130
+ const provider = dbState?.provider || env.WALLE_PROVIDER || 'anthropic';
131
+ const authMethod = dbState?.authMethod || env.WALLE_AUTH_METHOD || '';
132
+ const hasStoredKey = !!dbState?.hasStoredKey;
133
+ _needsSetupCache = !setupProviderHasRuntimeAccess({ type: provider, env, authMethod, hasStoredKey });
90
134
  return _needsSetupCache;
91
135
  }
92
136
 
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { pipeline } = require('stream/promises');
7
+
8
+ async function copyFileStream(src, dest) {
9
+ await fs.promises.mkdir(path.dirname(dest), { recursive: true });
10
+ await pipeline(fs.createReadStream(src), fs.createWriteStream(dest));
11
+ }
12
+
13
+ async function main() {
14
+ const [srcDir, destDir] = process.argv.slice(2);
15
+ if (!srcDir || !destDir) {
16
+ console.error('usage: sync-images.js <srcDir> <destDir>');
17
+ process.exit(2);
18
+ }
19
+ if (!fs.existsSync(srcDir)) {
20
+ console.log(` Images: source missing (${srcDir}), skipped`);
21
+ return;
22
+ }
23
+
24
+ await fs.promises.rm(destDir, { recursive: true, force: true });
25
+ await fs.promises.mkdir(destDir, { recursive: true });
26
+
27
+ const entries = await fs.promises.readdir(srcDir, { withFileTypes: true });
28
+ let copied = 0;
29
+ let skipped = 0;
30
+ for (const entry of entries) {
31
+ if (!entry.isFile()) continue;
32
+ const src = path.join(srcDir, entry.name);
33
+ const dest = path.join(destDir, entry.name);
34
+ try {
35
+ await copyFileStream(src, dest);
36
+ const stat = await fs.promises.stat(dest);
37
+ if (stat.size === 0) throw new Error('copied zero-byte file');
38
+ copied += 1;
39
+ } catch (err) {
40
+ skipped += 1;
41
+ try { await fs.promises.unlink(dest); } catch {}
42
+ if (skipped <= 5) {
43
+ console.warn(` Images: skipped ${entry.name}: ${err.message}`);
44
+ }
45
+ }
46
+ }
47
+ const suffix = skipped > 5 ? ` (${skipped - 5} more skipped)` : '';
48
+ console.log(` Images: synced ${copied}, skipped ${skipped}${suffix}`);
49
+ }
50
+
51
+ main().catch((err) => {
52
+ console.error(` Images: warning - image sync failed: ${err.message}`);
53
+ });
@@ -0,0 +1,17 @@
1
+ # Builder Journal
2
+
3
+ ## 2026-04-29 — Surface Wall-E AI provider failures
4
+
5
+ - Added structured `AI_PROVIDER_ERROR` classification for provider failures, including rate limit, quota, auth, model unavailable, timeout, network, context-window, and unavailable-provider cases.
6
+ - Threaded provider error metadata through Wall-E chat, `/api/wall-e/chat` SSE/JSON responses, CTM Wall-E session WebSocket errors, JSONL error parts, and service alerts.
7
+ - Updated Wall-E chat UX with a persistent provider-failure banner, inline diagnostic message, setup action, dismiss control, and collapsible raw provider details.
8
+ - Updated CTM Wall-E session error notices to render the same provider diagnostic shape instead of collapsing it into a generic string.
9
+ - Added focused node tests for provider classification and chat alerting, plus a Playwright rendering scenario for mocked provider 429 errors.
10
+
11
+ ## 2026-04-29 — Make default-provider switching feel instant
12
+
13
+ - Found the slow path: CTM wrote the new default, then restarted Wall-E so its cached default client would reload, while the UI waited for the whole request before changing the star.
14
+ - Changed `/api/setup/set-default` to call Wall-E's live setup config endpoint and only schedule a Wall-E restart if that live update fails.
15
+ - Made setup default stars optimistic with rollback on failure, while keeping the selected star visually selected during the in-flight save.
16
+ - Raised contrast on provider card borders, default stars, and toggle tracks so the inline styles no longer wash out the controls.
17
+ - Added a Playwright setup-card regression for optimistic default switching and computed control colors.
@@ -7,6 +7,7 @@ const queueEngine = require('./queue-engine');
7
7
  const harvest = require('./prompt-harvest');
8
8
  const permissionSync = require('./lib/permission-sync');
9
9
  const walleClient = require('./lib/walle-client');
10
+ const claudeDesktopSessions = require('./lib/claude-desktop-sessions');
10
11
  // AI search uses direct HTTP calls to Claude API (supports Portkey proxy)
11
12
 
12
13
  // Embed a prompt (async, fire-and-forget)
@@ -593,26 +594,80 @@ function handleGetImage(req, res, url) {
593
594
  }
594
595
 
595
596
  function handleServeImage(req, res, url) {
596
- const rawFilename = url.pathname.replace('/api/images/file/', '');
597
+ let rawFilename = url.pathname.replace('/api/images/file/', '');
598
+ try { rawFilename = decodeURIComponent(rawFilename); } catch {}
597
599
  const safeFilename = path.basename(rawFilename);
600
+ const isUsableImageFile = (p) => {
601
+ try { return fs.existsSync(p) && fs.statSync(p).isFile() && fs.statSync(p).size > 0; }
602
+ catch { return false; }
603
+ };
604
+ const canReadImageFile = (p) => {
605
+ let fd = null;
606
+ try {
607
+ fd = fs.openSync(p, 'r');
608
+ const probe = Buffer.alloc(1);
609
+ return fs.readSync(fd, probe, 0, 1, 0) > 0;
610
+ } catch {
611
+ return false;
612
+ } finally {
613
+ if (fd !== null) { try { fs.closeSync(fd); } catch {} }
614
+ }
615
+ };
616
+ const serveImagePlaceholder = () => {
617
+ const svg = Buffer.from(
618
+ '<svg xmlns="http://www.w3.org/2000/svg" width="320" height="180" viewBox="0 0 320 180">' +
619
+ '<rect width="320" height="180" rx="8" fill="#1f2335"/>' +
620
+ '<text x="160" y="92" text-anchor="middle" font-family="sans-serif" font-size="14" fill="#7aa2f7">Image unavailable</text>' +
621
+ '</svg>'
622
+ );
623
+ res.writeHead(200, {
624
+ 'Content-Type': 'image/svg+xml',
625
+ 'Content-Length': svg.length,
626
+ 'Cache-Control': 'no-store',
627
+ });
628
+ res.end(svg);
629
+ };
630
+ const mimeMap = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml' };
631
+ const requestedExt = path.extname(safeFilename).toLowerCase();
632
+ if (!mimeMap[requestedExt]) {
633
+ serveImagePlaceholder(); return;
634
+ }
598
635
  let filePath = path.join(db.DEFAULT_IMAGES_DIR, safeFilename);
599
- if (!fs.existsSync(filePath)) {
600
- // Filename might be the original upload name look up actual file_path from DB
601
- const img = db.getDb().prepare('SELECT file_path FROM images WHERE filename = ? LIMIT 1').get(safeFilename);
602
- const resolvedImgPath = img ? path.resolve(img.file_path) : null;
603
- if (resolvedImgPath && resolvedImgPath.startsWith(path.resolve(db.DEFAULT_IMAGES_DIR) + path.sep) && fs.existsSync(resolvedImgPath)) {
604
- filePath = resolvedImgPath;
605
- } else {
606
- res.writeHead(404); res.end('Not found'); return;
636
+ if (!isUsableImageFile(filePath)) {
637
+ // Filename may be either the original upload name or the stored hash
638
+ // basename embedded in markdown/session history. Resolve only paths that
639
+ // came from the images table and whose basename matches the request.
640
+ const imgs = db.getDb().prepare(`
641
+ SELECT file_path FROM images
642
+ WHERE filename = ? OR file_path LIKE ? OR file_path LIKE ?
643
+ LIMIT 5
644
+ `).all(safeFilename, '%/' + safeFilename, '%\\' + safeFilename);
645
+ for (const img of imgs) {
646
+ const resolvedImgPath = img && img.file_path ? path.resolve(img.file_path) : null;
647
+ if (!resolvedImgPath || path.basename(resolvedImgPath) !== safeFilename) continue;
648
+ if (isUsableImageFile(resolvedImgPath)) {
649
+ filePath = resolvedImgPath;
650
+ break;
651
+ }
607
652
  }
608
653
  }
654
+ if (!isUsableImageFile(filePath)) {
655
+ serveImagePlaceholder(); return;
656
+ }
657
+ if (!canReadImageFile(filePath)) {
658
+ serveImagePlaceholder(); return;
659
+ }
609
660
  const ext = path.extname(filePath).toLowerCase();
610
- const mimeMap = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml' };
611
661
  res.writeHead(200, {
612
- 'Content-Type': mimeMap[ext] || 'application/octet-stream',
662
+ 'Content-Type': mimeMap[ext],
613
663
  'Cache-Control': 'public, max-age=86400'
614
664
  });
615
- fs.createReadStream(filePath).pipe(res);
665
+ const stream = fs.createReadStream(filePath);
666
+ stream.on('error', () => {
667
+ if (!res.headersSent) serveImagePlaceholder();
668
+ else res.destroy();
669
+ });
670
+ stream.pipe(res);
616
671
  }
617
672
 
618
673
  async function handleUpdateAnnotations(req, res, url) {
@@ -977,6 +1032,36 @@ async function _importCompactPair(parsed, jsonlPath, bakPath, jsonlSize, bakSize
977
1032
 
978
1033
  async function importSessionFile(filePath, projectPath, projectEntry) {
979
1034
  const parsed = parseSessionFile(filePath, projectPath, projectEntry);
1035
+ if (parsed.agent === claudeDesktopSessions.DESKTOP_AGENT) {
1036
+ const messages = claudeDesktopSessions.getMessages(parsed.sessionId) || [];
1037
+ if (messages.length === 0) return false;
1038
+ const userMessages = messages.filter(m => m.role === 'user');
1039
+ const assistantMessages = messages.filter(m => m.role === 'assistant');
1040
+ const firstUser = userMessages[0]?.text || parsed.firstMessage || '';
1041
+ const lastUser = userMessages[userMessages.length - 1]?.text || parsed.lastUserContent || firstUser;
1042
+ const firstAssistant = assistantMessages[0]?.text || parsed.firstAssistantText || '';
1043
+ const existing = db.getSessionConversation(parsed.sessionId);
1044
+ if (existing && existing.file_size === parsed.fileSize && existing.model_provider) return false;
1045
+ db.importSessionConversation({
1046
+ session_id: parsed.sessionId,
1047
+ project_path: parsed.project,
1048
+ messages,
1049
+ user_msg_count: userMessages.length,
1050
+ assistant_msg_count: assistantMessages.length,
1051
+ title: parsed.title || (existing && existing.title) || '',
1052
+ first_message: firstUser,
1053
+ last_user_content: lastUser,
1054
+ first_assistant_text: firstAssistant,
1055
+ rename_name: '',
1056
+ git_branch: '',
1057
+ file_size: parsed.fileSize,
1058
+ session_created_at: parsed.timestamp,
1059
+ hostname: parsed.hostname,
1060
+ model_provider: parsed.modelProvider || (existing && existing.model_provider) || '',
1061
+ model_id: parsed.modelId || (existing && existing.model_id) || '',
1062
+ });
1063
+ return true;
1064
+ }
980
1065
  if (parsed.isEmpty) return false;
981
1066
 
982
1067
  // Compact-pair detection: Claude Code's /compact rotates <id>.jsonl →
@@ -1108,7 +1193,7 @@ async function runIncrementalConversationImport() {
1108
1193
  // several imports in the same event-loop turn.
1109
1194
  for (const { filePath, projectPath, projectEntry } of allFiles) {
1110
1195
  try {
1111
- const stat = await fsp.stat(filePath);
1196
+ const stat = await fsp.stat(claudeDesktopSessions.sourcePathForStat(filePath));
1112
1197
  if (stat.mtimeMs <= lastScanAt) continue;
1113
1198
  scanned++;
1114
1199
  if (await importSessionFile(filePath, projectPath, projectEntry)) imported++;
@@ -4,6 +4,10 @@ const fs = require('fs');
4
4
  const db = require('./db');
5
5
  const gitUtils = require('./git-utils');
6
6
 
7
+ const REVIEW_SEVERITIES = new Set(['comment', 'suggestion', 'issue', 'nit', 'question']);
8
+ const REVIEW_STATUSES = new Set(['open', 'resolved']);
9
+ const REVIEW_SIDES = new Set(['old', 'new']);
10
+
7
11
  // --- Helpers ---
8
12
  function isValidProjectPath(p) {
9
13
  if (!p || typeof p !== 'string') return false;
@@ -36,6 +40,69 @@ function readBody(req, limit = 1024 * 1024) {
36
40
  });
37
41
  }
38
42
 
43
+ function normalizeReviewSeverity(value) {
44
+ value = String(value || 'comment').trim().toLowerCase();
45
+ return REVIEW_SEVERITIES.has(value) ? value : 'comment';
46
+ }
47
+
48
+ function normalizeReviewStatus(value) {
49
+ value = String(value || 'open').trim().toLowerCase();
50
+ if (!REVIEW_STATUSES.has(value)) throw new Error('Invalid review comment status');
51
+ return value;
52
+ }
53
+
54
+ function normalizeReviewSide(value) {
55
+ value = String(value || 'new').trim().toLowerCase();
56
+ return REVIEW_SIDES.has(value) ? value : 'new';
57
+ }
58
+
59
+ function positiveLineNumber(value, fieldName) {
60
+ const n = Number(value);
61
+ if (!Number.isInteger(n) || n < 1) throw new Error(`${fieldName} must be a positive integer`);
62
+ return n;
63
+ }
64
+
65
+ function sanitizeReviewCommentInput(reviewId, body) {
66
+ body = body || {};
67
+ const filePath = String(body.file_path || '').trim();
68
+ const text = String(body.body || '').trim();
69
+ if (!filePath) throw new Error('file_path required');
70
+ if (!text) throw new Error('body required');
71
+ if (filePath.length > 4096) throw new Error('file_path too long');
72
+ if (text.length > 20000) throw new Error('body too long');
73
+ const lineStart = positiveLineNumber(body.line_start, 'line_start');
74
+ let lineEnd = body.line_end == null || body.line_end === '' ? lineStart : positiveLineNumber(body.line_end, 'line_end');
75
+ if (lineEnd < lineStart) lineEnd = lineStart;
76
+ return {
77
+ review_id: reviewId,
78
+ file_path: filePath,
79
+ line_start: lineStart,
80
+ line_end: lineEnd,
81
+ side: normalizeReviewSide(body.side),
82
+ body: text,
83
+ severity: normalizeReviewSeverity(body.severity),
84
+ ai_generated: !!body.ai_generated,
85
+ };
86
+ }
87
+
88
+ function sanitizeReviewCommentUpdate(body) {
89
+ body = body || {};
90
+ const update = {};
91
+ if (Object.prototype.hasOwnProperty.call(body, 'body')) {
92
+ const text = String(body.body || '').trim();
93
+ if (!text) throw new Error('body required');
94
+ if (text.length > 20000) throw new Error('body too long');
95
+ update.body = text;
96
+ }
97
+ if (Object.prototype.hasOwnProperty.call(body, 'severity')) {
98
+ update.severity = normalizeReviewSeverity(body.severity);
99
+ }
100
+ if (Object.prototype.hasOwnProperty.call(body, 'status')) {
101
+ update.status = normalizeReviewStatus(body.status);
102
+ }
103
+ return update;
104
+ }
105
+
39
106
  // --- Change Detection for Badge Polling ---
40
107
  // Track last known diff stats per project to detect changes
41
108
  const changeTracker = new Map(); // projectPath -> { files: [...], hash: string }
@@ -218,8 +285,9 @@ function handleReviewApi(req, res, url) {
218
285
  const commentsMatch = p.match(/^\/api\/reviews\/(\d+)\/comments$/);
219
286
  if (commentsMatch && m === 'POST') {
220
287
  readBody(req).then(body => {
221
- body.review_id = parseInt(commentsMatch[1]);
222
- const id = db.addReviewComment(body);
288
+ const reviewId = parseInt(commentsMatch[1], 10);
289
+ if (!db.getReview(reviewId)) return jsonResponse(res, 404, { error: 'Review not found' });
290
+ const id = db.addReviewComment(sanitizeReviewCommentInput(reviewId, body));
223
291
  jsonResponse(res, 201, { id });
224
292
  }).catch(e => jsonResponse(res, 400, { error: e.message }));
225
293
  return true;
@@ -229,7 +297,7 @@ function handleReviewApi(req, res, url) {
229
297
  const commentMatch = p.match(/^\/api\/review-comments\/(\d+)$/);
230
298
  if (commentMatch && m === 'PUT') {
231
299
  readBody(req).then(body => {
232
- db.updateReviewComment(parseInt(commentMatch[1]), body);
300
+ db.updateReviewComment(parseInt(commentMatch[1], 10), sanitizeReviewCommentUpdate(body));
233
301
  jsonResponse(res, 200, { ok: true });
234
302
  }).catch(e => jsonResponse(res, 400, { error: e.message }));
235
303
  return true;
@@ -272,7 +340,8 @@ function composeReviewPrompt(review) {
272
340
  const lineRef = c.line_end && c.line_end !== c.line_start
273
341
  ? `Lines ${c.line_start}-${c.line_end}`
274
342
  : `Line ${c.line_start}`;
275
- const severity = c.severity !== 'comment' ? ` [${c.severity}]` : '';
343
+ const normalizedSeverity = normalizeReviewSeverity(c.severity);
344
+ const severity = normalizedSeverity !== 'comment' ? ` [${normalizedSeverity}]` : '';
276
345
  lines.push(`**${lineRef}**${severity}: ${c.body}\n`);
277
346
  }
278
347
  }
@@ -284,4 +353,12 @@ function composeReviewPrompt(review) {
284
353
  return lines.join('\n');
285
354
  }
286
355
 
287
- module.exports = { handleReviewApi, checkForChanges };
356
+ module.exports = {
357
+ handleReviewApi,
358
+ checkForChanges,
359
+ composeReviewPrompt,
360
+ _normalizeReviewSeverity: normalizeReviewSeverity,
361
+ _normalizeReviewStatus: normalizeReviewStatus,
362
+ _sanitizeReviewCommentInput: sanitizeReviewCommentInput,
363
+ _sanitizeReviewCommentUpdate: sanitizeReviewCommentUpdate,
364
+ };
@@ -2593,11 +2593,30 @@ function deleteReview(id) {
2593
2593
  getDb().prepare('DELETE FROM code_reviews WHERE id = ?').run(id);
2594
2594
  }
2595
2595
 
2596
+ const REVIEW_COMMENT_SEVERITIES = new Set(['comment', 'suggestion', 'issue', 'nit', 'question']);
2597
+ const REVIEW_COMMENT_STATUSES = new Set(['open', 'resolved']);
2598
+ const REVIEW_COMMENT_SIDES = new Set(['old', 'new']);
2599
+
2600
+ function normalizeReviewCommentSeverity(severity) {
2601
+ severity = String(severity || 'comment').trim().toLowerCase();
2602
+ return REVIEW_COMMENT_SEVERITIES.has(severity) ? severity : 'comment';
2603
+ }
2604
+
2605
+ function normalizeReviewCommentStatus(status) {
2606
+ status = String(status || 'open').trim().toLowerCase();
2607
+ return REVIEW_COMMENT_STATUSES.has(status) ? status : 'open';
2608
+ }
2609
+
2610
+ function normalizeReviewCommentSide(side) {
2611
+ side = String(side || 'new').trim().toLowerCase();
2612
+ return REVIEW_COMMENT_SIDES.has(side) ? side : 'new';
2613
+ }
2614
+
2596
2615
  function addReviewComment({ review_id, file_path, line_start, line_end, side, body, severity, ai_generated }) {
2597
2616
  const result = getDb().prepare(
2598
2617
  `INSERT INTO review_comments (review_id, file_path, line_start, line_end, side, body, severity, ai_generated)
2599
2618
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
2600
- ).run(review_id, file_path, line_start, line_end || line_start, side || 'new', body, severity || 'comment', ai_generated ? 1 : 0);
2619
+ ).run(review_id, file_path, line_start, line_end || line_start, normalizeReviewCommentSide(side), body, normalizeReviewCommentSeverity(severity), ai_generated ? 1 : 0);
2601
2620
  // Update comment count
2602
2621
  const count = getDb().prepare('SELECT COUNT(*) as n FROM review_comments WHERE review_id = ?').get(review_id);
2603
2622
  getDb().prepare('UPDATE code_reviews SET comment_count = ? WHERE id = ?').run(count.n, review_id);
@@ -2608,8 +2627,8 @@ function updateReviewComment(id, { body, severity, status }) {
2608
2627
  const sets = [];
2609
2628
  const params = [];
2610
2629
  if (body !== undefined) { sets.push('body = ?'); params.push(body); }
2611
- if (severity !== undefined) { sets.push('severity = ?'); params.push(severity); }
2612
- if (status !== undefined) { sets.push('status = ?'); params.push(status); }
2630
+ if (severity !== undefined) { sets.push('severity = ?'); params.push(normalizeReviewCommentSeverity(severity)); }
2631
+ if (status !== undefined) { sets.push('status = ?'); params.push(normalizeReviewCommentStatus(status)); }
2613
2632
  if (sets.length === 0) return;
2614
2633
  params.push(id);
2615
2634
  getDb().prepare(`UPDATE review_comments SET ${sets.join(', ')} WHERE id = ?`).run(...params);
@@ -3513,6 +3532,13 @@ function resolveSession(id) {
3513
3532
  return null;
3514
3533
  }
3515
3534
 
3535
+ function normalizeDbCreatedAt(value) {
3536
+ if (!value) return '';
3537
+ const ms = new Date(value).getTime();
3538
+ if (!Number.isFinite(ms)) return '';
3539
+ return new Date(ms).toISOString().replace('T', ' ').replace(/\.\d+Z$/, '');
3540
+ }
3541
+
3516
3542
  /**
3517
3543
  * Merge a ctm_sessions row + agent_sessions row into a unified object
3518
3544
  * that matches the old sessions table shape (for backward compatibility).
@@ -3594,6 +3620,7 @@ function upsertSession(id, data, opts) {
3594
3620
  title: data.title || '',
3595
3621
  user_renamed: data.userRenamed ? 1 : 0,
3596
3622
  starred: data.starred ? 1 : 0,
3623
+ created_at: normalizeDbCreatedAt(data.createdAt),
3597
3624
  };
3598
3625
  const agentParams = (agentId && agentId !== '__CLEAR__') ? {
3599
3626
  agent_session_id: agentId,
@@ -3615,8 +3642,8 @@ function upsertSession(id, data, opts) {
3615
3642
  const txn = d.transaction(() => {
3616
3643
  // Upsert ctm_sessions
3617
3644
  d.prepare(`
3618
- INSERT INTO ctm_sessions (id, provider, project_path, cwd, title, user_renamed, starred)
3619
- VALUES (@id, @provider, @project_path, @cwd, @title, @user_renamed, @starred)
3645
+ INSERT INTO ctm_sessions (id, provider, project_path, cwd, title, user_renamed, starred, created_at)
3646
+ VALUES (@id, @provider, @project_path, @cwd, @title, @user_renamed, @starred, COALESCE(NULLIF(@created_at, ''), datetime('now')))
3620
3647
  ON CONFLICT(id) DO UPDATE SET
3621
3648
  provider = COALESCE(NULLIF(excluded.provider, ''), ctm_sessions.provider),
3622
3649
  project_path = COALESCE(NULLIF(excluded.project_path, ''), ctm_sessions.project_path),