pi2web 0.2.1 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +44 -5
  2. package/dist/activity.js +178 -0
  3. package/dist/agent-feed.js +133 -0
  4. package/dist/agent-templates.js +144 -0
  5. package/dist/agents.js +518 -141
  6. package/dist/auth.js +3 -1
  7. package/dist/collab/baseline.js +84 -0
  8. package/dist/collab/dispatcher.js +45 -10
  9. package/dist/collab/hub.js +1392 -384
  10. package/dist/collab/pi-extension.js +322 -65
  11. package/dist/collab/review-flow.js +197 -48
  12. package/dist/collab/routes.js +94 -23
  13. package/dist/collab/schemas.js +69 -13
  14. package/dist/collab/scoring-flow.js +145 -39
  15. package/dist/collab/similarity.js +15 -0
  16. package/dist/collab/store.js +467 -112
  17. package/dist/collab/types.js +94 -10
  18. package/dist/collab/validate.js +62 -25
  19. package/dist/drafts.js +26 -6
  20. package/dist/http/agent-routes.js +196 -0
  21. package/dist/http/login-limiter.js +53 -0
  22. package/dist/http/mail-settings-routes.js +53 -0
  23. package/dist/http/system-prompt-routes.js +52 -0
  24. package/dist/http/utils.js +43 -0
  25. package/dist/http/web-assets.js +63 -0
  26. package/dist/http/workspace-routes.js +58 -0
  27. package/dist/mail-notifier.js +63 -17
  28. package/dist/main.js +61 -16
  29. package/dist/mcp.js +24 -5
  30. package/dist/metadata-store.js +190 -56
  31. package/dist/paths.js +10 -0
  32. package/dist/sdk-backend.js +221 -45
  33. package/dist/server.js +444 -620
  34. package/dist/session-namer.js +66 -17
  35. package/dist/terminals.js +94 -27
  36. package/dist/timeline.js +185 -0
  37. package/dist/voice.js +153 -35
  38. package/dist/workspaces.js +91 -26
  39. package/dist/ws/agent-socket.js +256 -0
  40. package/dist/ws/terminal-socket.js +46 -0
  41. package/npm-shrinkwrap.json +3685 -0
  42. package/package.json +11 -1
  43. package/packages/protocol/openapi.yaml +6 -2
  44. package/packages/protocol/websocket.schema.json +58 -15
  45. package/web/activity-view.js +121 -0
  46. package/web/app.js +3098 -743
  47. package/web/backend-state.js +59 -27
  48. package/web/collab.js +668 -255
  49. package/web/hid-keyboard.js +173 -140
  50. package/web/index.html +4 -1
  51. package/web/styles.css +14 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi2web",
3
- "version": "0.2.1",
3
+ "version": "0.2.6",
4
4
  "description": "Secure remote web client server for Pi coding agent",
5
5
  "type": "module",
6
6
  "author": "sxwxs",
@@ -18,6 +18,7 @@
18
18
  },
19
19
  "files": [
20
20
  "dist/",
21
+ "npm-shrinkwrap.json",
21
22
  "web/",
22
23
  "packages/protocol/",
23
24
  "packages/edge-tts-server/src/remote_pi_edge_tts/",
@@ -35,6 +36,8 @@
35
36
  "clean": "rm -rf dist",
36
37
  "build": "npm run clean && tsc -p tsconfig.json && chmod +x dist/main.js",
37
38
  "check": "npm run build",
39
+ "format": "prettier --write \"src/**/*.ts\" \"web/*.js\" \"test/**/*.ts\"",
40
+ "format:check": "prettier --check \"src/**/*.ts\" \"web/*.js\" \"test/**/*.ts\"",
38
41
  "test": "vitest run",
39
42
  "dev": "tsx src/main.ts",
40
43
  "dev:watch": "tsx watch src/main.ts",
@@ -46,8 +49,14 @@
46
49
  "node": ">=22.19.0"
47
50
  },
48
51
  "dependencies": {
52
+ "@earendil-works/chord": "1.1.0",
53
+ "@earendil-works/pi-agent-core": "1.1.0",
49
54
  "@earendil-works/pi-ai": "1.1.0",
55
+ "@earendil-works/pi-codemode": "1.1.0",
50
56
  "@earendil-works/pi-coding-agent": "1.1.0",
57
+ "@earendil-works/pi-mcp": "1.1.0",
58
+ "@earendil-works/pi-telemetry": "1.1.0",
59
+ "@earendil-works/pi-tui": "1.1.0",
51
60
  "@xterm/addon-fit": "^0.11.0",
52
61
  "@xterm/xterm": "^6.0.0",
53
62
  "better-sqlite3": "^12.11.1",
@@ -61,6 +70,7 @@
61
70
  "@types/better-sqlite3": "^7.6.13",
62
71
  "@types/node": "^22.0.0",
63
72
  "@types/ws": "^8.5.0",
73
+ "prettier": "3.6.2",
64
74
  "tsx": "^4.19.0",
65
75
  "typescript": "^5.9.3",
66
76
  "vitest": "^3.0.0"
@@ -1,5 +1,5 @@
1
1
  openapi: 3.1.0
2
- info: {title: Remote Pi API, version: 1.0.0}
2
+ info: {title: Remote Pi API, version: 2.0.0}
3
3
  servers: [{url: /api/v1}]
4
4
  security: [{bearerAuth: []}]
5
5
  paths:
@@ -19,8 +19,12 @@ paths:
19
19
  /agents:
20
20
  get: {operationId: listAgents, responses: {'200': {$ref: '#/components/responses/Success'}}}
21
21
  post: {operationId: createAgent, requestBody: {$ref: '#/components/requestBodies/Json'}, responses: {'201': {$ref: '#/components/responses/Success'}}}
22
+ /agents/{id}/timeline:
23
+ get: {operationId: agentTimeline, description: 'Older history as display items (see timelineItem in websocket.schema.json): about `limit` items before item `before`, never splitting a tool call from its result. Data {sessionId, items, total, start, hasMore}. Viewers get the newest page from the WebSocket focus snapshot. 404 when `before` is not on the current branch.', parameters: [{$ref: '#/components/parameters/Id'}, {name: limit, in: query, schema: {type: integer, minimum: 1, maximum: 500, default: 50}}, {name: before, in: query, schema: {type: string}}], responses: {'200': {$ref: '#/components/responses/Success'}}}
24
+ /agents/{id}/sessions/{sessionId}/items/{itemId}:
25
+ get: {operationId: agentTimelineItem, description: 'Full body of one timeline item, data {id, body}. Items never change, so it is served with Cache-Control private, max-age=31536000, immutable. 404 when the Agent is no longer on that session or branch.', parameters: [{$ref: '#/components/parameters/Id'}, {name: sessionId, in: path, required: true, schema: {type: string}}, {name: itemId, in: path, required: true, schema: {type: string}}], responses: {'200': {$ref: '#/components/responses/Success'}}}
22
26
  /agents/{id}/{action}:
23
- get: {operationId: getAgentResource, parameters: [{$ref: '#/components/parameters/Id'}, {name: action, in: path, required: true, schema: {enum: [state,messages,capabilities,session]}}], responses: {'200': {$ref: '#/components/responses/Success'}}}
27
+ get: {operationId: getAgentResource, parameters: [{$ref: '#/components/parameters/Id'}, {name: action, in: path, required: true, schema: {enum: [state,capabilities,session]}}], responses: {'200': {$ref: '#/components/responses/Success'}}}
24
28
  post: {operationId: agentCommand, parameters: [{$ref: '#/components/parameters/Id'}, {name: action, in: path, required: true, schema: {enum: [prompt,steer,follow-up,abort,compact,model,thinking,session-name,navigate,fork,extension-response]}}], requestBody: {$ref: '#/components/requestBodies/Json'}, responses: {'200': {$ref: '#/components/responses/Success'}}}
25
29
  /draft:
26
30
  post: {operationId: draftText, description: 'Insert text into the currently displayed Web UI input box on this backend, without sending it to an Agent. Body {text, mode?: append|replace, source?}; explicit agent/slot targets are rejected. Pending text is kept in memory until a page acknowledges it.', requestBody: {$ref: '#/components/requestBodies/Json'}, responses: {'200': {$ref: '#/components/responses/Success'}}}
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://remote-pi.local/protocol/v1/websocket.schema.json",
4
- "title": "Remote Pi WebSocket v1",
3
+ "$id": "https://remote-pi.local/protocol/v2/websocket.schema.json",
4
+ "title": "Remote Pi WebSocket v2",
5
+ "description": "Nothing is replayed. subscribe_all gives every Agent's state, then overview events of every Agent. focus answers with a snapshot of one Agent (newest timeline page, streamed text, dialogs); after it, on this socket and in order, come only appended items, streamed text and shell output of that Agent, or timeline_reset (focus again for a new snapshot). Timeline items never change. HTTP serves only what never changes: older pages (GET /api/v1/agents/{id}/timeline?before=) and item bodies (GET /api/v1/agents/{id}/sessions/{sessionId}/items/{itemId}).",
5
6
  "oneOf": [
6
- {"$ref":"#/$defs/subscribe"}, {"$ref":"#/$defs/command"}, {"$ref":"#/$defs/subscribed"},
7
- {"$ref":"#/$defs/commandResult"}, {"$ref":"#/$defs/agentEvent"}, {"$ref":"#/$defs/agentSnapshot"}, {"$ref":"#/$defs/agentStateUpdate"},
7
+ {"$ref":"#/$defs/subscribeAll"}, {"$ref":"#/$defs/subscribedAll"}, {"$ref":"#/$defs/focus"}, {"$ref":"#/$defs/watch"},
8
+ {"$ref":"#/$defs/command"}, {"$ref":"#/$defs/commandResult"}, {"$ref":"#/$defs/agentEvent"}, {"$ref":"#/$defs/agentStateUpdate"},
9
+ {"$ref":"#/$defs/focusState"}, {"$ref":"#/$defs/focusError"}, {"$ref":"#/$defs/delta"}, {"$ref":"#/$defs/items"}, {"$ref":"#/$defs/timelineReset"}, {"$ref":"#/$defs/toolOutput"},
8
10
  {"$ref":"#/$defs/subscribeDrafts"}, {"$ref":"#/$defs/subscribedDrafts"}, {"$ref":"#/$defs/draftAck"}, {"$ref":"#/$defs/draftEvent"}
9
11
  ],
10
12
  "$defs": {
@@ -12,15 +14,58 @@
12
14
  "subscribedDrafts": {"type":"object","required":["type","protocolVersion"],"properties":{"type":{"const":"subscribed_drafts"},"protocolVersion":{"const":1}}},
13
15
  "draftAck": {"type":"object","additionalProperties":false,"required":["type","id"],"properties":{"type":{"const":"draft_ack"},"id":{"type":"string"}}},
14
16
  "draftEvent": {"type":"object","required":["type","draft"],"properties":{"type":{"const":"draft_event"},"replay":{"type":"boolean"},"draft":{"type":"object","required":["id","text","mode","createdAt"],"properties":{"id":{"type":"string"},"text":{"type":"string"},"mode":{"enum":["append","replace"]},"source":{"type":"string"},"createdAt":{"type":"string"}}}}},
15
- "subscribe": {"type":"object","additionalProperties":false,"required":["type","agentId"],"properties":{"type":{"const":"subscribe"},"agentId":{"type":"string"},"lastSequence":{"type":"integer","minimum":0},"fromNow":{"type":"boolean"},"messageLimit":{"type":"integer","minimum":1,"maximum":200}}},
17
+ "subscribeAll": {"type":"object","additionalProperties":false,"required":["type"],"properties":{"type":{"const":"subscribe_all"}},"description":"Receive agent_state for every Agent, then overview agent_event messages of every Agent."},
18
+ "subscribedAll": {"type":"object","required":["type","protocolVersion","serverTime"],"properties":{"type":{"const":"subscribed_all"},"protocolVersion":{"const":2},"serverTime":{"type":"integer","description":"Server epoch milliseconds, to correct clock skew for activity times."}}},
19
+ "focus": {"type":"object","additionalProperties":false,"required":["type","agentId"],"properties":{"type":{"const":"focus"},"agentId":{"type":["string","null"]},"limit":{"type":"integer","minimum":1,"maximum":500,"description":"Size of the snapshot's page, 50 by default."}},"description":"View one Agent in detail (null: none). Loads it if needed and answers with focus_state, or focus_error; replaces the previous focus and its watches."},
20
+ "watch": {"type":"object","additionalProperties":false,"required":["type","toolCallId"],"properties":{"type":{"enum":["watch","unwatch"]},"toolCallId":{"type":"string"}},"description":"Start or stop tool_output for a running tool of the focused Agent, e.g. while its card is expanded."},
16
21
  "command": {"type":"object","additionalProperties":false,"required":["type","requestId","agentId"],"properties":{"type":{"enum":["prompt","steer","follow-up","abort","clear-queue"]},"requestId":{"type":"string"},"agentId":{"type":"string"},"message":{"type":"string","maxLength":2097152}}},
17
- "subscribed": {"type":"object","required":["type","agentId","protocolVersion"],"properties":{"type":{"const":"subscribed"},"agentId":{"type":"string"},"protocolVersion":{"const":1},"currentSequence":{"type":"integer","minimum":0}}},
18
- "commandResult": {"type":"object","required":["type","success"],"properties":{"type":{"const":"command_result"},"requestId":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object"},"disposition":{"enum":["handled","queued"],"description":"For steer and follow-up: whether the running turn took the message or it waits in the queue. Omitted by older servers."},"queue":{"$ref":"#/$defs/queue"}}},
22
+ "commandResult": {"type":"object","required":["type","success"],"properties":{"type":{"const":"command_result"},"requestId":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object"},"disposition":{"enum":["handled","queued"],"description":"For steer and follow-up: whether the running turn took the message or it waits in the queue."},"queue":{"$ref":"#/$defs/queue"}}},
19
23
  "queue": {"type":"object","required":["steering","followUp"],"description":"Messages waiting for the running turn.","properties":{"steering":{"type":"array","items":{"type":"string"}},"followUp":{"type":"array","items":{"type":"string"}}}},
20
- "agentEvent": {"type":"object","required":["type","agentId","eventId","sequence","event"],"properties":{"type":{"const":"agent_event"},"agentId":{"type":"string"},"eventId":{"type":"string"},"sequence":{"type":"integer","minimum":1},"timestamp":{"type":"integer","minimum":0},"status":{"$ref":"#/$defs/agentStatus","description":"Agent status after this event. Omitted by older servers."},"event":{"type":"object","required":["type"]}}},
24
+ "agentEvent": {"type":"object","required":["type","agentId","timestamp","status","event"],"properties":{"type":{"const":"agent_event"},"agentId":{"type":"string"},"timestamp":{"type":"integer","minimum":0,"description":"Epoch milliseconds."},"status":{"$ref":"#/$defs/agentStatus","description":"Agent status after this event."},"event":{"type":"object","required":["type"],"description":"Overview events of every Agent: agent_loaded (its backend started on demand), agent_start, agent_end {willRetry, aborted}, agent_settled, activity_changed {activity}, queue_update, session_info_changed, agent_unloaded, auto_retry_*, summarization_retry_*, extension_ui_*. For the focused Agent also bash_execution_start/update/end (output of user shell commands)."}}},
25
+ "focusState": {"type":"object","required":["type","agentId","sessionId","page","live"],"description":"Snapshot of the focused Agent. Every detail message after it is newer.","properties":{"type":{"const":"focus_state"},"agentId":{"type":"string"},"sessionId":{"type":"string"},"page":{"type":"object","required":["items","total","start","hasMore"],"properties":{"items":{"type":"array","items":{"$ref":"#/$defs/timelineItem"}},"total":{"type":"integer"},"start":{"type":"integer"},"hasMore":{"type":"boolean"}}},"live":{"oneOf":[{"type":"null"},{"type":"object","required":["text","thinking"],"properties":{"text":{"type":"string"},"thinking":{"type":"string"}}}],"description":"Text streamed so far for the message being generated."},"dialogRequests":{"type":"array","description":"Pending dialogs, to rebuild their controls in the transcript."}}},
26
+ "focusError": {"type":"object","required":["type","agentId","error"],"properties":{"type":{"const":"focus_error"},"agentId":{"type":"string"},"error":{"type":"object"}}},
27
+ "delta": {"type":"object","required":["type","agentId","kind","delta"],"properties":{"type":{"const":"delta"},"agentId":{"type":"string"},"kind":{"enum":["text","thinking"]},"delta":{"type":"string"}},"description":"More streamed text; adjacent tokens are coalesced."},
28
+ "items": {"type":"object","required":["type","agentId","sessionId","items"],"properties":{"type":{"const":"items"},"agentId":{"type":"string"},"sessionId":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/$defs/timelineItem"},"description":"Appended to the timeline."},"clearLive":{"const":true,"description":"They record the streamed message: drop its streamed text."}}},
29
+ "timelineReset": {"type":"object","required":["type","agentId","sessionId"],"properties":{"type":{"const":"timeline_reset"},"agentId":{"type":"string"},"sessionId":{"type":"string"}},"description":"The branch changed other than by appending: focus again for a new snapshot."},
30
+ "toolOutput": {"type":"object","required":["type","agentId","toolCallId","output"],"description":"Output so far of a watched running tool, at most one per tool per 100 ms.","properties":{"type":{"const":"tool_output"},"agentId":{"type":"string"},"toolCallId":{"type":"string"},"output":{"type":"string"}}},
31
+ "timelineItem": {
32
+ "type":"object","required":["id","kind","size"],
33
+ "description":"One displayable part of one recorded session entry; it never changes. A tool's call and result are separate items paired by toolCallId (a page never splits them); whether a call is running is the Agent's activity. Bodies of user, assistant and error items, and other bodies up to 256 characters, are inline; others load from the item endpoint, cacheable forever.",
34
+ "properties":{
35
+ "id":{"type":"string"},
36
+ "kind":{"enum":["user","assistant","thinking","error","tool_call","tool_result","bash","compaction","branch_summary","custom","system"]},
37
+ "timestamp":{"type":"integer"},
38
+ "title":{"type":"string","description":"Tool name, shell command, or what a system item is."},
39
+ "summary":{"type":"string"},
40
+ "status":{"enum":["ok","error","cancelled"],"description":"tool_result and bash: how it ended."},
41
+ "durationMs":{"type":"number"},
42
+ "toolCallId":{"type":"string"},
43
+ "size":{"type":"integer","minimum":0},
44
+ "body":{"type":"string"}
45
+ }
46
+ },
21
47
  "agentStatus": {"enum":["unloaded","starting","idle","streaming","waiting_for_user","error","stopping","stopped"]},
22
- "agentSnapshot": {"type":"object","required":["type","agentId","lastSequence","state","messages"],"properties":{"type":{"const":"agent_snapshot"},"agentId":{"type":"string"},"lastSequence":{"type":"integer","minimum":0},"state":{"$ref":"#/$defs/agentState"},"messages":{"type":"array"}}},
23
- "agentStateUpdate": {"type":"object","required":["type","agentId","sequence","state"],"description":"Current state after initial replay/snapshot and before live events, also sent for fromNow or empty replay. Does not replace transcript messages.","properties":{"type":{"const":"agent_state"},"agentId":{"type":"string"},"sequence":{"type":"integer","minimum":0},"state":{"$ref":"#/$defs/agentState"}}},
48
+ "agentStateUpdate": {"type":"object","required":["type","agentId","state"],"description":"Current state of one Agent, sent for every Agent on subscribe_all.","properties":{"type":{"const":"agent_state"},"agentId":{"type":"string"},"state":{"$ref":"#/$defs/agentState"}}},
49
+ "activity": {
50
+ "type":"object","required":["run","phase","bash","dialogs"],
51
+ "description":"What the Agent is doing now, maintained by the server. Also pushed as agent event {type:'activity_changed', activity} on every change. Times are server epoch milliseconds; correct skew with subscribed_all.serverTime.",
52
+ "properties":{
53
+ "run":{"oneOf":[{"type":"null"},{"type":"object","required":["startedAt"],"properties":{"startedAt":{"type":"integer"}}}]},
54
+ "phase":{
55
+ "type":"object","required":["kind","since"],
56
+ "properties":{
57
+ "kind":{"enum":["idle","waiting_llm","thinking","generating","preparing_tool","running_tools","retrying","compacting","stopping"]},
58
+ "since":{"type":"integer"},
59
+ "toolName":{"type":"string","description":"preparing_tool"},
60
+ "tools":{"type":"array","description":"running_tools: top-level tools in parallel","items":{"type":"object","required":["toolCallId","toolName","since"],"properties":{"toolCallId":{"type":"string"},"toolName":{"type":"string"},"summary":{"type":"string","description":"Command, path or pattern, at most 80 characters"},"since":{"type":"integer"}}}},
61
+ "attempt":{"type":"integer"},"maxAttempts":{"type":"integer"},"retryAt":{"type":"integer"},"error":{"type":"string"},
62
+ "reason":{"type":"string","description":"compacting"}
63
+ }
64
+ },
65
+ "bash":{"type":"array","items":{"type":"object","required":["id","since"],"properties":{"id":{"type":"string"},"command":{"type":"string"},"since":{"type":"integer"}}}},
66
+ "dialogs":{"type":"array","items":{"type":"object","required":["requestId","since"],"properties":{"requestId":{"type":"string"},"title":{"type":"string"},"since":{"type":"integer"}}}}
67
+ }
68
+ },
24
69
  "agentState": {
25
70
  "type":"object","required":["status","agentId","sessionId","cwd"],
26
71
  "properties":{
@@ -31,13 +76,11 @@
31
76
  "sessionName":{"type":"string"},
32
77
  "cwd":{"type":"string"},
33
78
  "activity":{
34
- "type":"object","required":["bashIds","dialogIds"],
35
- "description":"Runtime operations independent of LLM status. Omitted by older servers; never persisted.",
79
+ "description":"Live activity plus dialog payloads and queue. Never persisted.",
80
+ "allOf":[{"$ref":"#/$defs/activity"}],
36
81
  "properties":{
37
- "bashIds":{"type":"array","uniqueItems":true,"items":{"type":"string"}},
38
- "dialogIds":{"type":"array","uniqueItems":true,"items":{"type":"string"}},
39
82
  "dialogRequests":{
40
- "type":"array","description":"Pending request payloads for rebuilding dialog controls, without submitted answers. Omitted by older servers.",
83
+ "type":"array","description":"Pending request payloads for rebuilding dialog controls, without submitted answers.",
41
84
  "items":{
42
85
  "type":"object","required":["type","requestId","kind"],
43
86
  "properties":{
@@ -0,0 +1,121 @@
1
+ (() => {
2
+ 'use strict';
3
+
4
+ // Presentation of the server-maintained Agent activity (see src/activity.ts).
5
+ // The server reports what is happening and since when; elapsed times tick locally.
6
+ const PHASE_TEXT = {
7
+ waiting_llm: ['⏳', '等待 LLM 响应'],
8
+ thinking: ['💭', 'LLM 思考中'],
9
+ generating: ['✍️', 'LLM 输出中'],
10
+ preparing_tool: ['🛠️', '生成工具参数'],
11
+ running_tools: ['🔧', '执行工具'],
12
+ retrying: ['🔁', '重试'],
13
+ compacting: ['🗜️', '压缩上下文'],
14
+ stopping: ['⏹️', '正在停止']
15
+ };
16
+
17
+ const isBusy = activity =>
18
+ !!activity &&
19
+ (!!activity.run ||
20
+ (!!activity.phase?.kind && activity.phase.kind !== 'idle') ||
21
+ !!activity.bash?.length ||
22
+ !!activity.dialogs?.length);
23
+
24
+ function formatElapsed(ms) {
25
+ const total = Math.max(0, Math.floor(ms / 1000)),
26
+ h = Math.floor(total / 3600),
27
+ m = Math.floor((total % 3600) / 60),
28
+ s = String(total % 60).padStart(2, '0');
29
+ return h ? `${h}:${String(m).padStart(2, '0')}:${s}` : `${m}:${s}`;
30
+ }
31
+
32
+ /** What an Agent is doing, most important first: open dialogs, the run's phase, then user shell commands. */
33
+ function activityParts(activity) {
34
+ if (!activity) return [];
35
+ const parts = (activity.dialogs || []).map(dialog => ({
36
+ icon: '❓',
37
+ text: `等待你回答${dialog.title ? `:${dialog.title}` : ''}`,
38
+ since: dialog.since
39
+ }));
40
+ const phase = activity.phase;
41
+ if (phase && phase.kind !== 'idle') {
42
+ const [icon, label] = PHASE_TEXT[phase.kind] || ['•', phase.kind];
43
+ if (phase.kind === 'running_tools')
44
+ for (const tool of phase.tools || [])
45
+ parts.push({
46
+ icon,
47
+ text: `${tool.toolName}${tool.summary ? `: ${tool.summary}` : ''}`,
48
+ since: tool.since
49
+ });
50
+ else if (phase.kind === 'preparing_tool')
51
+ parts.push({
52
+ icon,
53
+ text: `${label}${phase.toolName ? `:${phase.toolName}` : ''}`,
54
+ since: phase.since
55
+ });
56
+ else if (phase.kind === 'retrying')
57
+ parts.push({
58
+ icon,
59
+ text: `${label}${phase.attempt ? ` ${phase.attempt}/${phase.maxAttempts || '?'}` : ''}${phase.error ? ` · ${phase.error}` : ''}`,
60
+ since: phase.since
61
+ });
62
+ else parts.push({icon, text: label, since: phase.since});
63
+ }
64
+ for (const bash of activity.bash || [])
65
+ parts.push({icon: '$', text: bash.command || 'bash', since: bash.since});
66
+ return parts;
67
+ }
68
+
69
+ /**
70
+ * DOM builders. `serverNow(backendId)` returns that backend's current time, so
71
+ * client clock skew does not distort elapsed times.
72
+ */
73
+ function createActivityView(serverNow) {
74
+ function elapsed(backendId, since) {
75
+ const span = document.createElement('span');
76
+ span.className = 'elapsed';
77
+ span.dataset.since = String(since);
78
+ span.dataset.backend = backendId || '';
79
+ span.textContent = formatElapsed(serverNow(backendId) - since);
80
+ return span;
81
+ }
82
+ function chip(part, backendId) {
83
+ const node = document.createElement('span'),
84
+ text = document.createElement('span');
85
+ node.className = 'activity-chip';
86
+ text.className = 'activity-text';
87
+ text.textContent = `${part.icon} ${part.text}`;
88
+ text.title = part.text;
89
+ node.append(text, elapsed(backendId, part.since));
90
+ return node;
91
+ }
92
+ /** The full activity of the selected Agent: every part plus the run's total time. */
93
+ function bar(activity, backendId) {
94
+ const nodes = activityParts(activity).map(part => chip(part, backendId));
95
+ if (activity?.run) {
96
+ const run = document.createElement('span');
97
+ run.className = 'activity-run';
98
+ run.append('本次运行 ', elapsed(backendId, activity.run.startedAt));
99
+ nodes.push(run);
100
+ }
101
+ return nodes;
102
+ }
103
+ /** One compact line for the Agent list: the most important part and how many more there are. */
104
+ function line(activity, backendId) {
105
+ const parts = activityParts(activity);
106
+ if (!parts.length) return null;
107
+ const node = document.createElement('div');
108
+ node.className = 'agent-activity';
109
+ node.append(chip(parts[0], backendId));
110
+ if (parts.length > 1) node.append(` +${parts.length - 1}`);
111
+ return node;
112
+ }
113
+ function tick() {
114
+ for (const el of document.querySelectorAll('.elapsed[data-since]'))
115
+ el.textContent = formatElapsed(serverNow(el.dataset.backend) - Number(el.dataset.since));
116
+ }
117
+ return {bar, line, tick};
118
+ }
119
+
120
+ window.ActivityView = {isBusy, formatElapsed, activityParts, createActivityView};
121
+ })();