@keith9681/dsh-sidebar 0.15.2
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/LICENSE +21 -0
- package/README.md +0 -0
- package/README_EN.md +0 -0
- package/cordis.patch.yml +49 -0
- package/lib/client-editor.js +34651 -0
- package/lib/client-mermaid.js +200629 -0
- package/lib/client-registry.js +13388 -0
- package/lib/client-terminal.js +8904 -0
- package/lib/client.js +13388 -0
- package/lib/index.js +3959 -0
- package/lib/invariant.js +22 -0
- package/lib/types/agent-pty.d.ts +226 -0
- package/lib/types/browser-probe.d.ts +15 -0
- package/lib/types/bundle-route.d.ts +12 -0
- package/lib/types/client/BrowserView.d.ts +22 -0
- package/lib/types/client/DiffTab.d.ts +6 -0
- package/lib/types/client/DiffView.d.ts +51 -0
- package/lib/types/client/EditorHost.d.ts +12 -0
- package/lib/types/client/FileTree.d.ts +36 -0
- package/lib/types/client/GitView.d.ts +8 -0
- package/lib/types/client/OrphanedTab.d.ts +11 -0
- package/lib/types/client/PdfView.d.ts +6 -0
- package/lib/types/client/RenderBoundary.d.ts +32 -0
- package/lib/types/client/SandboxStatusBar.d.ts +10 -0
- package/lib/types/client/SideCardSection.d.ts +74 -0
- package/lib/types/client/SideChatView.d.ts +16 -0
- package/lib/types/client/Sidebar.d.ts +6 -0
- package/lib/types/client/SubagentView.d.ts +16 -0
- package/lib/types/client/TabBar.d.ts +43 -0
- package/lib/types/client/TerminalView.d.ts +24 -0
- package/lib/types/client/TextEditor.d.ts +10 -0
- package/lib/types/client/TreePanel.d.ts +23 -0
- package/lib/types/client/UploadOverlay.d.ts +20 -0
- package/lib/types/client/add-plugin-modal.d.ts +15 -0
- package/lib/types/client/api.d.ts +268 -0
- package/lib/types/client/binary-download.d.ts +12 -0
- package/lib/types/client/breakpoints.d.ts +12 -0
- package/lib/types/client/browser.d.ts +50 -0
- package/lib/types/client/builtins/index.d.ts +18 -0
- package/lib/types/client/builtins/tabs.d.ts +11 -0
- package/lib/types/client/builtins/viewers.d.ts +3 -0
- package/lib/types/client/chunk-loader.d.ts +124 -0
- package/lib/types/client/chunks/editor.d.ts +9 -0
- package/lib/types/client/chunks/mermaid.d.ts +10 -0
- package/lib/types/client/chunks/terminal.d.ts +9 -0
- package/lib/types/client/cm-themes.d.ts +24 -0
- package/lib/types/client/conversation-draft.d.ts +14 -0
- package/lib/types/client/desktop-env.d.ts +35 -0
- package/lib/types/client/editable-state.d.ts +27 -0
- package/lib/types/client/editor-load.d.ts +66 -0
- package/lib/types/client/frame-batcher.d.ts +24 -0
- package/lib/types/client/icons.d.ts +57 -0
- package/lib/types/client/image-types.d.ts +3 -0
- package/lib/types/client/ime-guard.d.ts +36 -0
- package/lib/types/client/index.d.ts +19 -0
- package/lib/types/client/intercept.d.ts +33 -0
- package/lib/types/client/lang.d.ts +12 -0
- package/lib/types/client/lazy-chunk.d.ts +25 -0
- package/lib/types/client/link-intercept.d.ts +42 -0
- package/lib/types/client/locales.d.ts +366 -0
- package/lib/types/client/mermaid-blocks.d.ts +41 -0
- package/lib/types/client/mermaid-sanitize.d.ts +2 -0
- package/lib/types/client/mermaid.d.ts +12 -0
- package/lib/types/client/open-when-sized.d.ts +20 -0
- package/lib/types/client/open-with-settings.d.ts +4 -0
- package/lib/types/client/open-with.d.ts +87 -0
- package/lib/types/client/openpath-intercept.d.ts +41 -0
- package/lib/types/client/paths.d.ts +35 -0
- package/lib/types/client/pdf-types.d.ts +2 -0
- package/lib/types/client/plugin-settings.d.ts +8 -0
- package/lib/types/client/plugins-shared.d.ts +27 -0
- package/lib/types/client/plugins-tabs.d.ts +3 -0
- package/lib/types/client/plugins-viewers.d.ts +3 -0
- package/lib/types/client/prefs.d.ts +41 -0
- package/lib/types/client/produced-files.d.ts +23 -0
- package/lib/types/client/selection-payload.d.ts +27 -0
- package/lib/types/client/service.d.ts +440 -0
- package/lib/types/client/settings-nav-icon.d.ts +19 -0
- package/lib/types/client/shell-presets.d.ts +48 -0
- package/lib/types/client/sidechat-transcript.d.ts +103 -0
- package/lib/types/client/split-pane.d.ts +30 -0
- package/lib/types/client/state.d.ts +359 -0
- package/lib/types/client/subagent-detect.d.ts +54 -0
- package/lib/types/client/subagent-jobs.d.ts +63 -0
- package/lib/types/client/tab-content-memo.d.ts +33 -0
- package/lib/types/client/terminal-font.d.ts +20 -0
- package/lib/types/client/theme.d.ts +45 -0
- package/lib/types/client/titlebar-strip.d.ts +23 -0
- package/lib/types/client/upload.d.ts +69 -0
- package/lib/types/client/wco.d.ts +47 -0
- package/lib/types/config.d.ts +64 -0
- package/lib/types/context-types.d.ts +603 -0
- package/lib/types/fs-operations.d.ts +28 -0
- package/lib/types/fs-search.d.ts +24 -0
- package/lib/types/fs-tree.d.ts +51 -0
- package/lib/types/git.d.ts +82 -0
- package/lib/types/html-route.d.ts +59 -0
- package/lib/types/index.d.ts +47 -0
- package/lib/types/invariant.d.ts +15 -0
- package/lib/types/jobs-routes.d.ts +45 -0
- package/lib/types/open-external.d.ts +24 -0
- package/lib/types/prefs-shared.d.ts +225 -0
- package/lib/types/pty-deps.d.ts +78 -0
- package/lib/types/pty-manager.d.ts +168 -0
- package/lib/types/sidechat-core.d.ts +172 -0
- package/lib/types/sidechat-routes.d.ts +30 -0
- package/lib/types/subagent-activity.d.ts +44 -0
- package/lib/types/subagent-live-route.d.ts +44 -0
- package/lib/types/tools.d.ts +31 -0
- package/lib/types/trust-fence.d.ts +24 -0
- package/lib/types/wire.d.ts +38 -0
- package/package.json +173 -0
- package/scripts/install.ps1 +289 -0
- package/scripts/install.sh +287 -0
- package/src/agent-pty.ts +527 -0
- package/src/browser-probe.ts +26 -0
- package/src/bundle-route.ts +129 -0
- package/src/client/BrowserView.tsx +253 -0
- package/src/client/DiffTab.tsx +110 -0
- package/src/client/DiffView.tsx +305 -0
- package/src/client/EditorHost.tsx +525 -0
- package/src/client/FileTree.tsx +653 -0
- package/src/client/GitView.tsx +558 -0
- package/src/client/OrphanedTab.tsx +27 -0
- package/src/client/PdfView.tsx +110 -0
- package/src/client/RenderBoundary.tsx +49 -0
- package/src/client/SandboxStatusBar.tsx +60 -0
- package/src/client/SideCardSection.module.css +960 -0
- package/src/client/SideCardSection.tsx +1152 -0
- package/src/client/SideChatView.module.css +437 -0
- package/src/client/SideChatView.tsx +644 -0
- package/src/client/Sidebar.tsx +1148 -0
- package/src/client/SubagentView.module.css +547 -0
- package/src/client/SubagentView.tsx +883 -0
- package/src/client/TabBar.tsx +305 -0
- package/src/client/TerminalView.tsx +383 -0
- package/src/client/TextEditor.tsx +500 -0
- package/src/client/TreePanel.tsx +273 -0
- package/src/client/UploadOverlay.tsx +62 -0
- package/src/client/add-plugin-modal.tsx +199 -0
- package/src/client/api.ts +335 -0
- package/src/client/binary-download.tsx +23 -0
- package/src/client/breakpoints.ts +51 -0
- package/src/client/browser.ts +119 -0
- package/src/client/builtins/index.ts +37 -0
- package/src/client/builtins/tabs.tsx +331 -0
- package/src/client/builtins/viewers.tsx +125 -0
- package/src/client/chunk-loader.ts +351 -0
- package/src/client/chunks/editor.tsx +9 -0
- package/src/client/chunks/mermaid.tsx +10 -0
- package/src/client/chunks/terminal.tsx +9 -0
- package/src/client/cm-themes.ts +129 -0
- package/src/client/conversation-draft.ts +29 -0
- package/src/client/css-modules.d.ts +5 -0
- package/src/client/desktop-env.ts +72 -0
- package/src/client/editable-state.ts +70 -0
- package/src/client/editor-load.ts +92 -0
- package/src/client/frame-batcher.ts +56 -0
- package/src/client/icons.tsx +161 -0
- package/src/client/image-types.ts +8 -0
- package/src/client/ime-guard.ts +47 -0
- package/src/client/index.tsx +369 -0
- package/src/client/intercept.tsx +110 -0
- package/src/client/lang.ts +103 -0
- package/src/client/layout.css +94 -0
- package/src/client/lazy-chunk.tsx +89 -0
- package/src/client/link-intercept.ts +73 -0
- package/src/client/locales.ts +734 -0
- package/src/client/mermaid-blocks.ts +110 -0
- package/src/client/mermaid-sanitize.ts +93 -0
- package/src/client/mermaid.tsx +401 -0
- package/src/client/open-when-sized.ts +43 -0
- package/src/client/open-with-settings.tsx +135 -0
- package/src/client/open-with.ts +221 -0
- package/src/client/openpath-intercept.ts +62 -0
- package/src/client/paths.ts +47 -0
- package/src/client/pdf-types.ts +4 -0
- package/src/client/plugin-settings.ts +43 -0
- package/src/client/plugins-shared.ts +29 -0
- package/src/client/plugins-tabs.ts +87 -0
- package/src/client/plugins-viewers.ts +30 -0
- package/src/client/prefs.ts +207 -0
- package/src/client/produced-files.ts +91 -0
- package/src/client/selection-payload.ts +86 -0
- package/src/client/service.ts +838 -0
- package/src/client/settings-nav-icon.ts +45 -0
- package/src/client/shell-presets.ts +83 -0
- package/src/client/sidebar.module.css +2577 -0
- package/src/client/sidechat-transcript.ts +321 -0
- package/src/client/split-pane.tsx +322 -0
- package/src/client/state.ts +1175 -0
- package/src/client/subagent-detect.ts +137 -0
- package/src/client/subagent-jobs.ts +161 -0
- package/src/client/tab-content-memo.ts +46 -0
- package/src/client/terminal-font.ts +28 -0
- package/src/client/theme.ts +110 -0
- package/src/client/titlebar-strip.ts +37 -0
- package/src/client/upload.ts +187 -0
- package/src/client/wco.ts +120 -0
- package/src/config.ts +155 -0
- package/src/context-types.ts +610 -0
- package/src/fs-operations.ts +99 -0
- package/src/fs-search.ts +86 -0
- package/src/fs-tree.ts +158 -0
- package/src/git.ts +244 -0
- package/src/html-route.ts +106 -0
- package/src/index.ts +1132 -0
- package/src/invariant.ts +32 -0
- package/src/jobs-routes.ts +257 -0
- package/src/open-external.ts +90 -0
- package/src/prefs-shared.ts +268 -0
- package/src/pty-deps.ts +240 -0
- package/src/pty-manager.ts +376 -0
- package/src/sidechat-core.ts +468 -0
- package/src/sidechat-routes.ts +344 -0
- package/src/subagent-activity.ts +91 -0
- package/src/subagent-live-route.ts +93 -0
- package/src/tools.ts +479 -0
- package/src/trust-fence.ts +77 -0
- package/src/wire.ts +100 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure side-conversation ("Side Chat") logic shared by the host routes and
|
|
3
|
+
* the client tab. Framework-free (no React, no Node) so both halves and the
|
|
4
|
+
* node test environment can import it.
|
|
5
|
+
*
|
|
6
|
+
* A side thread is a child session the plugin creates ITSELF with a custom
|
|
7
|
+
* seed — the parent session's FULL event log up to the click moment
|
|
8
|
+
* (completed turns, the unanswered user message, and — when the parent is
|
|
9
|
+
* mid-turn — the in-progress assistant output and tool activity). The log
|
|
10
|
+
* model forbids open-turn seeds, so an in-flight parent turn is copied
|
|
11
|
+
* verbatim and CLOSED with synthetic `step/end` + `turn/end{reason:
|
|
12
|
+
* 'interrupted'}` events: the child sees the partial turn as honestly
|
|
13
|
+
* frozen ("cut off"), never as a completed answer. The one case that cannot
|
|
14
|
+
* be closed honestly — a tool call still executing (no `tool/result` yet;
|
|
15
|
+
* providers reject dangling assistant calls) — falls back to cutting before
|
|
16
|
+
* the open turn and carrying the partial content as a structured text
|
|
17
|
+
* snapshot inside the boundary prompt.
|
|
18
|
+
*/
|
|
19
|
+
import type { SidebarHistoryEntry, SidebarSessionSummary } from './context-types.ts'
|
|
20
|
+
|
|
21
|
+
/** The durable thread-label prefix (also the row filter in the client list). */
|
|
22
|
+
export const SIDE_LABEL_PREFIX = 'Side: '
|
|
23
|
+
|
|
24
|
+
/** The pinned label of a freshly created thread that no prompt has reached
|
|
25
|
+
* yet (Codex-style immediate create: the tab opens an EMPTY thread, the
|
|
26
|
+
* first composer message carries the boundary and earns the real label).
|
|
27
|
+
* The client renders it localized; the prefix keeps the row filter honest. */
|
|
28
|
+
export const SIDE_NEW_THREAD_TITLE = 'Side: New thread'
|
|
29
|
+
|
|
30
|
+
/** Maximum code points kept in a durable thread label (matches subagent labels). */
|
|
31
|
+
export const LABEL_MAX_CHARS = 48
|
|
32
|
+
|
|
33
|
+
/** The boundary message's opening line — the transcript mapping drops user
|
|
34
|
+
* rows starting with it (same first line as dsh-sidechain's boundary, so
|
|
35
|
+
* the two plugins' threads render consistently in either UI). */
|
|
36
|
+
export const SIDE_BOUNDARY_PREFIX = 'Side conversation boundary'
|
|
37
|
+
|
|
38
|
+
/** The plugin identity stamped on the source of context-injection messages
|
|
39
|
+
* (boundary prompt + parked snapshot), so the transcript recognizes them
|
|
40
|
+
* structurally — not by text prefix. */
|
|
41
|
+
export const SIDE_INJECTION_PLUGIN = 'dsh-better-sidebar'
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The boundary prompt delivered as the thread's first user message: the
|
|
45
|
+
* inherited seed is reference context only, never active instruction.
|
|
46
|
+
* Model-facing contract — change only with intent, tests pin the sentences.
|
|
47
|
+
*/
|
|
48
|
+
export const SIDE_BOUNDARY_PROMPT = `Side conversation boundary.
|
|
49
|
+
|
|
50
|
+
Everything before this boundary is inherited history from the parent session: its completed turns, its pending question, and — if the parent was mid-turn — its in-progress output frozen at the moment this side conversation started. It is reference context only. It is not your current task.
|
|
51
|
+
|
|
52
|
+
Do not continue, execute, or complete any instructions, plans, tool calls, approvals, edits, or requests from before this boundary. Only messages submitted after this boundary are active user instructions for this side conversation.
|
|
53
|
+
|
|
54
|
+
Mode: this is a continuable side conversation. Your answers stay in this side thread and are viewed in the side panel; they are never delivered into the parent session.`
|
|
55
|
+
|
|
56
|
+
/** One seed event (structural mirror of the durable SessionEvent). The
|
|
57
|
+
* envelope fields are preserved verbatim: surface-eligible events
|
|
58
|
+
* (user/message, assistant/message, tool/result) REQUIRE the `surfaceOp`
|
|
59
|
+
* marker (and may carry `sourceEventSeqs`) — the seed validator rejects
|
|
60
|
+
* them without it. */
|
|
61
|
+
export interface SeedEvent {
|
|
62
|
+
type: string
|
|
63
|
+
seq: number
|
|
64
|
+
time: number
|
|
65
|
+
data: Record<string, unknown>
|
|
66
|
+
/** Surface marker of message-producing events ('append' | replace op). */
|
|
67
|
+
surfaceOp?: unknown
|
|
68
|
+
/** Seq numbers of earlier events this event cites as sources. */
|
|
69
|
+
sourceEventSeqs?: unknown
|
|
70
|
+
/** Reader-skip marker of purely informational events. */
|
|
71
|
+
ignorable?: true
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The minimal structural face of a session-log event this module reads
|
|
75
|
+
* (loose enough to accept both the host's real SessionEvent and the
|
|
76
|
+
* client's SidebarSessionEvent mirror). */
|
|
77
|
+
export interface SidechatLogEvent {
|
|
78
|
+
type: string
|
|
79
|
+
seq: number
|
|
80
|
+
time: number
|
|
81
|
+
data: unknown
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The result of cutting a parent log into a side-thread inheritance. */
|
|
85
|
+
export interface SidechatInheritance {
|
|
86
|
+
/** The child seed: contiguous from seq 0, ends outside any open turn. */
|
|
87
|
+
seed: SeedEvent[]
|
|
88
|
+
/**
|
|
89
|
+
* Structured snapshot of the parent's in-progress turn when it could NOT
|
|
90
|
+
* be included as events (a tool call was still executing); null when the
|
|
91
|
+
* seed already carries the whole picture.
|
|
92
|
+
*/
|
|
93
|
+
snapshot: string | null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The data record of one event (narrowed from the loose face). */
|
|
97
|
+
function dataOf(event: SidechatLogEvent): Record<string, unknown> {
|
|
98
|
+
return event.data as Record<string, unknown>
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Copy parent events verbatim (their live seq === array index contract).
|
|
102
|
+
* The FULL envelope is preserved — stripping `surfaceOp` would make the
|
|
103
|
+
* seed validator reject every surface-eligible message event. */
|
|
104
|
+
function copyEvents(events: readonly SidechatLogEvent[]): SeedEvent[] {
|
|
105
|
+
return events.map(event => {
|
|
106
|
+
const source = event as SidechatLogEvent & {
|
|
107
|
+
surfaceOp?: unknown
|
|
108
|
+
sourceEventSeqs?: unknown
|
|
109
|
+
ignorable?: true
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
type: source.type,
|
|
113
|
+
seq: source.seq,
|
|
114
|
+
time: source.time,
|
|
115
|
+
data: dataOf(source),
|
|
116
|
+
...(source.surfaceOp === undefined ? {} : { surfaceOp: source.surfaceOp }),
|
|
117
|
+
...(source.sourceEventSeqs === undefined ? {} : { sourceEventSeqs: source.sourceEventSeqs }),
|
|
118
|
+
...(source.ignorable === undefined ? {} : { ignorable: source.ignorable }),
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Index of the last `turn/start` or `turn/end`, or -1. */
|
|
124
|
+
function lastTurnBoundary(events: readonly SidechatLogEvent[]): number {
|
|
125
|
+
for (let index = events.length - 1; index >= 0; index--) {
|
|
126
|
+
const type = events[index]?.type
|
|
127
|
+
if (type === 'turn/start' || type === 'turn/end') return index
|
|
128
|
+
}
|
|
129
|
+
return -1
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Numeric field of an event's data (turn / step numbers). */
|
|
133
|
+
function numberAt(data: Record<string, unknown>, key: string): number {
|
|
134
|
+
const value = data[key]
|
|
135
|
+
return typeof value === 'number' && Number.isSafeInteger(value) ? value : 0
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** The step number still open at the log tail inside the turn starting at
|
|
139
|
+
* `turnStart` (undefined when no step is open). */
|
|
140
|
+
function openStepInTurn(events: readonly SidechatLogEvent[], turnStart: number): number | undefined {
|
|
141
|
+
let open: number | undefined
|
|
142
|
+
for (let index = turnStart + 1; index < events.length; index++) {
|
|
143
|
+
const event = events[index]
|
|
144
|
+
if (event === undefined) continue
|
|
145
|
+
if (event.type === 'step/start') open = numberAt(dataOf(event), 'step')
|
|
146
|
+
else if (event.type === 'step/end') open = undefined
|
|
147
|
+
}
|
|
148
|
+
return open
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Whether the open turn ending the log has a `tool/call` without its paired
|
|
153
|
+
* `tool/result` in the CURRENT open step. Providers reject dangling
|
|
154
|
+
* assistant calls, so such a turn cannot be honestly closed and the
|
|
155
|
+
* inheritance must fall back to the snapshot.
|
|
156
|
+
*/
|
|
157
|
+
export function hasDanglingToolCall(events: readonly SidechatLogEvent[], turnStart: number): boolean {
|
|
158
|
+
const pending = new Set<string>()
|
|
159
|
+
for (let index = turnStart + 1; index < events.length; index++) {
|
|
160
|
+
const event = events[index]
|
|
161
|
+
if (event === undefined) continue
|
|
162
|
+
const data = dataOf(event)
|
|
163
|
+
if (event.type === 'step/end') {
|
|
164
|
+
pending.clear()
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
if (event.type === 'tool/call') {
|
|
168
|
+
const callId = data.callId
|
|
169
|
+
if (typeof callId === 'string') pending.add(callId)
|
|
170
|
+
continue
|
|
171
|
+
}
|
|
172
|
+
if (event.type === 'tool/result') {
|
|
173
|
+
const source = data.message as { source?: { callId?: unknown } } | undefined
|
|
174
|
+
const callId = source?.source?.callId
|
|
175
|
+
if (typeof callId === 'string') pending.delete(callId)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return pending.size > 0
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** The plain text of one tool/result message (text blocks inside its
|
|
182
|
+
* `tool-result` content block). */
|
|
183
|
+
function toolResultText(data: Record<string, unknown>): string {
|
|
184
|
+
const message = data.message as { content?: unknown } | undefined
|
|
185
|
+
const content = message?.content
|
|
186
|
+
if (!Array.isArray(content)) return ''
|
|
187
|
+
const parts: string[] = []
|
|
188
|
+
for (const block of content) {
|
|
189
|
+
if (block === null || typeof block !== 'object') continue
|
|
190
|
+
const candidate = block as { type?: unknown; content?: unknown }
|
|
191
|
+
if (candidate.type !== 'tool-result') continue
|
|
192
|
+
const inner = candidate.content
|
|
193
|
+
if (!Array.isArray(inner)) continue
|
|
194
|
+
for (const item of inner) {
|
|
195
|
+
if (item === null || typeof item !== 'object') continue
|
|
196
|
+
const textItem = item as { type?: unknown; text?: unknown }
|
|
197
|
+
if (textItem.type === 'text' && typeof textItem.text === 'string') {
|
|
198
|
+
parts.push(textItem.text)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return parts.join('\n')
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Cap applied to one tool-result's text inside a snapshot (prompt budget). */
|
|
206
|
+
const SNAPSHOT_RESULT_CAP = 2000
|
|
207
|
+
/** Cap applied to the whole snapshot (prompt budget). */
|
|
208
|
+
const SNAPSHOT_TOTAL_CAP = 8000
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Build the side-thread inheritance for one parent log: the full event log
|
|
212
|
+
* up to the click moment, honestly closed when it ends inside an open turn.
|
|
213
|
+
*/
|
|
214
|
+
export function buildSidechatInheritance(events: readonly SidechatLogEvent[]): SidechatInheritance {
|
|
215
|
+
if (events.length === 0) return { seed: [], snapshot: null }
|
|
216
|
+
const boundary = lastTurnBoundary(events)
|
|
217
|
+
if (boundary < 0 || events[boundary]?.type === 'turn/end') {
|
|
218
|
+
// Ends outside any turn (or has no turns at all): the whole log is a
|
|
219
|
+
// valid, balanced seed — possibly ending with a pending user message.
|
|
220
|
+
return { seed: copyEvents(events), snapshot: null }
|
|
221
|
+
}
|
|
222
|
+
// Ends inside the open turn starting at `boundary`.
|
|
223
|
+
if (hasDanglingToolCall(events, boundary)) {
|
|
224
|
+
// Cannot close honestly: cut before the open turn; the caller attaches
|
|
225
|
+
// the structured snapshot to the boundary prompt instead.
|
|
226
|
+
return {
|
|
227
|
+
seed: copyEvents(events.slice(0, boundary)),
|
|
228
|
+
snapshot: buildOpenTurnSnapshot(events),
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const seed = copyEvents(events)
|
|
232
|
+
const last = events[events.length - 1]
|
|
233
|
+
const turn = numberAt(dataOf(events[boundary]!), 'turn')
|
|
234
|
+
const now = last?.time ?? 0
|
|
235
|
+
const openStep = openStepInTurn(events, boundary)
|
|
236
|
+
if (openStep !== undefined) {
|
|
237
|
+
seed.push({ type: 'step/end', seq: seed.length, time: now, data: { turn, step: openStep } })
|
|
238
|
+
}
|
|
239
|
+
seed.push({
|
|
240
|
+
type: 'turn/end',
|
|
241
|
+
seq: seed.length,
|
|
242
|
+
time: now,
|
|
243
|
+
data: { turn, reason: { kind: 'interrupted' } },
|
|
244
|
+
})
|
|
245
|
+
return { seed, snapshot: null }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** The seed half of {@link buildSidechatInheritance} (test convenience). */
|
|
249
|
+
export function sidechatSeed(events: readonly SidechatLogEvent[]): SeedEvent[] {
|
|
250
|
+
return buildSidechatInheritance(events).seed
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Structured text snapshot of the parent's OPEN turn (from its `turn/start`
|
|
255
|
+
* to the log tail): the accumulated assistant/reasoning output verbatim
|
|
256
|
+
* (code blocks ride the raw deltas) and the tool activity — executed tools
|
|
257
|
+
* with their result text, the still-executing one marked. Returns null when
|
|
258
|
+
* there is no open turn or nothing to show.
|
|
259
|
+
*/
|
|
260
|
+
export function buildOpenTurnSnapshot(events: readonly SidechatLogEvent[]): string | null {
|
|
261
|
+
const boundary = lastTurnBoundary(events)
|
|
262
|
+
if (boundary < 0 || events[boundary]?.type !== 'turn/start') return null
|
|
263
|
+
let text = ''
|
|
264
|
+
let reasoning = ''
|
|
265
|
+
const tools: string[] = []
|
|
266
|
+
const pendingCalls = new Map<string, { name: string; args: string }>()
|
|
267
|
+
let total = 0
|
|
268
|
+
for (let index = boundary + 1; index < events.length; index++) {
|
|
269
|
+
const event = events[index]
|
|
270
|
+
if (event === undefined) continue
|
|
271
|
+
const data = dataOf(event)
|
|
272
|
+
if (event.type === 'step/end') {
|
|
273
|
+
pendingCalls.clear()
|
|
274
|
+
continue
|
|
275
|
+
}
|
|
276
|
+
if (event.type === 'assistant/chunk') {
|
|
277
|
+
const chunk = data.chunk as { type?: unknown; text?: unknown } | undefined
|
|
278
|
+
if (chunk === null || typeof chunk !== 'object') continue
|
|
279
|
+
if (chunk.type === 'text-delta' && typeof chunk.text === 'string') text += chunk.text
|
|
280
|
+
else if (chunk.type === 'reasoning-delta' && typeof chunk.text === 'string') reasoning += chunk.text
|
|
281
|
+
continue
|
|
282
|
+
}
|
|
283
|
+
if (event.type === 'tool/call') {
|
|
284
|
+
const callId = data.callId
|
|
285
|
+
if (typeof callId === 'string') {
|
|
286
|
+
pendingCalls.set(callId, {
|
|
287
|
+
name: typeof data.name === 'string' ? data.name : 'tool',
|
|
288
|
+
args: typeof data.arguments === 'string' ? data.arguments : '',
|
|
289
|
+
})
|
|
290
|
+
}
|
|
291
|
+
continue
|
|
292
|
+
}
|
|
293
|
+
if (event.type === 'tool/result') {
|
|
294
|
+
const source = data.message as { source?: { callId?: unknown } } | undefined
|
|
295
|
+
const callId = typeof source?.source?.callId === 'string' ? source.source.callId : undefined
|
|
296
|
+
const name = callId !== undefined ? pendingCalls.get(callId)?.name : undefined
|
|
297
|
+
const args = callId !== undefined ? pendingCalls.get(callId)?.args : undefined
|
|
298
|
+
if (callId !== undefined) pendingCalls.delete(callId)
|
|
299
|
+
const result = toolResultText(data).slice(0, SNAPSHOT_RESULT_CAP)
|
|
300
|
+
const failed = data.error !== undefined
|
|
301
|
+
const line = [
|
|
302
|
+
`- \`${name ?? 'tool'}\`${failed ? ' (failed)' : ''}`
|
|
303
|
+
+ (args !== undefined && args !== '' ? ` — arguments: \`${args}\`` : ''),
|
|
304
|
+
...(result === '' ? [] : [` Result: ${result}`]),
|
|
305
|
+
].join('\n')
|
|
306
|
+
tools.push(line)
|
|
307
|
+
total += line.length
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
for (const [, call] of pendingCalls) {
|
|
311
|
+
const line = `- \`${call.name}\` (executing) — arguments: \`${call.args}\``
|
|
312
|
+
tools.push(line)
|
|
313
|
+
total += line.length
|
|
314
|
+
}
|
|
315
|
+
const sections: string[] = []
|
|
316
|
+
if (text.trim() !== '') sections.push(`Assistant output so far:\n\n${text}`)
|
|
317
|
+
if (reasoning.trim() !== '') sections.push(`Reasoning so far:\n\n${reasoning}`)
|
|
318
|
+
if (tools.length > 0) sections.push(`Tool activity:\n${tools.join('\n')}`)
|
|
319
|
+
if (sections.length === 0) return null
|
|
320
|
+
const body = sections.join('\n\n')
|
|
321
|
+
return body.length > SNAPSHOT_TOTAL_CAP
|
|
322
|
+
? `Parent session in-progress turn (reference only):\n\n${body.slice(0, SNAPSHOT_TOTAL_CAP)}…`
|
|
323
|
+
: `Parent session in-progress turn (reference only):\n\n${body}`
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** One side-thread row in the client's thread list. */
|
|
327
|
+
export interface SideThreadRow {
|
|
328
|
+
id: string
|
|
329
|
+
/** The durable thread title ('Side: …'). */
|
|
330
|
+
title: string
|
|
331
|
+
/** Whether the thread's agent is currently running. */
|
|
332
|
+
running: boolean
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Derive the side threads of one parent session from the client session list:
|
|
337
|
+
* durable `origin: 'subagent'` children of the parent whose pinned title
|
|
338
|
+
* carries the thread label prefix (our creation path pins it via
|
|
339
|
+
* sessionTitle.rename; dsh-sidechain threads share the convention, so they
|
|
340
|
+
* are visible here too).
|
|
341
|
+
*/
|
|
342
|
+
export function sideThreadRows(
|
|
343
|
+
byId: Readonly<Record<string, SidebarSessionSummary>>,
|
|
344
|
+
sessionId: string,
|
|
345
|
+
): SideThreadRow[] {
|
|
346
|
+
const rows: SideThreadRow[] = []
|
|
347
|
+
for (const summary of Object.values(byId)) {
|
|
348
|
+
if (summary.origin !== 'subagent' || summary.parentId !== sessionId) continue
|
|
349
|
+
if (!summary.displayTitle.startsWith(SIDE_LABEL_PREFIX)) continue
|
|
350
|
+
rows.push({ id: summary.id, title: summary.displayTitle, running: summary.running === true })
|
|
351
|
+
}
|
|
352
|
+
return rows
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Truncate + prefix a question into a durable thread label. */
|
|
356
|
+
export function sideLabel(question: string): string {
|
|
357
|
+
const flat = question.replace(/\s+/g, ' ').trim()
|
|
358
|
+
const max = Math.max(1, LABEL_MAX_CHARS - SIDE_LABEL_PREFIX.length)
|
|
359
|
+
const body = flat.length > max ? `${flat.slice(0, max - 1)}…` : flat
|
|
360
|
+
return `${SIDE_LABEL_PREFIX}${body}`
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Whether the thread log already carries the side boundary message — i.e.
|
|
365
|
+
* the first prompt was delivered. Tolerant to the content shape (block
|
|
366
|
+
* array or bare string) and to inherited seed messages (only an OWN
|
|
367
|
+
* boundary message starts with the prefix; seed messages came from the
|
|
368
|
+
* parent's log, which never contains one).
|
|
369
|
+
*/
|
|
370
|
+
export function boundaryDelivered(events: readonly SidechatLogEvent[]): boolean {
|
|
371
|
+
for (const event of events) {
|
|
372
|
+
if (event.type !== 'user/message') continue
|
|
373
|
+
if (messageLeadText(dataOf(event)).startsWith(SIDE_BOUNDARY_PREFIX)) return true
|
|
374
|
+
}
|
|
375
|
+
return false
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** The leading text of a user/message's content (block array or bare string). */
|
|
379
|
+
function messageLeadText(data: Record<string, unknown>): string {
|
|
380
|
+
const content = data.content
|
|
381
|
+
const first = Array.isArray(content) ? content[0] : content
|
|
382
|
+
return typeof first === 'string'
|
|
383
|
+
? first
|
|
384
|
+
: (typeof first === 'object' && first !== null && 'text' in first
|
|
385
|
+
? String((first as { text: unknown }).text)
|
|
386
|
+
: '')
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Whether a logged user/message is a CONTEXT INJECTION (the boundary prompt
|
|
391
|
+
* plus the parked in-progress snapshot) rather than a real user message.
|
|
392
|
+
* New threads deliver the injection via `agent.inject` stamped with a
|
|
393
|
+
* non-'user' source kind; threads created before that split carry
|
|
394
|
+
* boundary+question in ONE 'user' message, recognized by the boundary
|
|
395
|
+
* prefix. Both render as one collapsible injection row — never as a user
|
|
396
|
+
* bubble.
|
|
397
|
+
*/
|
|
398
|
+
export function isContextInjectionMessage(data: Record<string, unknown>): boolean {
|
|
399
|
+
const source = data.source as { kind?: unknown } | null | undefined
|
|
400
|
+
if (source?.kind !== undefined && source.kind !== 'user') return true
|
|
401
|
+
return messageLeadText(data).startsWith(SIDE_BOUNDARY_PREFIX)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** The info the thread header shows (live runtime state + agent identity). */
|
|
405
|
+
export interface SidechatThreadInfo {
|
|
406
|
+
/** A live agent drives the thread right now (false = cold/persisted). */
|
|
407
|
+
live: boolean
|
|
408
|
+
/** Live lifecycle state; absent on cold threads. */
|
|
409
|
+
status?: 'idle' | 'running'
|
|
410
|
+
/** Provider route of the live agent. */
|
|
411
|
+
provider?: string
|
|
412
|
+
/** Model id of the live agent. */
|
|
413
|
+
model?: string
|
|
414
|
+
/** The recorded agent preset (live header, or persisted on cold reads). */
|
|
415
|
+
preset?: string
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** The events a thread produced itself: everything after the LAST
|
|
419
|
+
* `session/end-seed` marker (the fork-seed boundary). */
|
|
420
|
+
export function threadOwnEvents(entries: readonly SidebarHistoryEntry[]): SidechatLogEvent[] {
|
|
421
|
+
const events = entries.map(entry => entry.event)
|
|
422
|
+
for (let index = events.length - 1; index >= 0; index--) {
|
|
423
|
+
if (events[index]?.type === 'session/end-seed') return events.slice(index + 1)
|
|
424
|
+
}
|
|
425
|
+
return events
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Whether the thread has at least one completed turn — the save-as-new-
|
|
430
|
+
* session precondition (`session.fork` refuses to fork before the first
|
|
431
|
+
* `turn/end`).
|
|
432
|
+
*/
|
|
433
|
+
export function threadHasCompletedTurn(entries: readonly SidebarHistoryEntry[]): boolean {
|
|
434
|
+
return threadOwnEvents(entries).some(event => event.type === 'turn/end')
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** Whether the thread ends with a user message that no completed turn
|
|
438
|
+
* answered yet — such a pending follow-up is NOT carried into the saved
|
|
439
|
+
* session (the fork cut is the last `turn/end`). */
|
|
440
|
+
export function threadTrailingPending(entries: readonly SidebarHistoryEntry[]): boolean {
|
|
441
|
+
const own = threadOwnEvents(entries)
|
|
442
|
+
let lastUser = -1
|
|
443
|
+
let lastTurnEnd = -1
|
|
444
|
+
own.forEach((event, index) => {
|
|
445
|
+
if (event.type === 'user/message') lastUser = index
|
|
446
|
+
if (event.type === 'turn/end') lastTurnEnd = index
|
|
447
|
+
})
|
|
448
|
+
return lastUser > lastTurnEnd
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* The agent preset a session actually runs: newest `agent-preset/selected`
|
|
453
|
+
* event wins, else the creation header (mirror of the dsh-agent-presets
|
|
454
|
+
* resolveSessionPreset helper — replicated here to avoid a host dependency
|
|
455
|
+
* on that package).
|
|
456
|
+
*/
|
|
457
|
+
export function resolvePresetId(
|
|
458
|
+
header: { agentPreset?: string },
|
|
459
|
+
events: readonly SidechatLogEvent[],
|
|
460
|
+
): string | undefined {
|
|
461
|
+
for (let index = events.length - 1; index >= 0; index--) {
|
|
462
|
+
const event = events[index]
|
|
463
|
+
if (event?.type !== 'agent-preset/selected') continue
|
|
464
|
+
const preset = dataOf(event).agentPreset
|
|
465
|
+
if (typeof preset === 'string') return preset
|
|
466
|
+
}
|
|
467
|
+
return header.agentPreset
|
|
468
|
+
}
|