deepccc 0.2.7 → 0.2.8

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 CHANGED
@@ -155,7 +155,7 @@ deepccc
155
155
 
156
156
  Web UI 支持新建、恢复、重命名和删除多会话,多个会话可以同时运行,即使它们指向同一个工作目录。每个会话可独立选择 model、subModel 和 effort,并持续复用 CLI 已保存的历史。注意:当前版本不自动创建 Git worktree;同目录的多个运行中 Agent 直接修改同一组文件,页面会提示覆盖与冲突风险。
157
157
 
158
- 模型文本、reasoning 心跳和工具事件通过 SSE 实时更新。每次工具调用与对应结果合并成一张卡片,折叠态显示 emoji、工具名、状态和关键参数;展开后调用参数默认保留前 8/后 4 行,工具结果保留前 12/后 6 行,省略内容可继续展开。工具卡及省略行的展开状态保存在当前浏览器标签页的 `sessionStorage`,持续生成、切换会话和刷新页面均不会自动收起。消息正文支持标题、表格、列表、引用、链接和代码块等常用 Markdown。
158
+ 模型文本、reasoning 心跳和工具事件通过 SSE 实时更新。每轮消息按真实发生顺序持久化和回放,因此刷新后仍会保持“阶段说明 → 工具调用 → 后续结论”的交错时间线;旧版会话没有顺序数据时,会兼容显示为“工具调用 → 最终回答”。每次工具调用与对应结果合并成一张卡片,折叠态显示 emoji、工具名、状态和关键参数;展开后调用参数默认保留前 8/后 4 行,工具结果保留前 12/后 6 行,省略内容可继续展开。工具卡及省略行的展开状态保存在当前浏览器标签页的 `sessionStorage`,持续生成、切换会话和刷新页面均不会自动收起。消息正文支持标题、表格、列表、引用、链接和代码块等常用 Markdown。
159
159
 
160
160
  图片始终按本地附件处理,不转换为 Provider 原生多模态消息。Web 支持文件选择、剪贴板粘贴和拖拽,每条消息最多 10 张 PNG/JPEG/WebP、单张最大 20 MB;附件复制到 `~/.deepccc/attachments/<session-id>/`,Agent 收到本地绝对路径后使用可用工具自行处理。Agent 可调用 `present_file` 把当前工作目录或会话附件目录中的图片直接展示在会话中;删除会话时对应附件一并清理。
161
161
 
package/dist/context.d.ts CHANGED
@@ -13,11 +13,28 @@ export interface BuiltinContextToolCall {
13
13
  output?: string;
14
14
  is_error?: boolean;
15
15
  }
16
+ export type BuiltinContextTimelineEntry = {
17
+ type: "text";
18
+ text: string;
19
+ } | {
20
+ type: "tool_use";
21
+ id: string;
22
+ name: string;
23
+ input?: string;
24
+ } | {
25
+ type: "tool_result";
26
+ tool_use_id: string;
27
+ name?: string;
28
+ output?: string;
29
+ is_error?: boolean;
30
+ };
16
31
  export interface BuiltinContextMessage {
17
32
  role: BuiltinContextRole;
18
33
  content: string;
19
34
  /** 可选结构化工具调用记录(assistant 消息专用) */
20
35
  toolCalls?: BuiltinContextToolCall[];
36
+ /** 文本与工具事件的真实发生顺序,仅供 UI 回放;模型上下文仍使用 content。 */
37
+ timeline?: BuiltinContextTimelineEntry[];
21
38
  }
22
39
  export interface BuiltinContextState {
23
40
  version: 1;
@@ -90,6 +107,8 @@ export declare function buildPersistedAssistantMessage(params: {
90
107
  transcriptLines: readonly string[];
91
108
  /** 结构化工具调用(按调用顺序);有则写入消息的 toolCalls 字段 */
92
109
  toolCalls?: readonly BuiltinContextToolCall[];
110
+ /** 文本和工具事件的有序时间线;不参与模型输入。 */
111
+ timeline?: readonly BuiltinContextTimelineEntry[];
93
112
  maxAssistantChars?: number;
94
113
  maxTranscriptChars?: number;
95
114
  }): BuiltinContextMessage;
package/dist/context.js CHANGED
@@ -49,8 +49,42 @@ function normalizeMessage(value) {
49
49
  const toolCalls = normalizeToolCalls(raw.toolCalls);
50
50
  if (toolCalls.length > 0)
51
51
  message.toolCalls = toolCalls;
52
+ const timeline = normalizeTimeline(raw.timeline);
53
+ if (timeline.length > 0)
54
+ message.timeline = timeline;
52
55
  return message;
53
56
  }
57
+ function normalizeTimeline(value) {
58
+ if (!Array.isArray(value))
59
+ return [];
60
+ const entries = [];
61
+ for (const item of value.slice(0, 1000)) {
62
+ if (!item || typeof item !== "object" || Array.isArray(item))
63
+ continue;
64
+ const raw = item;
65
+ if (raw.type === "text" && typeof raw.text === "string") {
66
+ entries.push({ type: "text", text: raw.text });
67
+ }
68
+ else if (raw.type === "tool_use" && typeof raw.id === "string" && typeof raw.name === "string") {
69
+ entries.push({
70
+ type: "tool_use",
71
+ id: raw.id,
72
+ name: raw.name,
73
+ ...(typeof raw.input === "string" ? { input: raw.input } : {}),
74
+ });
75
+ }
76
+ else if (raw.type === "tool_result" && typeof raw.tool_use_id === "string") {
77
+ entries.push({
78
+ type: "tool_result",
79
+ tool_use_id: raw.tool_use_id,
80
+ ...(typeof raw.name === "string" ? { name: raw.name } : {}),
81
+ ...(typeof raw.output === "string" ? { output: raw.output } : {}),
82
+ ...(typeof raw.is_error === "boolean" ? { is_error: raw.is_error } : {}),
83
+ });
84
+ }
85
+ }
86
+ return entries;
87
+ }
54
88
  function normalizeToolCalls(value) {
55
89
  if (!Array.isArray(value))
56
90
  return [];
@@ -217,8 +251,54 @@ export function buildPersistedAssistantMessage(params) {
217
251
  const toolCalls = normalizeToolCalls(params.toolCalls);
218
252
  if (toolCalls.length > 0)
219
253
  message.toolCalls = toolCalls;
254
+ const timeline = truncatePersistedTimeline(normalizeTimeline(params.timeline));
255
+ if (timeline.length > 0)
256
+ message.timeline = timeline;
220
257
  return message;
221
258
  }
259
+ function truncatePersistedTimeline(entries) {
260
+ const maxTextChars = DEFAULT_PERSISTED_ASSISTANT_TEXT_CHARS;
261
+ const maxToolChars = DEFAULT_PERSISTED_TOOL_TRANSCRIPT_CHARS;
262
+ const totalText = entries.reduce((sum, entry) => sum + (entry.type === "text" ? entry.text.length : 0), 0);
263
+ const textPayloads = entries.filter((entry) => entry.type === "text" && entry.text.length > 0).length;
264
+ const totalTool = entries.reduce((sum, entry) => {
265
+ if (entry.type === "tool_use")
266
+ return sum + (entry.input?.length ?? 0);
267
+ if (entry.type === "tool_result")
268
+ return sum + (entry.output?.length ?? 0);
269
+ return sum;
270
+ }, 0);
271
+ const toolPayloads = entries.filter((entry) => (entry.type === "tool_use" && (entry.input?.length ?? 0) > 0)
272
+ || (entry.type === "tool_result" && (entry.output?.length ?? 0) > 0)).length;
273
+ const payloadBudget = (length, total, count, maximum) => {
274
+ if (total <= maximum)
275
+ return length;
276
+ const sharedBudget = Math.max(0, maximum - count);
277
+ return length > 0 ? 1 + Math.floor(sharedBudget * length / Math.max(1, total)) : 0;
278
+ };
279
+ const truncatePayload = (value, budget, marker) => {
280
+ if (value.length <= budget)
281
+ return value;
282
+ if (budget <= marker.length + 2)
283
+ return value.slice(0, budget);
284
+ return truncateMiddle(value, budget, marker);
285
+ };
286
+ return entries.map((entry) => {
287
+ if (entry.type === "text") {
288
+ const budget = payloadBudget(entry.text.length, totalText, textPayloads, maxTextChars);
289
+ return { ...entry, text: truncatePayload(entry.text, budget, ASSISTANT_TRUNCATED_MARKER) };
290
+ }
291
+ if (entry.type === "tool_use" && entry.input !== undefined) {
292
+ const budget = payloadBudget(entry.input.length, totalTool, toolPayloads, maxToolChars);
293
+ return { ...entry, input: truncatePayload(entry.input, budget, TOOL_TRANSCRIPT_TRUNCATED_MARKER) };
294
+ }
295
+ if (entry.type === "tool_result" && entry.output !== undefined) {
296
+ const budget = payloadBudget(entry.output.length, totalTool, toolPayloads, maxToolChars);
297
+ return { ...entry, output: truncatePayload(entry.output, budget, TOOL_TRANSCRIPT_TRUNCATED_MARKER) };
298
+ }
299
+ return entry;
300
+ });
301
+ }
222
302
  function serializeMessagesForCompaction(messages) {
223
303
  const sections = [];
224
304
  let remaining = MAX_COMPACTION_SOURCE_CHARS;
package/dist/index.js CHANGED
@@ -393,6 +393,8 @@ export class ChatSession {
393
393
  // assistant 消息 toolCalls 字段;[Tool transcript] 文本视图仍按原格式生成。
394
394
  const toolCallsById = new Map();
395
395
  const toolCallOrder = [];
396
+ const timeline = [];
397
+ let toolContext = [];
396
398
  try {
397
399
  if (this.context.planCompaction()) {
398
400
  yield { type: "status", phase: "compacting" };
@@ -460,9 +462,10 @@ export class ChatSession {
460
462
  for (let attempt = 0; attempt < 2; attempt += 1) {
461
463
  fullText = "";
462
464
  safeAccumulated = "";
463
- const toolContext = [];
465
+ toolContext = [];
464
466
  toolCallsById.clear();
465
467
  toolCallOrder.length = 0;
468
+ timeline.length = 0;
466
469
  let lastReasoningProgressAt;
467
470
  const attemptMessages = attempt === 0
468
471
  ? modelMessages
@@ -493,6 +496,11 @@ export class ChatSession {
493
496
  }
494
497
  else if (part.type === "text-delta") {
495
498
  fullText += part.text;
499
+ const previous = timeline[timeline.length - 1];
500
+ if (previous?.type === "text")
501
+ previous.text += part.text;
502
+ else
503
+ timeline.push({ type: "text", text: part.text });
496
504
  // 隐私替换只在展示层:safeAccumulated 供事件消费者(终端/JSONL)使用,
497
505
  // fullText 原文用于持久化上下文,避免替换结果回流污染上下文。
498
506
  const safeText = applyPrivacy(part.text);
@@ -500,9 +508,11 @@ export class ChatSession {
500
508
  yield { type: "text", text: safeText, accumulated: safeAccumulated };
501
509
  }
502
510
  else if (part.type === "tool-call") {
503
- toolContext.push(`tool_call ${part.toolName}: ${safeJson(part.input)}`);
504
- toolCallsById.set(part.toolCallId, { id: part.toolCallId, name: part.toolName, input: safeJson(part.input) });
511
+ const input = safeJson(part.input);
512
+ toolContext.push(`tool_call ${part.toolName}: ${input}`);
513
+ toolCallsById.set(part.toolCallId, { id: part.toolCallId, name: part.toolName, input });
505
514
  toolCallOrder.push(part.toolCallId);
515
+ timeline.push({ type: "tool_use", id: part.toolCallId, name: part.toolName, input });
506
516
  yield {
507
517
  type: "tool_use",
508
518
  id: part.toolCallId,
@@ -511,10 +521,12 @@ export class ChatSession {
511
521
  };
512
522
  }
513
523
  else if (part.type === "tool-result") {
514
- toolContext.push(`tool_result ${part.toolName}: ${truncateToolContext(safeJson(part.output))}`);
524
+ const output = truncateToolContext(safeJson(part.output));
525
+ toolContext.push(`tool_result ${part.toolName}: ${output}`);
515
526
  const call = toolCallsById.get(part.toolCallId);
516
527
  if (call)
517
- call.output = truncateToolContext(safeJson(part.output));
528
+ call.output = output;
529
+ timeline.push({ type: "tool_result", tool_use_id: part.toolCallId, name: part.toolName, output });
518
530
  yield {
519
531
  type: "tool_result",
520
532
  tool_use_id: part.toolCallId,
@@ -531,6 +543,7 @@ export class ChatSession {
531
543
  call.output = message;
532
544
  call.is_error = true;
533
545
  }
546
+ timeline.push({ type: "tool_result", tool_use_id: part.toolCallId, name: part.toolName, output: message, is_error: true });
534
547
  yield {
535
548
  type: "tool_result",
536
549
  tool_use_id: part.toolCallId,
@@ -570,6 +583,7 @@ export class ChatSession {
570
583
  fullText,
571
584
  transcriptLines: toolContext,
572
585
  toolCalls: collectedToolCalls,
586
+ timeline,
573
587
  }));
574
588
  yield { type: "done", text: safeAccumulated };
575
589
  return;
@@ -581,9 +595,22 @@ export class ChatSession {
581
595
  if (malformedProtocolOutput)
582
596
  yield { type: "text_reset" };
583
597
  if (err.name === "AbortError" || signal?.aborted) {
584
- // 被中断时,不保存不完整的助手消息
585
- if (fullText && !malformedProtocolOutput) {
586
- this.context.appendMessage({ role: "assistant", content: `${fullText}\n[interrupted]` });
598
+ if ((fullText || toolCallOrder.length > 0) && !malformedProtocolOutput) {
599
+ const collectedToolCalls = toolCallOrder
600
+ .map((id) => toolCallsById.get(id))
601
+ .filter((call) => call !== undefined);
602
+ const interruptedTimeline = timeline.map((entry) => ({ ...entry }));
603
+ const previous = interruptedTimeline[interruptedTimeline.length - 1];
604
+ if (previous?.type === "text")
605
+ previous.text += "\n[interrupted]";
606
+ else
607
+ interruptedTimeline.push({ type: "text", text: "[interrupted]" });
608
+ this.context.appendMessage(buildPersistedAssistantMessage({
609
+ fullText: `${fullText}\n[interrupted]`,
610
+ transcriptLines: toolContext,
611
+ toolCalls: collectedToolCalls,
612
+ timeline: interruptedTimeline,
613
+ }));
587
614
  }
588
615
  yield { type: "done", text: safeAccumulated };
589
616
  return;
package/dist/web-page.js CHANGED
@@ -325,7 +325,10 @@ export const DEEPCCC_WEB_PAGE = `<!DOCTYPE html>
325
325
  function setToolState(key,value){try{sessionStorage.setItem(toolStorageKey(key),value?'1':'0');}catch(error){}}
326
326
  function clearToolSessionState(sessionId){try{var prefix='deepccc:tool-ui:v1:'+sessionId+':';for(var index=sessionStorage.length-1;index>=0;index--){var key=sessionStorage.key(index);if(key&&key.indexOf(prefix)===0)sessionStorage.removeItem(key);}}catch(error){}}
327
327
  function toolSectionHtml(call,kind,value,headLines,tailLines){if(value===undefined)return'';var result=truncateToolPayload(value,headLines,tailLines);var stateKey=call.uiKey+':'+kind+'-full';var expanded=toolState(stateKey);var text=expanded?result.full:result.preview;var label=kind==='input'?'调用参数':'工具结果';var button=result.truncated?'<button type="button" class="tool-overflow-toggle" data-tool-overflow="'+escapeHtml(stateKey)+'">'+escapeHtml(expanded?'收起省略内容':(result.omittedLines>0?'展开省略的 '+result.omittedLines+' 行':'展开完整内容'))+'</button>':'';return'<section class="tool-section"><div class="tool-section-title">'+label+'</div><pre>'+escapeHtml(text)+'</pre>'+button+'</section>';}
328
- function messageHtml(role,content,toolCalls,messageIndex){var decoded=role==='user'?decodeAttachmentMessage(content):{text:withoutToolTranscript(content),attachments:[]};var tools='';(toolCalls||[]).forEach(function(call,index){tools+=toolCardHtml({uiKey:call.id?'tool:'+call.id:'message:'+messageIndex+':tool:'+index,name:call.name,input:call.input,output:call.output,isError:!!call.is_error,pending:false});});return '<article class="message '+role+'"><div class="avatar">'+(role==='user'?'你':'D')+'</div><div class="bubble"><div class="role">'+(role==='user'?'YOU':'DEEPCCC')+'</div><div class="markdown">'+markdown(decoded.text)+'</div>'+attachmentGallery(decoded.attachments,state.detail.sessionId)+tools+'</div></article>';}
328
+ function timelineHtml(timeline,messageIndex){var results={};(timeline||[]).forEach(function(item){if(item&&item.type==='tool_result'&&item.tool_use_id)results[item.tool_use_id]=item;});var renderedCalls={};var html='';(timeline||[]).forEach(function(item,index){if(!item)return;if(item.type==='text'){if(item.text)html+='<div class="markdown">'+markdown(item.text)+'</div>';return;}if(item.type==='tool_use'){var id=item.id||('timeline-'+messageIndex+'-'+index);var result=results[id];renderedCalls[id]=true;html+=toolCardHtml({uiKey:'tool:'+id,name:item.name||'tool',input:item.input,output:result?result.output:undefined,isError:!!(result&&result.is_error),pending:!result});return;}if(item.type==='tool_result'&&!renderedCalls[item.tool_use_id]){var fallbackId=item.tool_use_id||('result-'+messageIndex+'-'+index);renderedCalls[fallbackId]=true;html+=toolCardHtml({uiKey:'tool:'+fallbackId,name:item.name||'tool',output:item.output,isError:!!item.is_error,pending:false});}});return html;}
329
+ function liveTimeline(events){var timeline=[];(events||[]).forEach(function(event){if(event.type!=='agent'||!event.data)return;var item=event.data;if(item.type==='text_reset'){timeline.length=0;return;}if(item.type==='text'){var previous=timeline[timeline.length-1];if(previous&&previous.type==='text')previous.text+=(item.text||'');else timeline.push({type:'text',text:item.text||''});return;}if(item.type==='tool_use')timeline.push({type:'tool_use',id:item.id,name:item.name,input:item.input});else if(item.type==='tool_result')timeline.push({type:'tool_result',tool_use_id:item.tool_use_id,name:item.name,output:item.content,is_error:!!item.is_error});});return timeline;}
330
+ function legacyToolsHtml(toolCalls,messageIndex){var tools='';(toolCalls||[]).forEach(function(call,index){tools+=toolCardHtml({uiKey:call.id?'tool:'+call.id:'message:'+messageIndex+':tool:'+index,name:call.name,input:call.input,output:call.output,isError:!!call.is_error,pending:false});});return tools;}
331
+ function messageHtml(role,content,toolCalls,messageIndex,timeline){var decoded=role==='user'?decodeAttachmentMessage(content):{text:withoutToolTranscript(content),attachments:[]};var body;if(role==='user')body='<div class="markdown">'+markdown(decoded.text)+'</div>'+attachmentGallery(decoded.attachments,state.detail.sessionId);else if(timeline&&timeline.length)body=timelineHtml(timeline,messageIndex);else body=legacyToolsHtml(toolCalls,messageIndex)+'<div class="markdown">'+markdown(decoded.text)+'</div>';return '<article class="message '+role+'"><div class="avatar">'+(role==='user'?'你':'D')+'</div><div class="bubble"><div class="role">'+(role==='user'?'YOU':'DEEPCCC')+'</div>'+body+'</div></article>';}
329
332
  function buildToolGroups(events){var groups=[];var byId={};var order=[];(events||[]).forEach(function(event){if(event.type!=='agent'||!event.data)return;var item=event.data;if(item.type==='tool_use'){var id=item.id||('call-'+order.length);var group={id:id,uiKey:'tool:'+id,name:item.name||'tool',input:item.input,output:undefined,isError:false,pending:true};byId[id]=group;order.push(id);groups.push(group);}else if(item.type==='tool_result'){var key=item.tool_use_id;var target=key?byId[key]:null;if(target){target.output=item.content;target.isError=!!item.is_error;target.pending=false;}}});return groups;}
330
333
  function toolCardHtml(call){var open=toolState(call.uiKey+':card');var present=call.name==='present_file'&&!call.pending?artifactFromOutput(call.output):null;var inputHtml=toolSectionHtml(call,'input',call.input,8,4);var outputHtml=call.pending?'':(present?'<section class="tool-section"><div class="tool-section-title">工具结果</div>'+artifactGallery([{name:'present_file',output:call.output}],state.detail.sessionId)+'</section>':toolSectionHtml(call,'output',call.output==null?'(无输出)':call.output,12,6));return'<details class="tool-card" data-tool-state="'+escapeHtml(call.uiKey+':card')+'"'+(open?' open':'')+'><summary><span class="tool-summary-text">'+escapeHtml(toolSummary(call))+'</span></summary>'+inputHtml+outputHtml+'</details>';}
331
334
  function toolCardsHtml(calls){return (calls||[]).map(toolCardHtml).join('');}
@@ -356,11 +359,10 @@ export const DEEPCCC_WEB_PAGE = `<!DOCTYPE html>
356
359
  renderAttachmentDrafts();
357
360
  var html='<div class="thread">';
358
361
  if(!(detail.messages||[]).length&&detail.status!=='running')html+='<div class="empty-thread"><strong>准备开始这个会话</strong><span>在下方输入任务,可附加截图。DeepCCC 会读取当前项目指令、调用工具并持续保存上下文。</span></div>';
359
- (detail.messages||[]).forEach(function(message,index){html+=messageHtml(message.role,message.content,message.toolCalls,index);});
362
+ (detail.messages||[]).forEach(function(message,index){html+=messageHtml(message.role,message.content,message.toolCalls,index,message.timeline);});
360
363
  if(detail.status==='running'){
361
- var live=(detail.events||[]).slice().reverse().find(function(event){return event.type==='agent'&&event.data&&event.data.type==='text';});
362
- if(live)html+=messageHtml('assistant',live.data.accumulated||live.data.text||'',[],'live');
363
- html+=toolCardsHtml(buildToolGroups(detail.events));
364
+ var currentTimeline=liveTimeline(detail.events);
365
+ if(currentTimeline.length)html+=messageHtml('assistant','',[],'live',currentTimeline);
364
366
  }
365
367
  var approvals=(detail.approvals||[]).slice();
366
368
  if(detail.pendingApproval&&!approvals.some(function(item){return item.approvalId===detail.pendingApproval.approvalId;}))approvals.push(Object.assign({status:'pending'},detail.pendingApproval));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepccc",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "A lightweight coding agent with OpenAI-compatible and Anthropic Messages API support.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [