create-walle 0.9.18 → 0.9.20
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/README.md +6 -6
- package/package.json +2 -2
- package/template/claude-task-manager/api-prompts.js +176 -29
- package/template/claude-task-manager/api-reviews.js +56 -3
- package/template/claude-task-manager/approval-agent.js +9 -2
- package/template/claude-task-manager/db.js +178 -55
- package/template/claude-task-manager/docs/conversation-import-freshness.md +24 -0
- package/template/claude-task-manager/docs/large-jsonl-transcript-ingestion.md +128 -0
- package/template/claude-task-manager/docs/mobile-live-streaming.md +59 -6
- package/template/claude-task-manager/docs/mobile-unified-live-timeline-design.md +20 -4
- package/template/claude-task-manager/docs/mobile-walle-chat-design.md +56 -0
- package/template/claude-task-manager/docs/obsidian-resource-map-design.md +141 -0
- package/template/claude-task-manager/docs/session-management-architecture.md +38 -1
- package/template/claude-task-manager/docs/session-search-corpus-architecture.md +121 -0
- package/template/claude-task-manager/docs/session-timeline-consistency-design.md +107 -0
- package/template/claude-task-manager/docs/smart-session-search-design.md +10 -3
- package/template/claude-task-manager/docs/walle-transcript-display-projection.md +42 -0
- package/template/claude-task-manager/lib/auth-rules.js +1 -0
- package/template/claude-task-manager/lib/background-llm.js +1 -0
- package/template/claude-task-manager/lib/coding-agent-models.js +2 -0
- package/template/claude-task-manager/lib/document-review.js +250 -0
- package/template/claude-task-manager/lib/remote-relay-protocol.js +16 -1
- package/template/claude-task-manager/lib/resource-links.js +500 -0
- package/template/claude-task-manager/lib/session-history.js +158 -17
- package/template/claude-task-manager/lib/session-jobs.js +46 -25
- package/template/claude-task-manager/lib/session-standup.js +49 -2
- package/template/claude-task-manager/lib/session-stream.js +8 -5
- package/template/claude-task-manager/lib/session-timeline.js +161 -0
- package/template/claude-task-manager/lib/setup-provider-config.js +9 -1
- package/template/claude-task-manager/lib/standup-incremental.js +144 -0
- package/template/claude-task-manager/lib/tailscale-setup.js +2 -1
- package/template/claude-task-manager/lib/transcript-store.js +647 -0
- package/template/claude-task-manager/lib/walle-ctm-history.js +74 -3
- package/template/claude-task-manager/lib/walle-external-actions.js +276 -0
- package/template/claude-task-manager/lib/walle-permission-policy.js +59 -0
- package/template/claude-task-manager/public/css/reviews.css +124 -0
- package/template/claude-task-manager/public/css/setup.css +48 -0
- package/template/claude-task-manager/public/css/walle-session.css +153 -2
- package/template/claude-task-manager/public/index.html +957 -86
- package/template/claude-task-manager/public/js/message-renderer.js +285 -12
- package/template/claude-task-manager/public/js/prompts.js +13 -1
- package/template/claude-task-manager/public/js/reviews.js +467 -18
- package/template/claude-task-manager/public/js/setup.js +145 -16
- package/template/claude-task-manager/public/js/stream-view.js +61 -16
- package/template/claude-task-manager/public/js/walle-session.js +453 -73
- package/template/claude-task-manager/public/js/walle.js +99 -22
- package/template/claude-task-manager/public/m/app.css +1313 -85
- package/template/claude-task-manager/public/m/app.js +3196 -286
- package/template/claude-task-manager/public/m/claim.html +1 -1
- package/template/claude-task-manager/public/m/index.html +38 -15
- package/template/claude-task-manager/public/m/sw.js +9 -2
- package/template/claude-task-manager/server.js +1573 -513
- package/template/claude-task-manager/session-integrity.js +130 -8
- package/template/claude-task-manager/session-search-ranking.js +79 -0
- package/template/claude-task-manager/session-utils.js +2 -1
- package/template/claude-task-manager/workers/headless-term-worker.js +31 -0
- package/template/docs/designs/2026-05-17-portkey-gateway-provider-ux.md +154 -0
- package/template/docs/designs/2026-05-18-walle-external-action-approval.md +57 -0
- package/template/package.json +1 -1
- package/template/wall-e/api-walle.js +74 -61
- package/template/wall-e/auth/flow-manager.js +133 -0
- package/template/wall-e/auth/provider-flows.js +114 -0
- package/template/wall-e/brain.js +2 -2
- package/template/wall-e/chat/capability-resolver.js +1 -0
- package/template/wall-e/chat.js +876 -64
- package/template/wall-e/coding/model-message.js +3 -2
- package/template/wall-e/coding/permission-service.js +1 -1
- package/template/wall-e/coding/provider-transform.js +1 -1
- package/template/wall-e/coding-orchestrator.js +5 -1
- package/template/wall-e/coding-prompts.js +3 -0
- package/template/wall-e/context/context-builder.js +3 -3
- package/template/wall-e/deploy.sh +1 -1
- package/template/wall-e/docs/external-action-controller.md +97 -0
- package/template/wall-e/eval/chat-eval.js +1 -1
- package/template/wall-e/eval/coding-agent-real.js +2 -0
- package/template/wall-e/eval/eval-orchestrator.js +2 -0
- package/template/wall-e/eval/head-to-head.js +5 -0
- package/template/wall-e/eval/provider-normalizer.js +1 -0
- package/template/wall-e/eval/run-coding-agent-real.js +1 -1
- package/template/wall-e/eval/run-eval.js +11 -1
- package/template/wall-e/evaluation/self-critique.js +1 -0
- package/template/wall-e/evaluation/tier-selector.js +1 -0
- package/template/wall-e/external-action-controller.js +419 -0
- package/template/wall-e/http/chat-api.js +18 -2
- package/template/wall-e/http/model-admin.js +20 -4
- package/template/wall-e/llm/anthropic.js +1 -1
- package/template/wall-e/llm/client.js +17 -7
- package/template/wall-e/llm/coding-availability.js +2 -1
- package/template/wall-e/llm/index.js +1 -0
- package/template/wall-e/llm/moonshot.js +149 -0
- package/template/wall-e/llm/moonshot.plugin.json +22 -0
- package/template/wall-e/llm/openai.js +5 -2
- package/template/wall-e/llm/portkey.js +124 -0
- package/template/wall-e/llm/provider-detector.js +24 -2
- package/template/wall-e/llm/provider-error.js +1 -0
- package/template/wall-e/llm/supported-models.js +80 -0
- package/template/wall-e/llm/text-tool-calls.js +103 -6
- package/template/wall-e/runtime/execution-trace.js +17 -2
- package/template/wall-e/runtime/live-capabilities.js +6 -6
- package/template/wall-e/scripts/smoke-coding-agent-jsonl.js +5 -0
- package/template/wall-e/skills/_bundled/gws-workspace/setup.js +92 -22
- package/template/wall-e/skills/skill-planner.js +1 -1
- package/template/wall-e/tools/local-tools.js +1358 -50
- package/template/wall-e/tools/permission-checker.js +1 -1
- package/template/wall-e/tools/permission-rules.js +1 -0
- package/template/wall-e/tools/slack-mcp.js +2 -2
- package/template/website/index.html +10 -9
- package/template/wall-e/docs/ab-screenshots/sl-web-v2-DESIGN-SPEC.md +0 -131
- package/template/wall-e/docs/ab-screenshots/sl-web-verdict.json +0 -44
- package/template/wall-e/docs/frontend-design-ab-test.md +0 -235
- package/template/wall-e/scripts/ab-rebuild-shanni.js +0 -248
- package/template/wall-e/scripts/ab-visual-review-shanni.js +0 -102
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# create-walle
|
|
2
2
|
|
|
3
|
-
Set up **CTM + Wall-E** in one command — a browser-based command center for AI coding agents, phone access, and a personal AI agent that builds a searchable second brain from your work life.
|
|
3
|
+
Set up **CTM + Wall-E** in one command — a browser-based command center for AI coding agents, remote phone access, review workflows, and a personal AI agent that builds a searchable second brain from your work life.
|
|
4
4
|
|
|
5
5
|
## What You Get
|
|
6
6
|
|
|
@@ -12,8 +12,8 @@ A web dashboard for running and managing AI coding sessions across multiple prov
|
|
|
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
|
-
- **Phone Access** — Pair your phone with a QR code and use a mobile CTM UI over Microsoft Dev Tunnels, Tailscale, Cloudflare Tunnel, or Walle Remote
|
|
16
|
-
- **Code Review** —
|
|
15
|
+
- **Remote Phone Access** — Pair your phone with a QR code and use a mobile CTM UI over Microsoft Dev Tunnels, Tailscale, Cloudflare Tunnel, or Walle Remote
|
|
16
|
+
- **Code & Doc Review** — Review git diffs and Markdown docs side by side, add anchored comments, and send feedback into an agent session or queue
|
|
17
17
|
- **Model Registry** — Manage providers (Anthropic, OpenAI, Google, DeepSeek, Ollama, LM Studio, MLX, and CLI subscription providers), compare pricing, switch models per session
|
|
18
18
|
- **Session Insights** — Analyze patterns across sessions to optimize prompts and workflows
|
|
19
19
|
|
|
@@ -35,7 +35,7 @@ An always-on AI agent that learns from your Slack, email, calendar, and coding s
|
|
|
35
35
|
npx create-walle install ./walle
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
This copies the project, installs dependencies, auto-detects your name and timezone, and starts the server. Open **http://localhost:3456** to finish setup in the browser, then pair your phone from Setup if you want
|
|
38
|
+
This copies the project, installs dependencies, auto-detects your name and timezone, and starts the server. Open **http://localhost:3456** to finish setup in the browser, then pair your phone from Setup if you want remote access.
|
|
39
39
|
|
|
40
40
|
## Commands
|
|
41
41
|
|
|
@@ -62,7 +62,7 @@ On first launch, the browser setup page guides you through:
|
|
|
62
62
|
1. **Owner name** — auto-detected from `git config`
|
|
63
63
|
2. **API key** — enter manually, or click "Auto-detect" to find it from your shell environment, Claude Code OAuth, or corporate devbox
|
|
64
64
|
3. **Integrations** — connect Slack (OAuth), email and calendar auto-detected on macOS
|
|
65
|
-
4. **
|
|
65
|
+
4. **Remote phone access** — optional QR pairing with Microsoft Dev Tunnels, Tailscale, Cloudflare Tunnel, or Walle Remote
|
|
66
66
|
|
|
67
67
|
## Custom Port
|
|
68
68
|
|
|
@@ -78,7 +78,7 @@ Everything runs locally. CTM serves the dashboard, Wall-E runs as a background a
|
|
|
78
78
|
|
|
79
79
|
| Component | Default Port | What it does |
|
|
80
80
|
|-----------|-------------|--------------|
|
|
81
|
-
| CTM | 3456 | Dashboard, multi-agent terminal, prompt editor, queue, model registry, code review, phone UI |
|
|
81
|
+
| CTM | 3456 | Dashboard, multi-agent terminal, prompt editor, queue, model registry, code/doc review, remote phone UI |
|
|
82
82
|
| Wall-E | 3457 | AI agent, brain database, skills engine, multi-model chat API |
|
|
83
83
|
|
|
84
84
|
## Links
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-walle",
|
|
3
|
-
"version": "0.9.
|
|
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, phone access, and an agent that learns from Slack, email & calendar.",
|
|
3
|
+
"version": "0.9.20",
|
|
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, remote phone access, code/doc review, and an agent that learns from Slack, email & calendar.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"create-walle": "bin/create-walle.js"
|
|
7
7
|
},
|
|
@@ -9,10 +9,19 @@ const permissionSync = require('./lib/permission-sync');
|
|
|
9
9
|
const walleClient = require('./lib/walle-client');
|
|
10
10
|
const claudeDesktopSessions = require('./lib/claude-desktop-sessions');
|
|
11
11
|
const skillAutocomplete = require('./lib/skill-autocomplete');
|
|
12
|
+
const resourceLinks = require('./lib/resource-links');
|
|
13
|
+
const {
|
|
14
|
+
ingestJsonlFile,
|
|
15
|
+
normalizeProvider: normalizeTranscriptProvider,
|
|
16
|
+
sourceIdFromPath: transcriptSourceIdFromPath,
|
|
17
|
+
} = require('./lib/transcript-store');
|
|
12
18
|
// AI search uses direct HTTP calls to Claude API (supports Portkey proxy)
|
|
13
19
|
|
|
14
20
|
let dbMaintenanceRunner = null;
|
|
15
21
|
const MOBILE_ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
|
|
22
|
+
const TRANSCRIPT_IMPORT_MAX_BYTES = Math.max(1024 * 1024, Number(process.env.CTM_TRANSCRIPT_IMPORT_MAX_BYTES || 10 * 1024 * 1024));
|
|
23
|
+
const TRANSCRIPT_IMPORT_LARGE_FILE_BYTES = Math.max(1024 * 1024, Number(process.env.CTM_TRANSCRIPT_IMPORT_LARGE_FILE_BYTES || 64 * 1024 * 1024));
|
|
24
|
+
const CONVERSATION_IMPORT_RETRY_AFTER_MS = 30 * 1000;
|
|
16
25
|
|
|
17
26
|
function setDbMaintenanceRunner(fn) {
|
|
18
27
|
dbMaintenanceRunner = typeof fn === 'function' ? fn : null;
|
|
@@ -165,6 +174,8 @@ function handlePromptApi(req, res, url) {
|
|
|
165
174
|
|
|
166
175
|
// --- Prompt Usage by Session ---
|
|
167
176
|
if (p === '/api/session-prompts' && m === 'GET') return handleSessionPrompts(req, res, url);
|
|
177
|
+
if (p === '/api/resource-links' && m === 'GET') return handleResourceLinks(req, res, url);
|
|
178
|
+
if (p === '/api/resource-views' && m === 'GET') return handleResourceViews(req, res, url);
|
|
168
179
|
|
|
169
180
|
// --- Settings ---
|
|
170
181
|
if (p === '/api/settings' && m === 'GET') return handleGetSettings(req, res, url);
|
|
@@ -389,6 +400,31 @@ function handleSessionPrompts(req, res, url) {
|
|
|
389
400
|
jsonResponse(res, 200, rows);
|
|
390
401
|
}
|
|
391
402
|
|
|
403
|
+
function handleResourceLinks(req, res, url) {
|
|
404
|
+
try {
|
|
405
|
+
const type = url.searchParams.get('type') || '';
|
|
406
|
+
const id = url.searchParams.get('id') || '';
|
|
407
|
+
const limit = parseInt(url.searchParams.get('limit') || '30', 10);
|
|
408
|
+
const data = resourceLinks.getResourceContext(db, { type, id, limit });
|
|
409
|
+
jsonResponse(res, 200, data);
|
|
410
|
+
} catch (e) {
|
|
411
|
+
if (e.code === 'ENOENT') return jsonResponse(res, 404, { error: e.message });
|
|
412
|
+
if (e.code === 'EINVAL') return jsonResponse(res, 400, { error: e.message });
|
|
413
|
+
console.error('[resource-links] request failed:', e.message, e.stack);
|
|
414
|
+
jsonResponse(res, 500, { error: 'Failed to build resource map' });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function handleResourceViews(req, res, url) {
|
|
419
|
+
try {
|
|
420
|
+
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
|
|
421
|
+
jsonResponse(res, 200, resourceLinks.getResourceViews(db, { limit }));
|
|
422
|
+
} catch (e) {
|
|
423
|
+
console.error('[resource-links] views failed:', e.message, e.stack);
|
|
424
|
+
jsonResponse(res, 500, { error: 'Failed to build resource views' });
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
392
428
|
// --- Folders ---
|
|
393
429
|
function handleListFolders(req, res) {
|
|
394
430
|
jsonResponse(res, 200, db.listFolders());
|
|
@@ -850,13 +886,16 @@ const fsp = require('fs').promises;
|
|
|
850
886
|
*/
|
|
851
887
|
const _RENAME_CMD_RE = /^\s*\/rename\s+(.+?)\s*$/m;
|
|
852
888
|
const _LARGE_JSONL_LINE_BYTES = 512 * 1024;
|
|
889
|
+
const _CONVERSATION_CACHE_TEXT_LIMIT = 5000;
|
|
890
|
+
const _TITLE_SIGNAL_TEXT_LIMIT = 500;
|
|
853
891
|
|
|
854
|
-
function _readJsonStringPrefix(line, marker, maxChars, fromIndex = 0) {
|
|
892
|
+
function _readJsonStringPrefix(line, marker, maxChars = Number.POSITIVE_INFINITY, fromIndex = 0) {
|
|
855
893
|
const markerIndex = line.indexOf(marker, fromIndex);
|
|
856
894
|
if (markerIndex === -1) return '';
|
|
857
895
|
let pos = markerIndex + marker.length;
|
|
858
896
|
let out = '';
|
|
859
|
-
|
|
897
|
+
const limit = Number.isFinite(maxChars) && maxChars > 0 ? maxChars : Number.POSITIVE_INFINITY;
|
|
898
|
+
while (pos < line.length && out.length < limit) {
|
|
860
899
|
const ch = line[pos++];
|
|
861
900
|
if (ch === '"') break;
|
|
862
901
|
if (ch !== '\\') {
|
|
@@ -888,9 +927,9 @@ function _parseLargeConversationLine(line) {
|
|
|
888
927
|
let text = '';
|
|
889
928
|
const textBlock = line.indexOf('"type":"text"');
|
|
890
929
|
if (textBlock !== -1) {
|
|
891
|
-
text = _readJsonStringPrefix(line, '"text":"',
|
|
930
|
+
text = _readJsonStringPrefix(line, '"text":"', Number.POSITIVE_INFINITY, textBlock);
|
|
892
931
|
} else if (!line.includes('"type":"tool_result"')) {
|
|
893
|
-
text = _readJsonStringPrefix(line, '"content":"',
|
|
932
|
+
text = _readJsonStringPrefix(line, '"content":"', Number.POSITIVE_INFINITY);
|
|
894
933
|
}
|
|
895
934
|
if (!text) return null;
|
|
896
935
|
return { role: 'user', text, timestamp };
|
|
@@ -898,11 +937,15 @@ function _parseLargeConversationLine(line) {
|
|
|
898
937
|
if (line.includes('"type":"assistant"') && line.includes('"role":"assistant"')) {
|
|
899
938
|
const parts = [];
|
|
900
939
|
let scanFrom = 0;
|
|
901
|
-
|
|
940
|
+
let compactLength = 0;
|
|
941
|
+
while (compactLength < _CONVERSATION_CACHE_TEXT_LIMIT) {
|
|
902
942
|
const textBlock = line.indexOf('"type":"text"', scanFrom);
|
|
903
943
|
if (textBlock === -1) break;
|
|
904
|
-
const text = _readJsonStringPrefix(line, '"text":"',
|
|
905
|
-
if (text)
|
|
944
|
+
const text = _readJsonStringPrefix(line, '"text":"', _CONVERSATION_CACHE_TEXT_LIMIT, textBlock);
|
|
945
|
+
if (text) {
|
|
946
|
+
parts.push(text);
|
|
947
|
+
compactLength += text.length + 1;
|
|
948
|
+
}
|
|
906
949
|
scanFrom = textBlock + 13;
|
|
907
950
|
}
|
|
908
951
|
scanFrom = 0;
|
|
@@ -916,7 +959,7 @@ function _parseLargeConversationLine(line) {
|
|
|
916
959
|
if (!parts.length) return null;
|
|
917
960
|
return {
|
|
918
961
|
role: 'assistant',
|
|
919
|
-
text: parts.join('\n').slice(0,
|
|
962
|
+
text: parts.join('\n').slice(0, _CONVERSATION_CACHE_TEXT_LIMIT),
|
|
920
963
|
timestamp,
|
|
921
964
|
parent: _readJsonStringPrefix(line, '"parentUuid":"', 80),
|
|
922
965
|
textOnly: parts.find(p => !p.startsWith('[Tool: ')) || '',
|
|
@@ -927,6 +970,7 @@ function _parseLargeConversationLine(line) {
|
|
|
927
970
|
|
|
928
971
|
async function _parseConversationContent(content) {
|
|
929
972
|
const messages = [];
|
|
973
|
+
const searchMessages = [];
|
|
930
974
|
let assistantCount = 0;
|
|
931
975
|
let firstUserContent = '';
|
|
932
976
|
let lastUserContent = '';
|
|
@@ -959,18 +1003,26 @@ async function _parseConversationContent(content) {
|
|
|
959
1003
|
if (line.length > _LARGE_JSONL_LINE_BYTES) {
|
|
960
1004
|
const large = _parseLargeConversationLine(line);
|
|
961
1005
|
if (large?.role === 'user') {
|
|
962
|
-
messages.push({ role: 'user', text: large.text.slice(0,
|
|
963
|
-
|
|
964
|
-
|
|
1006
|
+
messages.push({ role: 'user', text: large.text.slice(0, _CONVERSATION_CACHE_TEXT_LIMIT), timestamp: large.timestamp });
|
|
1007
|
+
searchMessages.push({ role: 'user', text: large.text, timestamp: large.timestamp });
|
|
1008
|
+
if (!firstUserContent) firstUserContent = large.text.slice(0, _TITLE_SIGNAL_TEXT_LIMIT);
|
|
1009
|
+
lastUserContent = large.text.slice(0, _TITLE_SIGNAL_TEXT_LIMIT);
|
|
965
1010
|
const m = large.text.match(_RENAME_CMD_RE);
|
|
966
1011
|
if (m && m[1]) renameName = m[1].slice(0, 200);
|
|
967
1012
|
} else if (large?.role === 'assistant') {
|
|
968
|
-
if (large.textOnly && !firstAssistantText) firstAssistantText = large.textOnly.slice(0,
|
|
1013
|
+
if (large.textOnly && !firstAssistantText) firstAssistantText = large.textOnly.slice(0, _TITLE_SIGNAL_TEXT_LIMIT);
|
|
969
1014
|
const lastMsg = messages[messages.length - 1];
|
|
970
1015
|
if (lastMsg && lastMsg.role === 'assistant' && lastMsg._parent === large.parent) {
|
|
971
|
-
lastMsg.text = large.text.slice(0,
|
|
1016
|
+
lastMsg.text = large.text.slice(0, _CONVERSATION_CACHE_TEXT_LIMIT);
|
|
1017
|
+
const lastSearchMsg = searchMessages[searchMessages.length - 1];
|
|
1018
|
+
if (lastSearchMsg && lastSearchMsg.role === 'assistant' && lastSearchMsg._parent === large.parent) {
|
|
1019
|
+
lastSearchMsg.text = large.text;
|
|
1020
|
+
} else {
|
|
1021
|
+
searchMessages.push({ role: 'assistant', text: large.text, timestamp: large.timestamp, _parent: large.parent });
|
|
1022
|
+
}
|
|
972
1023
|
} else {
|
|
973
|
-
messages.push({ role: 'assistant', text: large.text.slice(0,
|
|
1024
|
+
messages.push({ role: 'assistant', text: large.text.slice(0, _CONVERSATION_CACHE_TEXT_LIMIT), timestamp: large.timestamp, _parent: large.parent });
|
|
1025
|
+
searchMessages.push({ role: 'assistant', text: large.text, timestamp: large.timestamp, _parent: large.parent });
|
|
974
1026
|
assistantCount++;
|
|
975
1027
|
}
|
|
976
1028
|
}
|
|
@@ -984,9 +1036,10 @@ async function _parseConversationContent(content) {
|
|
|
984
1036
|
const text = typeof c === 'string' ? c
|
|
985
1037
|
: Array.isArray(c) ? c.filter(b => b.type === 'text').map(b => b.text).join('\n') : '';
|
|
986
1038
|
if (text) {
|
|
987
|
-
messages.push({ role: 'user', text: text.slice(0,
|
|
988
|
-
|
|
989
|
-
|
|
1039
|
+
messages.push({ role: 'user', text: text.slice(0, _CONVERSATION_CACHE_TEXT_LIMIT), timestamp: entry.timestamp });
|
|
1040
|
+
searchMessages.push({ role: 'user', text, timestamp: entry.timestamp });
|
|
1041
|
+
if (!firstUserContent) firstUserContent = text.slice(0, _TITLE_SIGNAL_TEXT_LIMIT);
|
|
1042
|
+
lastUserContent = text.slice(0, _TITLE_SIGNAL_TEXT_LIMIT);
|
|
990
1043
|
// A later /rename overrides an earlier one — last-write-wins.
|
|
991
1044
|
const m = text.match(_RENAME_CMD_RE);
|
|
992
1045
|
if (m && m[1]) renameName = m[1].slice(0, 200);
|
|
@@ -1005,13 +1058,20 @@ async function _parseConversationContent(content) {
|
|
|
1005
1058
|
// Prefer the first text-bearing assistant utterance; tool-only
|
|
1006
1059
|
// replies like "[Tool: Bash]" are not useful as a title.
|
|
1007
1060
|
const textOnly = c.filter(b => b.type === 'text' && b.text).map(b => b.text).join('\n');
|
|
1008
|
-
if (textOnly) firstAssistantText = textOnly.slice(0,
|
|
1061
|
+
if (textOnly) firstAssistantText = textOnly.slice(0, _TITLE_SIGNAL_TEXT_LIMIT);
|
|
1009
1062
|
}
|
|
1010
1063
|
const lastMsg = messages[messages.length - 1];
|
|
1011
1064
|
if (lastMsg && lastMsg.role === 'assistant' && lastMsg._parent === entry.parentUuid) {
|
|
1012
|
-
lastMsg.text = joined.slice(0,
|
|
1065
|
+
lastMsg.text = joined.slice(0, _CONVERSATION_CACHE_TEXT_LIMIT);
|
|
1066
|
+
const lastSearchMsg = searchMessages[searchMessages.length - 1];
|
|
1067
|
+
if (lastSearchMsg && lastSearchMsg.role === 'assistant' && lastSearchMsg._parent === entry.parentUuid) {
|
|
1068
|
+
lastSearchMsg.text = joined;
|
|
1069
|
+
} else {
|
|
1070
|
+
searchMessages.push({ role: 'assistant', text: joined, timestamp: entry.timestamp, _parent: entry.parentUuid });
|
|
1071
|
+
}
|
|
1013
1072
|
} else {
|
|
1014
|
-
messages.push({ role: 'assistant', text: joined.slice(0,
|
|
1073
|
+
messages.push({ role: 'assistant', text: joined.slice(0, _CONVERSATION_CACHE_TEXT_LIMIT), timestamp: entry.timestamp, _parent: entry.parentUuid });
|
|
1074
|
+
searchMessages.push({ role: 'assistant', text: joined, timestamp: entry.timestamp, _parent: entry.parentUuid });
|
|
1015
1075
|
assistantCount++;
|
|
1016
1076
|
}
|
|
1017
1077
|
}
|
|
@@ -1020,8 +1080,10 @@ async function _parseConversationContent(content) {
|
|
|
1020
1080
|
await maybeYield();
|
|
1021
1081
|
}
|
|
1022
1082
|
messages.forEach(m => delete m._parent);
|
|
1083
|
+
searchMessages.forEach(m => delete m._parent);
|
|
1023
1084
|
return {
|
|
1024
1085
|
messages,
|
|
1086
|
+
searchMessages,
|
|
1025
1087
|
assistantCount,
|
|
1026
1088
|
firstUserContent,
|
|
1027
1089
|
lastUserContent,
|
|
@@ -1089,6 +1151,9 @@ async function _importCompactPair(parsed, jsonlPath, bakPath, jsonlSize, bakSize
|
|
|
1089
1151
|
// possible mid-compact) end up in chronological order.
|
|
1090
1152
|
const allMessages = bakParsed.messages.concat(jsonlParsed.messages);
|
|
1091
1153
|
allMessages.sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || ''));
|
|
1154
|
+
const allSearchMessages = (bakParsed.searchMessages || bakParsed.messages)
|
|
1155
|
+
.concat(jsonlParsed.searchMessages || jsonlParsed.messages);
|
|
1156
|
+
allSearchMessages.sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || ''));
|
|
1092
1157
|
const signals = _conversationSignalsFromMessages(allMessages);
|
|
1093
1158
|
|
|
1094
1159
|
// Title-fallback chain: pre-compact (.bak) wins for first-message because
|
|
@@ -1108,6 +1173,7 @@ async function _importCompactPair(parsed, jsonlPath, bakPath, jsonlSize, bakSize
|
|
|
1108
1173
|
session_id: parsed.sessionId,
|
|
1109
1174
|
project_path: parsed.cwd || parsed.project,
|
|
1110
1175
|
messages: allMessages,
|
|
1176
|
+
search_messages: allSearchMessages,
|
|
1111
1177
|
user_msg_count: signals.userCount,
|
|
1112
1178
|
assistant_msg_count: signals.assistantCount,
|
|
1113
1179
|
title: parsed.title || (existing && existing.title) || '',
|
|
@@ -1134,6 +1200,25 @@ function _safeParseMessagesJson(json) {
|
|
|
1134
1200
|
}
|
|
1135
1201
|
}
|
|
1136
1202
|
|
|
1203
|
+
function _loadIndexedSessionMessages(sessionId) {
|
|
1204
|
+
const id = String(sessionId || '').trim();
|
|
1205
|
+
if (!id) return [];
|
|
1206
|
+
try {
|
|
1207
|
+
return db.getDb().prepare(
|
|
1208
|
+
`SELECT message_index, role, content, created_at
|
|
1209
|
+
FROM session_messages
|
|
1210
|
+
WHERE ctm_session_id = ? AND message_index >= 0
|
|
1211
|
+
ORDER BY message_index ASC`
|
|
1212
|
+
).all(id).map(row => ({
|
|
1213
|
+
role: row.role,
|
|
1214
|
+
text: row.content || '',
|
|
1215
|
+
timestamp: row.created_at || '',
|
|
1216
|
+
})).filter(m => m.role && m.text);
|
|
1217
|
+
} catch {
|
|
1218
|
+
return [];
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1137
1222
|
function _codexSeenUsersFromMessages(messages) {
|
|
1138
1223
|
const seen = new Set();
|
|
1139
1224
|
for (const msg of Array.isArray(messages) ? messages : []) {
|
|
@@ -1206,9 +1291,10 @@ async function _conversationImportCandidates(allFiles, lastScanAt) {
|
|
|
1206
1291
|
const cacheShrankAfterChange = !!existing && effectiveSize < existingSize && changedSinceScan;
|
|
1207
1292
|
const linkedMissingCache = linkedAgentIds.has(sessionId) && !existing;
|
|
1208
1293
|
const missingModel = !!existing && !existing.model_provider;
|
|
1294
|
+
const changedColdFile = changedSinceScan && !existing;
|
|
1209
1295
|
|
|
1210
1296
|
if (
|
|
1211
|
-
!
|
|
1297
|
+
!changedColdFile &&
|
|
1212
1298
|
!cacheBehind &&
|
|
1213
1299
|
!cacheShrankAfterChange &&
|
|
1214
1300
|
!linkedMissingCache &&
|
|
@@ -1217,12 +1303,13 @@ async function _conversationImportCandidates(allFiles, lastScanAt) {
|
|
|
1217
1303
|
continue;
|
|
1218
1304
|
}
|
|
1219
1305
|
|
|
1220
|
-
let priority =
|
|
1306
|
+
let priority = 6;
|
|
1221
1307
|
if (cacheBehind) priority = 0;
|
|
1222
1308
|
else if (linkedMissingCache) priority = 1;
|
|
1223
|
-
else if (
|
|
1224
|
-
else if (
|
|
1225
|
-
else if (
|
|
1309
|
+
else if (cacheShrankAfterChange) priority = 2;
|
|
1310
|
+
else if (hasCompactSibling && changedColdFile) priority = 3;
|
|
1311
|
+
else if (changedColdFile) priority = 4;
|
|
1312
|
+
else if (missingModel) priority = 5;
|
|
1226
1313
|
|
|
1227
1314
|
candidates.push({
|
|
1228
1315
|
filePath,
|
|
@@ -1267,6 +1354,12 @@ async function _importCodexSessionFile(parsed, filePath) {
|
|
|
1267
1354
|
const baseMessages = (prevFileSize > 0 && parsed.fileSize > prevFileSize)
|
|
1268
1355
|
? _safeParseMessagesJson(existing.messages)
|
|
1269
1356
|
: [];
|
|
1357
|
+
const indexedMessages = (prevFileSize > 0 && parsed.fileSize > prevFileSize)
|
|
1358
|
+
? _loadIndexedSessionMessages(sessionId)
|
|
1359
|
+
: [];
|
|
1360
|
+
const baseSearchMessages = (prevFileSize > 0 && parsed.fileSize > prevFileSize)
|
|
1361
|
+
? (indexedMessages.length ? indexedMessages : baseMessages)
|
|
1362
|
+
: [];
|
|
1270
1363
|
const seenUsers = _codexSeenUsersFromMessages(baseMessages);
|
|
1271
1364
|
|
|
1272
1365
|
const newMessages = [];
|
|
@@ -1279,7 +1372,9 @@ async function _importCodexSessionFile(parsed, filePath) {
|
|
|
1279
1372
|
}
|
|
1280
1373
|
|
|
1281
1374
|
const allMessages = baseMessages.concat(newMessages);
|
|
1375
|
+
const allSearchMessages = (baseSearchMessages.length ? baseSearchMessages : baseMessages).concat(newMessages);
|
|
1282
1376
|
allMessages.sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || ''));
|
|
1377
|
+
allSearchMessages.sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || ''));
|
|
1283
1378
|
const userMessages = allMessages.filter(m => m.role === 'user' && (m.text || m.content));
|
|
1284
1379
|
const assistantMessages = allMessages.filter(m => m.role === 'assistant' && (m.text || m.content));
|
|
1285
1380
|
if (allMessages.length === 0 || userMessages.length === 0) return false;
|
|
@@ -1294,11 +1389,13 @@ async function _importCodexSessionFile(parsed, filePath) {
|
|
|
1294
1389
|
: '';
|
|
1295
1390
|
const projectPath = meta.cwd || parsed.project || '';
|
|
1296
1391
|
const model = meta.model || parsed.modelId || parsed.model || '';
|
|
1392
|
+
const modelProvider = meta.model_provider || parsed.modelProvider || (existing && existing.model_provider) || 'openai';
|
|
1297
1393
|
|
|
1298
1394
|
db.importSessionConversation({
|
|
1299
1395
|
session_id: sessionId,
|
|
1300
1396
|
project_path: projectPath,
|
|
1301
1397
|
messages: allMessages,
|
|
1398
|
+
search_messages: allSearchMessages,
|
|
1302
1399
|
user_msg_count: userMessages.length,
|
|
1303
1400
|
assistant_msg_count: assistantMessages.length,
|
|
1304
1401
|
title: title || (existing && existing.title) || '',
|
|
@@ -1310,7 +1407,7 @@ async function _importCodexSessionFile(parsed, filePath) {
|
|
|
1310
1407
|
file_size: parsed.fileSize,
|
|
1311
1408
|
session_created_at: meta.timestamp || parsed.timestamp || '',
|
|
1312
1409
|
hostname: parsed.hostname,
|
|
1313
|
-
model_provider:
|
|
1410
|
+
model_provider: modelProvider,
|
|
1314
1411
|
model_id: model || (existing && existing.model_id) || '',
|
|
1315
1412
|
});
|
|
1316
1413
|
|
|
@@ -1339,8 +1436,38 @@ async function _importCodexSessionFile(parsed, filePath) {
|
|
|
1339
1436
|
return true;
|
|
1340
1437
|
}
|
|
1341
1438
|
|
|
1439
|
+
function _ingestTranscriptStoreForParsedFile(filePath, parsed) {
|
|
1440
|
+
try {
|
|
1441
|
+
const size = Number(parsed?.fileSize || fs.statSync(filePath).size || 0);
|
|
1442
|
+
const agentSessionId = String(parsed?.sessionId || transcriptSourceIdFromPath(filePath) || '').trim();
|
|
1443
|
+
if (!agentSessionId) return null;
|
|
1444
|
+
const provider = normalizeTranscriptProvider(parsed?.agent || parsed?.modelProvider || '', filePath);
|
|
1445
|
+
const largeColdMode = size >= TRANSCRIPT_IMPORT_LARGE_FILE_BYTES ? 'tail' : undefined;
|
|
1446
|
+
const result = ingestJsonlFile(db.getDb(), {
|
|
1447
|
+
filePath,
|
|
1448
|
+
agentSessionId,
|
|
1449
|
+
ctmSessionId: agentSessionId,
|
|
1450
|
+
provider,
|
|
1451
|
+
mode: largeColdMode,
|
|
1452
|
+
initialTailBytes: TRANSCRIPT_IMPORT_MAX_BYTES,
|
|
1453
|
+
maxBytes: largeColdMode ? TRANSCRIPT_IMPORT_MAX_BYTES : Math.min(size || TRANSCRIPT_IMPORT_MAX_BYTES, TRANSCRIPT_IMPORT_MAX_BYTES),
|
|
1454
|
+
});
|
|
1455
|
+
if ((result.inserted || 0) > 0 || (result.bytesRead || 0) > 0) {
|
|
1456
|
+
console.log(
|
|
1457
|
+
`[auto-import] transcript-store ${path.basename(filePath).slice(0, 32)}: ` +
|
|
1458
|
+
`inserted=${result.inserted || 0}, bytes=${result.bytesRead || 0}${largeColdMode ? ', mode=tail' : ''}`
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
return result;
|
|
1462
|
+
} catch (e) {
|
|
1463
|
+
console.error(`[auto-import] transcript-store ingest failed for ${path.basename(filePath).slice(0, 32)}: ${e.message}`);
|
|
1464
|
+
return null;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1342
1468
|
async function importSessionFile(filePath, projectPath, projectEntry) {
|
|
1343
1469
|
const parsed = parseSessionFile(filePath, projectPath, projectEntry);
|
|
1470
|
+
_ingestTranscriptStoreForParsedFile(filePath, parsed);
|
|
1344
1471
|
if (parsed.agent === 'codex') {
|
|
1345
1472
|
return _importCodexSessionFile(parsed, filePath);
|
|
1346
1473
|
}
|
|
@@ -1425,10 +1552,20 @@ async function importSessionFile(filePath, projectPath, projectEntry) {
|
|
|
1425
1552
|
|
|
1426
1553
|
const {
|
|
1427
1554
|
messages: newMessages, assistantCount: newAssistants,
|
|
1555
|
+
searchMessages: newSearchMessages,
|
|
1428
1556
|
firstUserContent: parsedFirstUser, lastUserContent: parsedLastUser,
|
|
1429
1557
|
firstAssistantText: parsedFirstAssistant, renameName: parsedRename,
|
|
1430
1558
|
} = await _parseConversationContent(content);
|
|
1431
1559
|
const allMessages = baseMessages.concat(newMessages);
|
|
1560
|
+
const indexedMessages = prevFileSize > 0 && parsed.fileSize > prevFileSize
|
|
1561
|
+
? _loadIndexedSessionMessages(parsed.sessionId)
|
|
1562
|
+
: [];
|
|
1563
|
+
const baseSearchMessages = prevFileSize > 0 && parsed.fileSize > prevFileSize
|
|
1564
|
+
? (indexedMessages.length ? indexedMessages : baseMessages)
|
|
1565
|
+
: [];
|
|
1566
|
+
const allSearchMessages = (baseSearchMessages.length ? baseSearchMessages : baseMessages)
|
|
1567
|
+
.concat(newSearchMessages && newSearchMessages.length ? newSearchMessages : newMessages);
|
|
1568
|
+
allSearchMessages.sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || ''));
|
|
1432
1569
|
const signals = _conversationSignalsFromMessages(allMessages);
|
|
1433
1570
|
if (allMessages.length === 0 || signals.userCount === 0) return false;
|
|
1434
1571
|
|
|
@@ -1447,6 +1584,7 @@ async function importSessionFile(filePath, projectPath, projectEntry) {
|
|
|
1447
1584
|
session_id: parsed.sessionId,
|
|
1448
1585
|
project_path: parsed.cwd || parsed.project,
|
|
1449
1586
|
messages: allMessages,
|
|
1587
|
+
search_messages: allSearchMessages,
|
|
1450
1588
|
user_msg_count: signals.userCount,
|
|
1451
1589
|
assistant_msg_count: signals.assistantCount || baseAssistantCount + newAssistants,
|
|
1452
1590
|
title: parsed.title || (existing && existing.title) || '',
|
|
@@ -1488,7 +1626,13 @@ async function runIncrementalConversationImport() {
|
|
|
1488
1626
|
let total = 0;
|
|
1489
1627
|
let failed = 0;
|
|
1490
1628
|
let hitLimit = false;
|
|
1629
|
+
let hitLimitLabel = '';
|
|
1491
1630
|
const maxImportedPerRun = Math.max(1, Number(process.env.CTM_CONVERSATION_IMPORT_MAX_PER_RUN || 12));
|
|
1631
|
+
const maxProcessedPerRun = Math.max(1, Number(process.env.CTM_CONVERSATION_IMPORT_MAX_PROCESSED_PER_RUN || maxImportedPerRun));
|
|
1632
|
+
const retryAfterMsRaw = Number(process.env.CTM_CONVERSATION_IMPORT_RETRY_AFTER_MS || CONVERSATION_IMPORT_RETRY_AFTER_MS);
|
|
1633
|
+
const retryAfterMs = Number.isFinite(retryAfterMsRaw) && retryAfterMsRaw >= 1000
|
|
1634
|
+
? retryAfterMsRaw
|
|
1635
|
+
: CONVERSATION_IMPORT_RETRY_AFTER_MS;
|
|
1492
1636
|
|
|
1493
1637
|
try {
|
|
1494
1638
|
// One-time backfill: force full rescan to populate model_provider for existing sessions
|
|
@@ -1510,8 +1654,11 @@ async function runIncrementalConversationImport() {
|
|
|
1510
1654
|
if (error) throw error;
|
|
1511
1655
|
scanned++;
|
|
1512
1656
|
if (await importSessionFile(filePath, projectPath, projectEntry)) imported++;
|
|
1513
|
-
|
|
1657
|
+
const importLimited = imported >= maxImportedPerRun;
|
|
1658
|
+
const processedLimited = scanned >= maxProcessedPerRun;
|
|
1659
|
+
if ((importLimited || processedLimited) && scanned < candidates.length) {
|
|
1514
1660
|
hitLimit = true;
|
|
1661
|
+
hitLimitLabel = importLimited ? `${maxImportedPerRun} imports` : `${maxProcessedPerRun} files`;
|
|
1515
1662
|
break;
|
|
1516
1663
|
}
|
|
1517
1664
|
await new Promise((resolve) => setImmediate(resolve));
|
|
@@ -1524,7 +1671,7 @@ async function runIncrementalConversationImport() {
|
|
|
1524
1671
|
}
|
|
1525
1672
|
|
|
1526
1673
|
if (imported > 0 || failed > 0) {
|
|
1527
|
-
const suffix = hitLimit ? `, paused after ${
|
|
1674
|
+
const suffix = hitLimit ? `, paused after ${hitLimitLabel}` : '';
|
|
1528
1675
|
console.log(`[auto-import] Imported ${imported}, failed ${failed} (scanned ${scanned}/${total}${suffix})`);
|
|
1529
1676
|
}
|
|
1530
1677
|
return {
|
|
@@ -1534,7 +1681,7 @@ async function runIncrementalConversationImport() {
|
|
|
1534
1681
|
candidates: candidates.length,
|
|
1535
1682
|
failed,
|
|
1536
1683
|
remaining: hitLimit,
|
|
1537
|
-
retry_after_ms: hitLimit ?
|
|
1684
|
+
retry_after_ms: hitLimit ? retryAfterMs : undefined,
|
|
1538
1685
|
};
|
|
1539
1686
|
} catch (e) {
|
|
1540
1687
|
console.error('[auto-import] Top-level error:', e.message);
|
|
@@ -3,6 +3,7 @@ const path = require('path');
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const db = require('./db');
|
|
5
5
|
const gitUtils = require('./git-utils');
|
|
6
|
+
const documentReview = require('./lib/document-review');
|
|
6
7
|
|
|
7
8
|
const REVIEW_SEVERITIES = new Set(['comment', 'suggestion', 'issue', 'nit', 'question']);
|
|
8
9
|
const REVIEW_STATUSES = new Set(['open', 'resolved']);
|
|
@@ -73,7 +74,7 @@ function sanitizeReviewCommentInput(reviewId, body) {
|
|
|
73
74
|
const lineStart = positiveLineNumber(body.line_start, 'line_start');
|
|
74
75
|
let lineEnd = body.line_end == null || body.line_end === '' ? lineStart : positiveLineNumber(body.line_end, 'line_end');
|
|
75
76
|
if (lineEnd < lineStart) lineEnd = lineStart;
|
|
76
|
-
|
|
77
|
+
const out = {
|
|
77
78
|
review_id: reviewId,
|
|
78
79
|
file_path: filePath,
|
|
79
80
|
line_start: lineStart,
|
|
@@ -83,6 +84,11 @@ function sanitizeReviewCommentInput(reviewId, body) {
|
|
|
83
84
|
severity: normalizeReviewSeverity(body.severity),
|
|
84
85
|
ai_generated: !!body.ai_generated,
|
|
85
86
|
};
|
|
87
|
+
const anchorKind = String(body.anchor_kind || '').trim().toLowerCase();
|
|
88
|
+
if (['line', 'block', 'file', 'page'].includes(anchorKind)) out.anchor_kind = anchorKind;
|
|
89
|
+
const blockId = String(body.block_id || '').trim();
|
|
90
|
+
if (blockId) out.block_id = blockId.slice(0, 512);
|
|
91
|
+
return out;
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
function sanitizeReviewCommentUpdate(body) {
|
|
@@ -129,6 +135,50 @@ function handleReviewApi(req, res, url) {
|
|
|
129
135
|
const p = url.pathname;
|
|
130
136
|
const m = req.method;
|
|
131
137
|
|
|
138
|
+
// GET /api/reviews/document?path=...&line=...
|
|
139
|
+
if (p === '/api/reviews/document' && m === 'GET') {
|
|
140
|
+
try {
|
|
141
|
+
const filePath = url.searchParams.get('path');
|
|
142
|
+
const line = url.searchParams.get('line') || 1;
|
|
143
|
+
const document = documentReview.readDocument(filePath, { line });
|
|
144
|
+
return jsonResponse(res, 200, { document });
|
|
145
|
+
} catch (e) {
|
|
146
|
+
return jsonResponse(res, e.statusCode || 500, { error: e.message });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// POST /api/reviews/document-review - create/reuse a review record for one document
|
|
151
|
+
if (p === '/api/reviews/document-review' && m === 'POST') {
|
|
152
|
+
readBody(req).then(body => {
|
|
153
|
+
const document = documentReview.readDocument(body.path, { line: body.line || 1 });
|
|
154
|
+
const review = db.listReviews({
|
|
155
|
+
project_path: document.projectRoot,
|
|
156
|
+
review_type: 'doc',
|
|
157
|
+
artifact_path: document.path,
|
|
158
|
+
limit: 1,
|
|
159
|
+
})[0];
|
|
160
|
+
const id = review?.id || db.createReview({
|
|
161
|
+
session_id: body.session_id || null,
|
|
162
|
+
project_path: document.projectRoot,
|
|
163
|
+
review_type: 'doc',
|
|
164
|
+
artifact_path: document.path,
|
|
165
|
+
artifact_hash: document.hash,
|
|
166
|
+
base_ref: `doc:${document.relativePath}`,
|
|
167
|
+
});
|
|
168
|
+
db.updateReview(id, {
|
|
169
|
+
file_count: 1,
|
|
170
|
+
artifact_hash: document.hash,
|
|
171
|
+
summary: document.headings[0]?.text || path.basename(document.path),
|
|
172
|
+
});
|
|
173
|
+
return jsonResponse(res, review ? 200 : 201, {
|
|
174
|
+
id,
|
|
175
|
+
review: db.getReview(id),
|
|
176
|
+
document,
|
|
177
|
+
});
|
|
178
|
+
}).catch(e => jsonResponse(res, e.statusCode || 400, { error: e.message }));
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
132
182
|
// GET /api/reviews/diff?project=...&base=...
|
|
133
183
|
if (p === '/api/reviews/diff' && m === 'GET') {
|
|
134
184
|
const project = url.searchParams.get('project');
|
|
@@ -332,7 +382,8 @@ function composeReviewPrompt(review) {
|
|
|
332
382
|
byFile[c.file_path].push(c);
|
|
333
383
|
}
|
|
334
384
|
|
|
335
|
-
const
|
|
385
|
+
const reviewKind = review.review_type === 'doc' ? 'documentation review' : 'code review';
|
|
386
|
+
const lines = [`Please address these ${reviewKind} comments:\n`];
|
|
336
387
|
|
|
337
388
|
for (const [filePath, fileComments] of Object.entries(byFile)) {
|
|
338
389
|
lines.push(`## ${filePath}\n`);
|
|
@@ -340,9 +391,10 @@ function composeReviewPrompt(review) {
|
|
|
340
391
|
const lineRef = c.line_end && c.line_end !== c.line_start
|
|
341
392
|
? `Lines ${c.line_start}-${c.line_end}`
|
|
342
393
|
: `Line ${c.line_start}`;
|
|
394
|
+
const anchor = c.anchor_kind === 'block' && c.block_id ? ` (${c.block_id})` : '';
|
|
343
395
|
const normalizedSeverity = normalizeReviewSeverity(c.severity);
|
|
344
396
|
const severity = normalizedSeverity !== 'comment' ? ` [${normalizedSeverity}]` : '';
|
|
345
|
-
lines.push(`**${lineRef}**${severity}: ${c.body}\n`);
|
|
397
|
+
lines.push(`**${lineRef}${anchor}**${severity}: ${c.body}\n`);
|
|
346
398
|
}
|
|
347
399
|
}
|
|
348
400
|
|
|
@@ -361,4 +413,5 @@ module.exports = {
|
|
|
361
413
|
_normalizeReviewStatus: normalizeReviewStatus,
|
|
362
414
|
_sanitizeReviewCommentInput: sanitizeReviewCommentInput,
|
|
363
415
|
_sanitizeReviewCommentUpdate: sanitizeReviewCommentUpdate,
|
|
416
|
+
_documentReview: documentReview,
|
|
364
417
|
};
|
|
@@ -563,6 +563,7 @@ Be pragmatic. Most development operations in a local dev environment are safe. O
|
|
|
563
563
|
// The headless terminal still receives all data; after the window expires,
|
|
564
564
|
// server.js sends a clean snapshot to the client.
|
|
565
565
|
const APPROVAL_TRANSITION_MS = 300;
|
|
566
|
+
const APPROVAL_TRANSITION_SETTLE_MS = 150;
|
|
566
567
|
|
|
567
568
|
// Phase 3 verification window. After writing the approval keystroke, we expect
|
|
568
569
|
// Claude Code to transition state (cursor moves, alt-screen exits, or
|
|
@@ -608,8 +609,14 @@ function sendApprovalKeystroke(session, context, headlessWorker) {
|
|
|
608
609
|
const sid = session.id ? session.id.slice(0, 8) : '?';
|
|
609
610
|
const decidedBy = context._decidedBy || 'unknown';
|
|
610
611
|
const ruleLabel = context._ruleLabel || context.toolName || 'Unknown';
|
|
611
|
-
// Set transition flag BEFORE writing keystroke — flushOutput checks this
|
|
612
|
-
|
|
612
|
+
// Set transition flag BEFORE writing keystroke — flushOutput checks this.
|
|
613
|
+
// When headless verification is available, keep the suppression window open
|
|
614
|
+
// through the verification lifecycle so the browser does not receive half of
|
|
615
|
+
// the approval-dismiss redraw before server.js repairs it with a snapshot.
|
|
616
|
+
const transitionMs = headlessWorker && session.id
|
|
617
|
+
? Math.max(APPROVAL_TRANSITION_MS, VERIFY_WINDOW_MS + APPROVAL_TRANSITION_SETTLE_MS)
|
|
618
|
+
: APPROVAL_TRANSITION_MS;
|
|
619
|
+
session._approvalTransitionUntil = Date.now() + transitionMs;
|
|
613
620
|
|
|
614
621
|
// Phase 3: tell the worker to start a verification window
|
|
615
622
|
if (headlessWorker && session.id) {
|