cc-viewer 1.6.93 → 1.6.95

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 (67) hide show
  1. package/cli.js +116 -6
  2. package/dist/assets/{App-hH0rDzOq.js → App-D-Apw-as.js} +16 -16
  3. package/dist/assets/AppHeader-B2WxpwwK.css +41 -0
  4. package/dist/assets/{AppHeader.module-_fn3e-3Y.js → AppHeader.module-DsVA04H2.js} +164 -164
  5. package/dist/assets/Mobile-RajPnX3l.js +10 -0
  6. package/dist/assets/{_basePickBy-Dwc6xlH0.js → _basePickBy-BJEP1GYD.js} +1 -1
  7. package/dist/assets/{_baseUniq-CIhWoRX6.js → _baseUniq-BubLHFZN.js} +1 -1
  8. package/dist/assets/{arc-ClMosRWS.js → arc-DQ_H2ge5.js} +1 -1
  9. package/dist/assets/{architectureDiagram-2XIMDMQ5-CiuBPipP.js → architectureDiagram-2XIMDMQ5-DxzXyiQV.js} +1 -1
  10. package/dist/assets/{blockDiagram-WCTKOSBZ-Bifbw0Pj.js → blockDiagram-WCTKOSBZ-Oko9Xh01.js} +1 -1
  11. package/dist/assets/{c4Diagram-IC4MRINW-Dwx54q11.js → c4Diagram-IC4MRINW-BWMun8B6.js} +1 -1
  12. package/dist/assets/channel-JA0OGVbb.js +1 -0
  13. package/dist/assets/{chunk-4BX2VUAB-BQo_Scg4.js → chunk-4BX2VUAB-D4f3BBSj.js} +1 -1
  14. package/dist/assets/{chunk-55IACEB6-DmMYL0yS.js → chunk-55IACEB6-DEELbrpC.js} +1 -1
  15. package/dist/assets/{chunk-FMBD7UC4-D54EDOVv.js → chunk-FMBD7UC4-BMDopkZI.js} +1 -1
  16. package/dist/assets/{chunk-JSJVCQXG-C4wM91-8.js → chunk-JSJVCQXG-Bl2ge6LE.js} +1 -1
  17. package/dist/assets/{chunk-KX2RTZJC-BuTQbTst.js → chunk-KX2RTZJC-D6-v2Nwr.js} +1 -1
  18. package/dist/assets/{chunk-NQ4KR5QH-pwWGt1sO.js → chunk-NQ4KR5QH-CPoQIkEQ.js} +1 -1
  19. package/dist/assets/{chunk-QZHKN3VN-DzdyrpMz.js → chunk-QZHKN3VN-5ObFPC7D.js} +1 -1
  20. package/dist/assets/{chunk-WL4C6EOR-BsqkjkKt.js → chunk-WL4C6EOR-Brx2xle-.js} +1 -1
  21. package/dist/assets/classDiagram-VBA2DB6C-DBQK8T70.js +1 -0
  22. package/dist/assets/classDiagram-v2-RAHNMMFH-DBQK8T70.js +1 -0
  23. package/dist/assets/clone-B_6tGAmz.js +1 -0
  24. package/dist/assets/{cose-bilkent-S5V4N54A-C_Zw3kps.js → cose-bilkent-S5V4N54A-BtTL0awI.js} +1 -1
  25. package/dist/assets/{dagre-KLK3FWXG-BBym-Z-r.js → dagre-KLK3FWXG-BnJdU6vj.js} +1 -1
  26. package/dist/assets/{diagram-E7M64L7V-CJK3xILI.js → diagram-E7M64L7V-DqmlnmsM.js} +1 -1
  27. package/dist/assets/{diagram-IFDJBPK2-CG0k9GmI.js → diagram-IFDJBPK2-CSfBzoTw.js} +1 -1
  28. package/dist/assets/{diagram-P4PSJMXO-BHJpgqgl.js → diagram-P4PSJMXO-W_YBtuzb.js} +1 -1
  29. package/dist/assets/{erDiagram-INFDFZHY-DY2TxE9e.js → erDiagram-INFDFZHY-BrXGXwQp.js} +1 -1
  30. package/dist/assets/{flowDiagram-PKNHOUZH-C6ujoYh5.js → flowDiagram-PKNHOUZH-D1qO_ftR.js} +1 -1
  31. package/dist/assets/{ganttDiagram-A5KZAMGK-DacCPw0V.js → ganttDiagram-A5KZAMGK-Dj8Dz895.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-K3NZZRJ6-1Hd4fFZI.js → gitGraphDiagram-K3NZZRJ6-CbtCyig0.js} +1 -1
  33. package/dist/assets/{graph-D8YLYosB.js → graph-BSyLsRRZ.js} +1 -1
  34. package/dist/assets/{index-DLkyZ-Zb.js → index-JcYe5KP6.js} +2 -2
  35. package/dist/assets/{infoDiagram-LFFYTUFH-ClN29cDZ.js → infoDiagram-LFFYTUFH-Dt7Cxc1A.js} +1 -1
  36. package/dist/assets/{ishikawaDiagram-PHBUUO56-DDgBFnJ4.js → ishikawaDiagram-PHBUUO56-BATp1eBz.js} +1 -1
  37. package/dist/assets/{journeyDiagram-4ABVD52K-Cl9xLEhq.js → journeyDiagram-4ABVD52K-Cuk87Qh1.js} +1 -1
  38. package/dist/assets/{kanban-definition-K7BYSVSG-CBfTb1XX.js → kanban-definition-K7BYSVSG-dNdjELb5.js} +1 -1
  39. package/dist/assets/{layout-BSGsZPU6.js → layout-ikzzHuR8.js} +1 -1
  40. package/dist/assets/{linear-Cm4oUzYV.js → linear-BBrDskBJ.js} +1 -1
  41. package/dist/assets/{mermaid.core-Bfq98W72.js → mermaid.core-TSkGvSH8.js} +4 -4
  42. package/dist/assets/{mindmap-definition-YRQLILUH-S_0pZiXc.js → mindmap-definition-YRQLILUH-CtDrb2x9.js} +1 -1
  43. package/dist/assets/{pieDiagram-SKSYHLDU-CUof-7Ag.js → pieDiagram-SKSYHLDU-bkNmBvtc.js} +1 -1
  44. package/dist/assets/{quadrantDiagram-337W2JSQ-74oAPb7-.js → quadrantDiagram-337W2JSQ-Byl5XeRq.js} +1 -1
  45. package/dist/assets/{requirementDiagram-Z7DCOOCP-Dx_1a8Sm.js → requirementDiagram-Z7DCOOCP-CZTVT8Rt.js} +1 -1
  46. package/dist/assets/{sankeyDiagram-WA2Y5GQK-Ollqup46.js → sankeyDiagram-WA2Y5GQK-xmaiO1U4.js} +1 -1
  47. package/dist/assets/{sequenceDiagram-2WXFIKYE-CUQdI1hH.js → sequenceDiagram-2WXFIKYE-DjXGcDqg.js} +1 -1
  48. package/dist/assets/{stateDiagram-RAJIS63D-DA4bEZTV.js → stateDiagram-RAJIS63D-42-_6Ebg.js} +1 -1
  49. package/dist/assets/stateDiagram-v2-FVOUBMTO-Dk8cVTL7.js +1 -0
  50. package/dist/assets/{timeline-definition-YZTLITO2-hIqAiIh9.js → timeline-definition-YZTLITO2-DMCYajRr.js} +1 -1
  51. package/dist/assets/{treemap-KZPCXAKY-BPMtLzV0.js → treemap-KZPCXAKY-BHYJalU8.js} +1 -1
  52. package/dist/assets/{vennDiagram-LZ73GAT5-8C63L59W.js → vennDiagram-LZ73GAT5-DlJJRJ0l.js} +1 -1
  53. package/dist/assets/{xychartDiagram-JWTSCODW-rb793_nJ.js → xychartDiagram-JWTSCODW-GeZvsPf-.js} +1 -1
  54. package/dist/index.html +2 -2
  55. package/i18n.js +3 -3
  56. package/lib/perm-bridge.js +27 -18
  57. package/lib/sdk-adapter.js +77 -0
  58. package/lib/sdk-manager.js +464 -0
  59. package/package.json +5 -2
  60. package/server.js +58 -4
  61. package/dist/assets/AppHeader-DN09T_4w.css +0 -41
  62. package/dist/assets/Mobile-DfiaHmP0.js +0 -10
  63. package/dist/assets/channel-CA7t6n2N.js +0 -1
  64. package/dist/assets/classDiagram-VBA2DB6C-iCP3aAMs.js +0 -1
  65. package/dist/assets/classDiagram-v2-RAHNMMFH-iCP3aAMs.js +0 -1
  66. package/dist/assets/clone-DykZ1sNt.js +0 -1
  67. package/dist/assets/stateDiagram-v2-FVOUBMTO-DaqDUk5Z.js +0 -1
@@ -0,0 +1,464 @@
1
+ /**
2
+ * sdk-manager.js — Agent SDK session lifecycle manager.
3
+ *
4
+ * Wraps @anthropic-ai/claude-agent-sdk query() to provide:
5
+ * - Structured message processing → JSONL entries for the frontend
6
+ * - canUseTool callback for AskUserQuestion + permission approval
7
+ * - Streaming status tracking
8
+ * - Multi-turn conversation via session resume
9
+ */
10
+
11
+ import { sdkToJSONLEntry, buildStreamingStatus } from './sdk-adapter.js';
12
+
13
+ let _query;
14
+ try {
15
+ const sdk = await import('@anthropic-ai/claude-agent-sdk');
16
+ _query = sdk.query;
17
+ } catch (err) {
18
+ console.warn('[SDK] Agent SDK not available:', err.message);
19
+ }
20
+
21
+ // Interactive tool names — filtered from entries, handled via canUseTool → WS
22
+ const INTERACTIVE_TOOLS = new Set(['AskUserQuestion', 'ExitPlanMode']);
23
+
24
+ // Session state
25
+ let _sessionId = null;
26
+ let _model = null;
27
+ let _cwd = null;
28
+ let _projectName = null;
29
+ let _permissionMode = 'default';
30
+ let _accumulatedMessages = [];
31
+ let _activeQuery = null;
32
+ let _queryBusy = false; // concurrency guard
33
+ let _sdkTools = null; // real tools list from SDKSystemMessage
34
+
35
+ // Stable timestamp per conversation turn — used as dedup key by frontend
36
+ let _turnTimestamp = null;
37
+
38
+ // Streaming accumulation state
39
+ let _streamingContent = [];
40
+ let _currentBlockData = null;
41
+ let _streamingRequestId = null;
42
+ let _streamThrottleTimer = null;
43
+
44
+ // Callbacks registered by server.js
45
+ let _onEntry = null;
46
+ let _onStreamingStatus = null;
47
+ let _broadcastWs = null;
48
+
49
+ // Pending canUseTool promises: id → { resolve }
50
+ const _pendingApprovals = new Map();
51
+
52
+ // Message queue for messages sent while a query is running
53
+ let _messageQueue = [];
54
+
55
+ export function isSdkAvailable() {
56
+ return typeof _query === 'function';
57
+ }
58
+
59
+ /**
60
+ * Initialize SDK session.
61
+ * Does NOT start a query — waits for the first user message via sendUserMessage().
62
+ */
63
+ export function initSdkSession(cwd, projectName, { onEntry, onStreamingStatus, broadcastWs, permissionMode }) {
64
+ _cwd = cwd;
65
+ _projectName = projectName;
66
+ _onEntry = onEntry;
67
+ _onStreamingStatus = onStreamingStatus;
68
+ _broadcastWs = broadcastWs;
69
+ _permissionMode = permissionMode || 'default';
70
+ _resetFullState();
71
+ }
72
+
73
+ /**
74
+ * Send a user message. Starts a new query (or resumes existing session).
75
+ * Queues the message if a query is already running.
76
+ */
77
+ export async function sendUserMessage(text) {
78
+ if (!_query) throw new Error('Agent SDK not available');
79
+
80
+ // If a query is already running, queue this message and return
81
+ if (_queryBusy) {
82
+ _messageQueue.push(text);
83
+ return;
84
+ }
85
+
86
+ _queryBusy = true;
87
+
88
+ try {
89
+ await _executeQuery(text);
90
+
91
+ // Process any queued messages
92
+ while (_messageQueue.length > 0) {
93
+ const next = _messageQueue.shift();
94
+ await _executeQuery(next);
95
+ }
96
+ } finally {
97
+ _queryBusy = false;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Execute a single query for one user message.
103
+ */
104
+ async function _executeQuery(text) {
105
+ // Generate stable timestamp for this turn — all entries share it for dedup
106
+ _turnTimestamp = new Date().toISOString();
107
+ _streamingRequestId = `sdk_${Date.now()}`;
108
+
109
+ // Accumulate user message BEFORE creating entries (this is the request)
110
+ _accumulatedMessages.push({ role: 'user', content: text });
111
+
112
+ const options = {
113
+ cwd: _cwd,
114
+ includePartialMessages: true,
115
+ permissionMode: _permissionMode,
116
+ canUseTool: _permissionMode === 'bypassPermissions' ? undefined : _handleCanUseTool,
117
+ ..._permissionMode === 'bypassPermissions' && { allowDangerouslySkipPermissions: true },
118
+ };
119
+
120
+ if (_sessionId) {
121
+ options.resume = _sessionId;
122
+ }
123
+
124
+ if (_onStreamingStatus) _onStreamingStatus(buildStreamingStatus(true, { model: _model }));
125
+
126
+ try {
127
+ _activeQuery = _query({ prompt: text, options });
128
+
129
+ for await (const msg of _activeQuery) {
130
+ _processMessage(msg);
131
+ }
132
+ } catch (err) {
133
+ if (err.name !== 'AbortError') {
134
+ console.error('[SDK] Query error:', err.message);
135
+ }
136
+ } finally {
137
+ _resetStreamingState();
138
+ _activeQuery = null;
139
+ if (_onStreamingStatus) _onStreamingStatus(buildStreamingStatus(false));
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Process a single SDK message.
145
+ */
146
+ function _processMessage(msg) {
147
+ switch (msg.type) {
148
+ case 'system':
149
+ if (msg.session_id) _sessionId = msg.session_id;
150
+ if (msg.model) _model = msg.model;
151
+ // Capture real tools from init message (tools is string[], convert to {name} format)
152
+ if (msg.subtype === 'init') {
153
+ if (Array.isArray(msg.tools)) _sdkTools = msg.tools.map(name => ({ name }));
154
+ }
155
+ // Handle compact boundary: reset accumulated messages since SDK compacted internally
156
+ if (msg.subtype === 'compact_boundary') {
157
+ _accumulatedMessages = [];
158
+ }
159
+ break;
160
+
161
+ case 'assistant':
162
+ // Main agent message (not sub-agent)
163
+ if (!msg.parent_tool_use_id && msg.message) {
164
+ _resetStreamingState();
165
+
166
+ // Snapshot messages BEFORE adding assistant (body.messages = request history, unfiltered)
167
+ const requestMessages = [..._accumulatedMessages];
168
+
169
+ // Accumulate the assistant response for future turns
170
+ // Merge with previous if both are assistant (same API turn, multiple content blocks)
171
+ const lastMsg = _accumulatedMessages[_accumulatedMessages.length - 1];
172
+ if (lastMsg && lastMsg.role === 'assistant' && Array.isArray(lastMsg.content) && Array.isArray(msg.message.content)) {
173
+ lastMsg.content = [...lastMsg.content, ...msg.message.content];
174
+ } else {
175
+ _accumulatedMessages.push({ role: 'assistant', content: msg.message.content });
176
+ }
177
+
178
+ // Only filter interactive tools from the RESPONSE content (Last Response rendering),
179
+ // NOT from body.messages (history) — filtering history creates orphaned tool_results
180
+ const filteredContent = _filterInteractiveContent(msg.message.content);
181
+ const displayMsg = { ...msg, message: { ...msg.message, content: filteredContent } };
182
+
183
+ // Convert to JSONL entry with stable turn timestamp and real SDK metadata
184
+ const entry = sdkToJSONLEntry(displayMsg, requestMessages, _model, _projectName, {
185
+ timestamp: _turnTimestamp,
186
+ tools: _sdkTools,
187
+ });
188
+ if (_onEntry) _onEntry(entry);
189
+ }
190
+ if (msg.session_id) _sessionId = msg.session_id;
191
+ break;
192
+
193
+ case 'user':
194
+ // Tool results from tool execution — accumulate (skip replays)
195
+ if (msg.message && !msg.isReplay) {
196
+ // Merge with previous if both are user (consecutive tool_results for same turn)
197
+ const lastUserMsg = _accumulatedMessages[_accumulatedMessages.length - 1];
198
+ if (lastUserMsg && lastUserMsg.role === 'user' && Array.isArray(lastUserMsg.content) && Array.isArray(msg.message.content)) {
199
+ lastUserMsg.content = [...lastUserMsg.content, ...msg.message.content];
200
+ } else {
201
+ _accumulatedMessages.push({ role: 'user', content: msg.message.content });
202
+ }
203
+ }
204
+ break;
205
+
206
+ case 'stream_event':
207
+ if (_onStreamingStatus) {
208
+ _onStreamingStatus(buildStreamingStatus(true, { model: _model }));
209
+ }
210
+ if (!msg.parent_tool_use_id) {
211
+ _processStreamEvent(msg.event);
212
+ }
213
+ break;
214
+
215
+ case 'result':
216
+ if (msg.session_id) _sessionId = msg.session_id;
217
+ if (_onStreamingStatus) _onStreamingStatus(buildStreamingStatus(false));
218
+ break;
219
+
220
+ default:
221
+ break;
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Filter interactive tool_use blocks from an array of content blocks.
227
+ */
228
+ function _filterInteractiveContent(content) {
229
+ return Array.isArray(content)
230
+ ? content.filter(b => b.type !== 'tool_use' || !INTERACTIVE_TOOLS.has(b.name))
231
+ : content;
232
+ }
233
+
234
+ /**
235
+ * Process a single streaming event.
236
+ * Accumulates content blocks and pushes throttled in-progress entries.
237
+ */
238
+ function _processStreamEvent(event) {
239
+ if (!event) return;
240
+ const type = event.type;
241
+
242
+ if (type === 'message_start') {
243
+ _streamingContent = [];
244
+ _currentBlockData = null;
245
+ } else if (type === 'content_block_start') {
246
+ const block = event.content_block;
247
+ if (!block) return;
248
+ if (block.type === 'text') {
249
+ _currentBlockData = { type: 'text', text: block.text || '' };
250
+ } else if (block.type === 'thinking') {
251
+ _currentBlockData = { type: 'thinking', thinking: block.thinking || '' };
252
+ } else if (block.type === 'tool_use') {
253
+ if (INTERACTIVE_TOOLS.has(block.name)) {
254
+ _currentBlockData = null;
255
+ } else {
256
+ _currentBlockData = { type: 'tool_use', id: block.id, name: block.name, input: {} };
257
+ }
258
+ } else {
259
+ _currentBlockData = null;
260
+ }
261
+ } else if (type === 'content_block_delta') {
262
+ const delta = event.delta;
263
+ if (!delta || !_currentBlockData) return;
264
+ if (delta.type === 'text_delta' && _currentBlockData.type === 'text') {
265
+ _currentBlockData.text += delta.text || '';
266
+ _pushStreamingEntry();
267
+ } else if (delta.type === 'thinking_delta' && _currentBlockData.type === 'thinking') {
268
+ _currentBlockData.thinking += delta.thinking || '';
269
+ } else if (delta.type === 'input_json_delta' && _currentBlockData.type === 'tool_use') {
270
+ if (!_currentBlockData._rawInput) _currentBlockData._rawInput = '';
271
+ _currentBlockData._rawInput += delta.partial_json || '';
272
+ }
273
+ } else if (type === 'content_block_stop') {
274
+ if (_currentBlockData) {
275
+ if (_currentBlockData.type === 'tool_use' && _currentBlockData._rawInput) {
276
+ try { _currentBlockData.input = JSON.parse(_currentBlockData._rawInput); } catch {}
277
+ delete _currentBlockData._rawInput;
278
+ }
279
+ _streamingContent.push(_currentBlockData);
280
+ _currentBlockData = null;
281
+ _pushStreamingEntry();
282
+ }
283
+ } else if (type === 'message_stop') {
284
+ _resetStreamingState();
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Push a throttled in-progress entry with accumulated streaming content.
290
+ */
291
+ function _pushStreamingEntry() {
292
+ if (_streamThrottleTimer) return;
293
+ _streamThrottleTimer = setTimeout(() => {
294
+ _streamThrottleTimer = null;
295
+ _flushStreamingEntry();
296
+ }, 100);
297
+ }
298
+
299
+ function _flushStreamingEntry() {
300
+ if (!_onEntry) return;
301
+ const content = [..._streamingContent];
302
+ if (_currentBlockData) {
303
+ const clone = { ..._currentBlockData };
304
+ if (clone._rawInput) delete clone._rawInput;
305
+ content.push(clone);
306
+ }
307
+ if (content.length === 0) return;
308
+
309
+ const syntheticMsg = {
310
+ message: {
311
+ id: _streamingRequestId,
312
+ type: 'message',
313
+ role: 'assistant',
314
+ model: _model,
315
+ content,
316
+ stop_reason: null,
317
+ usage: { input_tokens: 0, output_tokens: 0 },
318
+ },
319
+ };
320
+ // Use the SAME stable turn timestamp so in-progress entries dedup with the final entry
321
+ const entry = sdkToJSONLEntry(syntheticMsg, [..._accumulatedMessages], _model, _projectName, {
322
+ inProgress: true,
323
+ tools: _sdkTools,
324
+ requestId: _streamingRequestId,
325
+ timestamp: _turnTimestamp,
326
+ });
327
+ _onEntry(entry);
328
+ }
329
+
330
+ function _resetStreamingState() {
331
+ _streamingContent = [];
332
+ _currentBlockData = null;
333
+ if (_streamThrottleTimer) {
334
+ clearTimeout(_streamThrottleTimer);
335
+ _streamThrottleTimer = null;
336
+ }
337
+ }
338
+
339
+ /**
340
+ * canUseTool callback — handles AskUserQuestion + permission approval.
341
+ */
342
+ async function _handleCanUseTool(toolName, input, options) {
343
+ const id = options?.toolUseID || `sdk_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
344
+
345
+ if (toolName === 'ExitPlanMode') {
346
+ if (_broadcastWs) {
347
+ _broadcastWs({ type: 'sdk-plan-pending', id, input });
348
+ }
349
+ const result = await _waitForApproval(id, 5 * 60 * 1000);
350
+ if (result === null) {
351
+ return { behavior: 'deny', message: 'Timeout waiting for plan approval' };
352
+ }
353
+ if (typeof result === 'object' && result.approve === false) {
354
+ return { behavior: 'deny', message: result.feedback || 'User rejected the plan' };
355
+ }
356
+ return { behavior: 'allow', updatedInput: input };
357
+ }
358
+
359
+ if (toolName === 'AskUserQuestion') {
360
+ if (_broadcastWs) {
361
+ _broadcastWs({ type: 'sdk-ask-pending', id, questions: input.questions });
362
+ }
363
+ const answers = await _waitForApproval(id, 5 * 60 * 1000);
364
+ if (answers === null) {
365
+ return { behavior: 'deny', message: 'Timeout waiting for user answer' };
366
+ }
367
+ return { behavior: 'allow', updatedInput: { questions: input.questions, answers } };
368
+ }
369
+
370
+ // Only mutating tools need explicit approval; read-only tools auto-allow
371
+ const APPROVAL_TOOLS = new Set(['Bash', 'Edit', 'Write', 'NotebookEdit']);
372
+ if (!APPROVAL_TOOLS.has(toolName)) {
373
+ return { behavior: 'allow', updatedInput: input };
374
+ }
375
+
376
+ // Permission approval for mutating tools
377
+ const suggestions = options?.suggestions;
378
+ if (_broadcastWs) {
379
+ _broadcastWs({ type: 'perm-hook-pending', id, toolName, input });
380
+ }
381
+
382
+ const result = await _waitForApproval(id, 5 * 60 * 1000);
383
+ if (result === null) {
384
+ return { behavior: 'deny', message: 'Timeout waiting for user approval' };
385
+ }
386
+ const decision = typeof result === 'object' ? result.decision : result;
387
+ const allowSession = typeof result === 'object' && result.allowSession;
388
+ if (decision === 'deny') {
389
+ return { behavior: 'deny', message: 'User denied via cc-viewer' };
390
+ }
391
+ const response = { behavior: 'allow', updatedInput: input };
392
+ if (allowSession && Array.isArray(suggestions) && suggestions.length > 0) {
393
+ response.updatedPermissions = suggestions;
394
+ }
395
+ return response;
396
+ }
397
+
398
+ function _waitForApproval(id, timeoutMs) {
399
+ return new Promise((resolve) => {
400
+ const timer = setTimeout(() => {
401
+ _pendingApprovals.delete(id);
402
+ resolve(null);
403
+ }, timeoutMs);
404
+ _pendingApprovals.set(id, {
405
+ resolve: (value) => {
406
+ clearTimeout(timer);
407
+ _pendingApprovals.delete(id);
408
+ resolve(value);
409
+ },
410
+ });
411
+ });
412
+ }
413
+
414
+ /**
415
+ * Resolve a pending canUseTool approval.
416
+ * Called by server.js when a WS message arrives.
417
+ */
418
+ export function resolveApproval(id, value) {
419
+ const pending = _pendingApprovals.get(id);
420
+ if (pending) {
421
+ pending.resolve(value);
422
+ return true;
423
+ }
424
+ return false;
425
+ }
426
+
427
+ /**
428
+ * Stop the active SDK session.
429
+ */
430
+ export function stopSession() {
431
+ // Use close() instead of interrupt() — works in all modes
432
+ if (_activeQuery && typeof _activeQuery.close === 'function') {
433
+ _activeQuery.close();
434
+ }
435
+ _resetFullState();
436
+ }
437
+
438
+ /**
439
+ * Reset all session state.
440
+ */
441
+ function _resetFullState() {
442
+ _activeQuery = null;
443
+ _sessionId = null;
444
+ _model = null;
445
+ _sdkTools = null;
446
+ _queryBusy = false;
447
+ _accumulatedMessages = [];
448
+ _turnTimestamp = null;
449
+ _messageQueue = [];
450
+ _resetStreamingState();
451
+ _streamingRequestId = null;
452
+ // Reject all pending approvals
453
+ for (const [, pending] of _pendingApprovals) {
454
+ pending.resolve(null);
455
+ }
456
+ _pendingApprovals.clear();
457
+ }
458
+
459
+ /**
460
+ * Get current session ID (for resume).
461
+ */
462
+ export function getSessionId() {
463
+ return _sessionId;
464
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-viewer",
3
- "version": "1.6.93",
3
+ "version": "1.6.95",
4
4
  "description": "Claude Code Logger visualization management tool",
5
5
  "license": "MIT",
6
6
  "main": "server.js",
@@ -80,9 +80,9 @@
80
80
  "antd": "^5.29.2",
81
81
  "diff": "^8.0.3",
82
82
  "highlight.js": "^11.11.1",
83
- "mermaid": "^11.13.0",
84
83
  "html2canvas": "^1.4.1",
85
84
  "marked": "^17.0.2",
85
+ "mermaid": "^11.13.0",
86
86
  "qrcode.react": "^4.2.0",
87
87
  "react": "^18.3.1",
88
88
  "react-dom": "^18.3.1",
@@ -95,5 +95,8 @@
95
95
  "node-pty": "^1.1.0",
96
96
  "undici": "^7.22.0",
97
97
  "ws": "^8.19.0"
98
+ },
99
+ "optionalDependencies": {
100
+ "@anthropic-ai/claude-agent-sdk": "^0.2.91"
98
101
  }
99
102
  }
package/server.js CHANGED
@@ -43,7 +43,7 @@ import { uploadPlugins, installPluginFromUrl } from './lib/plugin-manager.js';
43
43
  import { getUserProfile } from './lib/user-profile.js';
44
44
  import { getGitDiffs } from './lib/git-diff.js';
45
45
  import { CONTEXT_WINDOW_FILE, readModelContextSize, buildContextWindowEvent, getContextSizeForModel } from './lib/context-watcher.js';
46
- import { watchLogFile, startWatching, getWatchedFiles, sendEventToClients } from './lib/log-watcher.js';
46
+ import { watchLogFile, startWatching, getWatchedFiles, sendEventToClients, sendToClients } from './lib/log-watcher.js';
47
47
  import { isMainAgentEntry, extractCachedContent } from './lib/kv-cache-analyzer.js';
48
48
  import { listLocalLogs, deleteLogFiles, mergeLogFiles } from './lib/log-management.js';
49
49
  import { countLogEntries, streamRawEntriesAsync, readPagedEntries } from './lib/log-stream.js';
@@ -60,6 +60,7 @@ try {
60
60
  }
61
61
  } catch { }
62
62
  const isCliMode = process.env.CCV_CLI_MODE === '1';
63
+ const isSdkMode = process.env.CCV_SDK_MODE === '1';
63
64
  const isWorkspaceMode = process.env.CCV_WORKSPACE_MODE === '1';
64
65
  const _defaultProxyProfiles = { active: 'max', profiles: [{ id: 'max', name: 'Default' }] };
65
66
  const _maskApiKey = (k) => k && typeof k === 'string' && k.length > 4 ? '****' + k.slice(-4) : k ? '****' : '';
@@ -1874,7 +1875,7 @@ async function handleRequest(req, res) {
1874
1875
  // CLI 模式检测
1875
1876
  if (url === '/api/cli-mode' && method === 'GET') {
1876
1877
  res.writeHead(200, { 'Content-Type': 'application/json' });
1877
- res.end(JSON.stringify({ cliMode: isCliMode, workspaceMode: isWorkspaceMode && !_workspaceLaunched }));
1878
+ res.end(JSON.stringify({ cliMode: isCliMode, sdkMode: isSdkMode, workspaceMode: isWorkspaceMode && !_workspaceLaunched }));
1878
1879
  return;
1879
1880
  }
1880
1881
 
@@ -2641,8 +2642,10 @@ async function setupTerminalWebSocket(httpServer) {
2641
2642
  } catch {}
2642
2643
  }
2643
2644
  } else if (msg.type === 'perm-hook-answer') {
2644
- // Client answered permission approval via hook bridge
2645
- if (pendingPermHook && msg.id && msg.id === pendingPermHook.id) {
2645
+ // Permission approval SDK mode (canUseTool) or PTY mode (hook bridge)
2646
+ if (isSdkMode && _sdkResolveApproval) {
2647
+ _sdkResolveApproval(msg.id, msg.allowSession ? { decision: msg.decision || 'allow', allowSession: true } : (msg.decision || 'deny'));
2648
+ } else if (pendingPermHook && msg.id && msg.id === pendingPermHook.id) {
2646
2649
  const { res: hookRes, timer } = pendingPermHook;
2647
2650
  clearTimeout(timer);
2648
2651
  pendingPermHook = null;
@@ -2653,6 +2656,23 @@ async function setupTerminalWebSocket(httpServer) {
2653
2656
  }
2654
2657
  } catch {}
2655
2658
  }
2659
+ } else if (msg.type === 'sdk-ask-answer') {
2660
+ // AskUserQuestion answer in SDK mode — resolve canUseTool Promise
2661
+ if (_sdkResolveApproval) {
2662
+ _sdkResolveApproval(msg.id, msg.answers);
2663
+ }
2664
+ } else if (msg.type === 'sdk-plan-answer') {
2665
+ // Plan approval in SDK mode
2666
+ if (_sdkResolveApproval) {
2667
+ _sdkResolveApproval(msg.id, { approve: msg.approve !== false, feedback: msg.feedback || '' });
2668
+ }
2669
+ } else if (msg.type === 'sdk-user-message') {
2670
+ // User message in SDK mode — relay to sdk-manager
2671
+ if (_sdkSendUserMessage && msg.text) {
2672
+ _sdkSendUserMessage(msg.text).catch(err => {
2673
+ console.error('[SDK] sendUserMessage error:', err.message);
2674
+ });
2675
+ }
2656
2676
  } else if (msg.type === 'resize') {
2657
2677
  // 存储该客户端的尺寸
2658
2678
  clientSizes.set(ws, { cols: msg.cols, rows: msg.rows });
@@ -2717,6 +2737,8 @@ let _lastStreamingActive = false;
2717
2737
  function startStreamingStatusTimer() {
2718
2738
  if (_streamingStatusTimer) return;
2719
2739
  _streamingStatusTimer = setInterval(() => {
2740
+ // SDK mode uses its own streaming state (pushed directly via setSdkStreamingState)
2741
+ if (isSdkMode) return;
2720
2742
  const changed = streamingState.active !== _lastStreamingActive;
2721
2743
  if (changed || streamingState.active) {
2722
2744
  const data = streamingState.active
@@ -2777,6 +2799,38 @@ async function _doStop() {
2777
2799
  try { unwatchFile(PROFILE_PATH); } catch {} // 清理 interceptor 的 StatWatcher
2778
2800
  }
2779
2801
 
2802
+ // ─── SDK Mode Exports ──────────────────────────────────────────
2803
+
2804
+ /** Push a JSONL entry to all SSE clients (for SDK mode). */
2805
+ export function pushSdkEntry(entry) {
2806
+ if (sendToClients) sendToClients(clients, entry);
2807
+ }
2808
+
2809
+ /** Update streaming status (for SDK mode). */
2810
+ export function setSdkStreamingState(data) {
2811
+ if (clients.length > 0 && sendEventToClients) {
2812
+ sendEventToClients(clients, 'streaming_status', data);
2813
+ }
2814
+ }
2815
+
2816
+ /** Broadcast a message to all terminal WS clients (for SDK canUseTool). */
2817
+ export function broadcastWsMessage(msg) {
2818
+ if (terminalWss) {
2819
+ const str = typeof msg === 'string' ? msg : JSON.stringify(msg);
2820
+ terminalWss.clients.forEach((c) => {
2821
+ if (c.readyState === 1) try { c.send(str); } catch {}
2822
+ });
2823
+ }
2824
+ }
2825
+
2826
+ /** Reference to sdk-manager's resolveApproval (set by cli.js after import). */
2827
+ let _sdkResolveApproval = null;
2828
+ export function setSdkResolveApproval(fn) { _sdkResolveApproval = fn; }
2829
+
2830
+ /** Reference to sdk-manager's sendUserMessage (set by cli.js after import). */
2831
+ let _sdkSendUserMessage = null;
2832
+ export function setSdkSendUserMessage(fn) { _sdkSendUserMessage = fn; }
2833
+
2780
2834
  // Auto-start the viewer after log file init completes
2781
2835
  // 工作区模式下由 cli.js 直接 import server.js 触发启动,跳过 _initPromise 自动启动
2782
2836
  if (!isWorkspaceMode) {