dsh-session-pages 0.5.6 → 0.5.7

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
@@ -4,6 +4,21 @@ DeepSeek Harness 的第四个对话视图标签:**窗口**(在「对话 /
4
4
 
5
5
  会话按**压缩(compaction)分界**切成若干窗口:第一窗从会话开头起算,之后每次压缩的 checkpoint 开头即新一窗。
6
6
 
7
+ ## 设置前缀不算一窗
8
+
9
+ 被 `dsh-log-trim` 裁剪过的日志开头是一段纯设置前缀(系统提示词、预设、权限等),没有对话。若按 checkpoint 切分,它会被当成「第 1 窗」,于是「保留 2 窗」显示成 3 窗。
10
+
11
+ 规则:
12
+
13
+ | 情况 | 结果 |
14
+ | --- | --- |
15
+ | 首窗有 checkpoint | 是真正的对话窗,保留 |
16
+ | 首窗无 checkpoint,但含 user/assistant 消息 | 裁剪没清掉它,保留 |
17
+ | 首窗无 checkpoint,且只有设置事件 | **并入**后面第一个带 checkpoint 的窗,不计入窗数 |
18
+ | 全程没有 checkpoint | 仍然是 1 窗 |
19
+
20
+ 并入后,那个窗自己持有系统提示词与工具声明(`inherited: false`),不再显示「沿用上一窗」。
21
+
7
22
  ## 每一窗画两层
8
23
 
9
24
  一个窗口**就是**两层,所以视图也分两层画:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-session-pages",
3
- "version": "0.5.6",
3
+ "version": "0.5.7",
4
4
  "description": "Fourth conversation view for DeepSeek Harness: the session split into windows at every compaction, each window headed by its checkpoint summary.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/client.js CHANGED
@@ -22,7 +22,7 @@ window.__ModuleLoader__.load({
22
22
  const react = require('react')
23
23
 
24
24
  const NS = 'dsh-session-pages'
25
- const VERSION = '0.5.6'
25
+ const VERSION = '0.5.7'
26
26
  const ROUTE_PREFIX = '/dsh-session-pages'
27
27
  const VIEW_ID = 'windows'
28
28
  const VIEW_ORDER = 30
package/src/index.js CHANGED
@@ -129,12 +129,21 @@ function timeAtStart(page, events) {
129
129
  }
130
130
 
131
131
  function stateFromProjection(projection, events, options) {
132
- const pages = projection.pages.map((page) => {
132
+ // A trimmed log opens with a settings-only prefix; it is not a window. Same
133
+ // rule as the fold: keep it only when it actually holds dialogue.
134
+ const first = projection.pages.length > 1 ? projection.pages[0] : undefined
135
+ const folded = first !== undefined && first.compactionId === null && (first.dialogue || 0) === 0
136
+ const source = folded
137
+ ? projection.pages.slice(1).map((page, index) => ({ ...page, index: index + 1 }))
138
+ : projection.pages
139
+ const pages = source.map((page, position) => {
133
140
  const context = {}
134
141
  if (page.systemChars > 0) {
135
142
  context.system = {
136
143
  present: true,
137
- inherited: page.systemInherited === true,
144
+ // The window that now opens the list owns the frame, even though the
145
+ // events carrying it sat in the dropped prefix.
146
+ inherited: folded === true && position === 0 ? false : page.systemInherited === true,
138
147
  chars: page.systemChars,
139
148
  preview: clipText(page.systemText, options.clipChars).text,
140
149
  full: page.systemText,
package/src/logic.js CHANGED
@@ -23,7 +23,7 @@
23
23
  // * it replaces the older range and is FOLLOWED by the retained recent units.
24
24
 
25
25
  export const PLUGIN_ID = 'dsh-session-pages'
26
- export const VERSION = '0.5.6'
26
+ export const VERSION = '0.5.7'
27
27
  export const ROUTE_PREFIX = '/dsh-session-pages'
28
28
  export const VIEW_ID = 'windows'
29
29
  export const VIEW_ORDER = 30
@@ -133,6 +133,42 @@ export function isCheckpoint(event) {
133
133
  return source !== null && source !== undefined && source.kind === CHECKPOINT_KIND
134
134
  }
135
135
 
136
+
137
+ /** True when the window holds at least one visible dialogue message. */
138
+ function windowHasDialogue(sorted, page) {
139
+ const from = page.startSeq
140
+ const to = page.endSeq === null ? Number.POSITIVE_INFINITY : page.endSeq
141
+ for (const event of sorted) {
142
+ const seq = Number(event.seq) || 0
143
+ if (seq < from) continue
144
+ if (seq > to) break
145
+ if (event.type === 'assistant/message') return true
146
+ if (event.type === 'user/message' && isHumanUserMessage(event)) return true
147
+ }
148
+ return false
149
+ }
150
+
151
+ /**
152
+ * Drop a leading window that holds nothing but settings.
153
+ *
154
+ * A trimmed log starts with a prefix of system/settings events and no dialogue.
155
+ * Splitting windows at checkpoints would count that prefix as "window 1", so
156
+ * "2 windows kept" reads as 3. The rule:
157
+ *
158
+ * - fewer than two windows -> keep as is (a fresh session is 1 window);
159
+ * - first window has a checkpoint -> it IS a conversation window, keep it;
160
+ * - prefix holds visible dialogue -> the trim did not clear it, keep it;
161
+ * - otherwise -> drop it, renumber, and let the first
162
+ * checkpoint window carry the frame.
163
+ */
164
+ export function collapseLeadingSettingsPrefix(sorted, pages) {
165
+ if (pages.length < 2) return pages
166
+ const first = pages[0]
167
+ if (first.compactionId !== null) return pages
168
+ if (windowHasDialogue(sorted, first) === true) return pages
169
+ return pages.slice(1).map((page, index) => ({ ...page, index: index + 1 }))
170
+ }
171
+
136
172
  export function isHumanUserMessage(event) {
137
173
  if (event === null || typeof event !== 'object' || event.type !== 'user/message' || isCheckpoint(event)) return false
138
174
  const source = event.data === null || event.data === undefined ? undefined : event.data.source
@@ -401,10 +437,29 @@ export function buildPages(events, options = {}) {
401
437
  }
402
438
  }
403
439
 
440
+ // 2b. A settings-only prefix is not a window of its own.
441
+ const withoutPrefix = collapseLeadingSettingsPrefix(sorted, pages)
442
+ let floorSeq = 0
443
+ if (withoutPrefix !== pages) {
444
+ pages.length = 0
445
+ for (const page of withoutPrefix) pages.push(page)
446
+ // The prefix's system prompt and tool roster now belong to this window, so
447
+ // it no longer inherits them from a window that is not shown.
448
+ if (pages[0].context.system !== null && pages[0].context.system.present === true) {
449
+ pages[0].context.system.inherited = false
450
+ }
451
+ if (pages[0].context.tools !== null && pages[0].context.tools.count > 0) {
452
+ pages[0].context.tools.inherited = false
453
+ }
454
+ floorSeq = pages[0].startSeq
455
+ }
456
+
404
457
  // 3. The window's own timeline.
405
458
  let dropped = 0
406
459
  for (const event of sorted) {
407
460
  const seq = Number(event.seq) || 0
461
+ // Events before the first shown window belong to the dropped prefix.
462
+ if (seq < floorSeq) continue
408
463
  const page = pageForSeq(seq)
409
464
  const time = Number(event.time)
410
465
  if (Number.isFinite(time)) {
package/src/projection.js CHANGED
@@ -27,6 +27,9 @@ const FIRST_PAGE = () => ({
27
27
  systemText: '',
28
28
  systemInherited: false,
29
29
  toolNames: null,
30
+ // Visible dialogue rows (~user/assistant). Settings-only prefixes stay 0 and
31
+ // are folded into the first checkpoint window by the reader.
32
+ dialogue: 0,
30
33
  turnFrom: null,
31
34
  turnTo: null,
32
35
  steps: 0,
@@ -117,8 +120,10 @@ export const windowProjection = {
117
120
  // rows are rows, not messages, except the checkpoint above.
118
121
  // The fold draws a row only when there is text to draw; a message that
119
122
  // carries nothing but an attachment still counts as a message, not a row.
120
- const rendered = textOfMessage(messageOf(event)).length > 0 ? 1 : 0
123
+ const text = textOfMessage(messageOf(event))
124
+ const rendered = text.length > 0 ? 1 : 0
121
125
  const page = addToPage(last, {
126
+ dialogue: last.dialogue + (rendered > 0 && (kind === 'user' || kind === undefined) ? 1 : 0),
122
127
  itemCount: last.itemCount + rendered,
123
128
  messages: last.messages + 1,
124
129
  })
@@ -138,6 +143,7 @@ export const windowProjection = {
138
143
  const text = textOfMessage(message)
139
144
  const page = {
140
145
  ...last,
146
+ dialogue: last.dialogue + 1,
141
147
  steps: last.steps + 1,
142
148
  messages: last.messages + 1,
143
149
  toolCalls: last.toolCalls + toolCalls,
@@ -38,7 +38,7 @@ function loadClient() {
38
38
  test('the bundle registers the windows view beside chat and trajectory', async () => {
39
39
  const exports = await loadClient()
40
40
  assert.equal(typeof exports.apply, 'function')
41
- assert.equal(exports.version, '0.5.6')
41
+ assert.equal(exports.version, '0.5.7')
42
42
 
43
43
  const registered = []
44
44
  const slots = {
@@ -209,3 +209,75 @@ test('session ids are validated', () => {
209
209
  assert.equal(isSessionId('1234abcd'), false)
210
210
  assert.equal(isSessionId('session-'), false)
211
211
  })
212
+
213
+ // --- a trimmed log must not count its settings prefix as a window -----------
214
+
215
+ test('a settings-only prefix is folded into the first checkpoint window', () => {
216
+ // Trimmed shape: settings + two checkpoints, no dialogue before the first one.
217
+ const events = [
218
+ at(1, 1001, 'permission/preset', { preset: 'standard' }),
219
+ at(2, 1002, 'sandbox/mode', { mode: 'workspace-write' }),
220
+ injection(3, 'runtime-context', 'Current runtime context.'),
221
+ checkpoint(10, 'cmp-1', 'first summary', { op: 'replace', startSeq: 4, endSeq: 9 }),
222
+ at(11, 1011, 'system/message', { role: 'system', content: [{ type: 'text', text: 'You are an agent.' }] }),
223
+ human(12, 1, 'hello'),
224
+ at(20, 1020, 'assistant/message', {
225
+ role: 'assistant', turn: 1, step: 1,
226
+ content: [{ type: 'text', text: 'hi' }],
227
+ usage: { totalTokens: 100 },
228
+ }),
229
+ checkpoint(30, 'cmp-2', 'second summary', { op: 'replace', startSeq: 10, endSeq: 29 }),
230
+ human(31, 2, 'again'),
231
+ at(40, 1040, 'assistant/message', {
232
+ role: 'assistant', turn: 2, step: 1,
233
+ content: [{ type: 'text', text: 'yes' }],
234
+ usage: { totalTokens: 120 },
235
+ }),
236
+ ]
237
+ const result = buildPages(events)
238
+ assert.equal(result.pages.length, 2)
239
+ assert.equal(result.totals.pages, 2)
240
+ // The window that opens the list carries the frame itself.
241
+ assert.equal(result.pages[0].context.system.present, true)
242
+ assert.equal(result.pages[0].context.system.inherited, false)
243
+ assert.equal(result.pages[0].index, 1)
244
+ assert.equal(result.pages[1].index, 2)
245
+ })
246
+
247
+ test('a prefix that still holds dialogue stays a window of its own', () => {
248
+ const events = [
249
+ injection(1, 'runtime-context', 'Current runtime context.'),
250
+ human(2, 1, 'still here'),
251
+ at(3, 1003, 'assistant/message', {
252
+ role: 'assistant', turn: 1, step: 1,
253
+ content: [{ type: 'text', text: 'ok' }],
254
+ usage: { totalTokens: 50 },
255
+ }),
256
+ checkpoint(10, 'cmp-1', 'summary', { op: 'replace', startSeq: 4, endSeq: 9 }),
257
+ human(11, 2, 'after'),
258
+ at(12, 1012, 'assistant/message', {
259
+ role: 'assistant', turn: 2, step: 1,
260
+ content: [{ type: 'text', text: 'yep' }],
261
+ usage: { totalTokens: 60 },
262
+ }),
263
+ ]
264
+ const result = buildPages(events)
265
+ assert.equal(result.pages.length, 2)
266
+ assert.equal(result.pages[0].compactionId, null)
267
+ })
268
+
269
+ test('a session with no checkpoint is still one window', () => {
270
+ const events = [
271
+ at(1, 1001, 'permission/preset', { preset: 'standard' }),
272
+ injection(2, 'runtime-context', 'Current runtime context.'),
273
+ human(3, 1, 'first'),
274
+ at(4, 1004, 'assistant/message', {
275
+ role: 'assistant', turn: 1, step: 1,
276
+ content: [{ type: 'text', text: 'reply' }],
277
+ usage: { totalTokens: 10 },
278
+ }),
279
+ ]
280
+ const result = buildPages(events)
281
+ assert.equal(result.pages.length, 1)
282
+ assert.equal(result.totals.pages, 1)
283
+ })