dsh-code-index 0.9.0 → 0.10.0

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.
@@ -0,0 +1,43 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026-present richerfu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ The MIT License (MIT)
24
+
25
+ Copyright (c) 2017 Max Brunsfeld
26
+
27
+ Permission is hereby granted, free of charge, to any person obtaining a copy
28
+ of this software and associated documentation files (the "Software"), to deal
29
+ in the Software without restriction, including without limitation the rights
30
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
31
+ copies of the Software, and to permit persons to whom the Software is
32
+ furnished to do so, subject to the following conditions:
33
+
34
+ The above copyright notice and this permission notice shall be included in all
35
+ copies or substantial portions of the Software.
36
+
37
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
38
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
39
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
40
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
41
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
42
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
43
+ SOFTWARE.
@@ -0,0 +1,25 @@
1
+ # ArkTS grammar asset
2
+
3
+ `tree-sitter-arkts.wasm` and `ARKTS-LICENSE` are copied unchanged from the
4
+ published **harmony-contrib/tree-sitter-arkts 0.3.0** npm archive, not compiled
5
+ from an unpinned branch. MIT notices for richerfu and Max Brunsfeld are retained.
6
+
7
+ - Source: https://github.com/harmony-contrib/tree-sitter-arkts
8
+ - npm: https://registry.npmjs.org/tree-sitter-arkts/-/tree-sitter-arkts-0.3.0.tgz
9
+ - Registry `gitHead`: `9a97c1e6ac93bc07732c15050a0e560bc63c8d78`
10
+ - Archive SHA1: `a02e2d883821af6036e1f9efc80f3f1e65227652`
11
+ - Archive integrity: `sha512-3mY013x1rWm8LLeuPp+kFLrl4vbQCfTEferO3GUwUhKHbQHzIT2ppjGARI3vNXIsuvQy/J8xJt2cIOlpfphlXQ==`
12
+ - WASM SHA256: `a5fbf24b2eeab43cf60e9fcdbde99e9089f7ceae43134d24e1d5cc166d285446`
13
+ - Size: 2,430,659 bytes; grammar ABI: 14.
14
+
15
+ Upstream publication regenerates with Tree-sitter CLI 0.24.4 / ABI 14 and builds
16
+ WASM with `tree-sitter build --wasm`. The upstream pipeline permits WASM build
17
+ failure; this specific archive was checked to contain the binary above. The
18
+ plugin vendors only this binary and license, and never imports upstream Node
19
+ bindings or runs `node-gyp-build`. No native compilation or network fetch occurs
20
+ when installing/loading the plugin. The package `files` list includes this directory.
21
+
22
+ To recover the exact asset, download the pinned archive, verify its integrity,
23
+ extract `package/tree-sitter-arkts.wasm` and `package/LICENSE`, then verify the
24
+ binary hash. `tests/arkts.spec.ts` checks the hash and actual runtime parsing;
25
+ `release:smoke` checks installed archive contents and `.ets` extraction.
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-code-index",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Edit-ready Context Packs for DSH agents: bounded source excerpts, changes, related tests, and inclusion evidence — local-first.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -16,6 +16,7 @@
16
16
  "./package.json": "./package.json"
17
17
  },
18
18
  "files": [
19
+ "grammars",
19
20
  "dist",
20
21
  "scripts",
21
22
  "cordis.patch.yml",
@@ -0,0 +1,88 @@
1
+ """Cut actual DSH screen footage; never generate or replace UI content.
2
+
3
+ Usage: python scripts/process-desktop-demo.py FFMPEG RAW_MP4 EDIT_JSON
4
+ EDIT_JSON: {"segments": [{"start": 0, "duration": 5}, ...]}
5
+ Segments must be chronological and within the original recording.
6
+ """
7
+ import hashlib
8
+ import json
9
+ from pathlib import Path
10
+ import subprocess
11
+ import sys
12
+
13
+
14
+ def sha256(path):
15
+ return hashlib.file_digest(path.open("rb"), "sha256").hexdigest()
16
+
17
+
18
+ def main():
19
+ ffmpeg, raw, edit_path = sys.argv[1:]
20
+ raw, edit_path = Path(raw), Path(edit_path)
21
+ edit = json.loads(edit_path.read_text(encoding="utf-8"))
22
+ segments = edit["segments"]
23
+ duration = sum(s["duration"] for s in segments)
24
+ assert 30 <= duration <= 60, duration
25
+ end = 0
26
+ for s in segments:
27
+ assert s["start"] >= end and s["duration"] > 0
28
+ end = s["start"] + s["duration"]
29
+ root = Path(__file__).resolve().parents[1]
30
+ assets = root / "assets"
31
+ video = assets / "desktop-agent-v090.mp4"
32
+ gif = assets / "desktop-agent-v090.gif"
33
+ poster = assets / "desktop-agent-v090.png"
34
+ filters = []
35
+ for i, s in enumerate(segments):
36
+ filters.append(
37
+ f"[0:v]trim=start={s['start']}:duration={s['duration']},"
38
+ f"setpts=PTS-STARTPTS[v{i}]"
39
+ )
40
+ inputs = "".join(f"[v{i}]" for i in range(len(segments)))
41
+ filters.append(
42
+ f"{inputs}concat=n={len(segments)}:v=1:a=0,"
43
+ "scale=1280:-2:flags=lanczos,setsar=1,fps=15[out]"
44
+ )
45
+ subprocess.run([
46
+ ffmpeg, "-hide_banner", "-loglevel", "error", "-y", "-i", str(raw), "-filter_complex", ";".join(filters),
47
+ "-map", "[out]", "-an", "-c:v", "libx264", "-crf", "20",
48
+ "-preset", "medium", "-pix_fmt", "yuv420p", "-movflags", "+faststart",
49
+ "-map_metadata", "-1", str(video),
50
+ ], check=True)
51
+ subprocess.run([
52
+ ffmpeg, "-hide_banner", "-loglevel", "error", "-y", "-i", str(video), "-vf",
53
+ "fps=2,split[a][b];[a]palettegen=stats_mode=diff[p];"
54
+ "[b][p]paletteuse=dither=bayer:bayer_scale=5", "-loop", "0", str(gif),
55
+ ], check=True)
56
+ subprocess.run([
57
+ ffmpeg, "-hide_banner", "-loglevel", "error", "-y", "-ss", str(edit.get("posterAt", 12)), "-i", str(video),
58
+ "-frames:v", "1", "-update", "1", str(poster),
59
+ ], check=True)
60
+ manifest = {
61
+ "method": "Actual DSH Desktop screen recording; Agent task submitted through UI",
62
+ "recordedDate": "2026-10-08",
63
+ "pluginVersion": "0.9.0 (visible installed/running in Desktop UI)",
64
+ "rawSha256": sha256(raw),
65
+ "durationSeconds": duration,
66
+ "segments": segments,
67
+ "posterAt": edit.get("posterAt", 12),
68
+ "edits": [
69
+ "Chronological cuts remove setup, waiting and redundant holds",
70
+ "No UI, tool output or Agent response generated or replaced",
71
+ "Silent video resized to 1280 pixels wide, 15 fps; GIF sampled at 2 fps",
72
+ "Elapsed wall time is not represented by edited playback duration",
73
+ ],
74
+ "captureOperation": "User started/stopped Windows Snipping Tool; assistant operated DSH",
75
+ "toolResult": {"usedChars": 2347, "budgetChars": 5000},
76
+ "outputs": {
77
+ p.name: {"sha256": sha256(p), "bytes": p.stat().st_size}
78
+ for p in [video, gif, poster]
79
+ },
80
+ }
81
+ (assets / "desktop-agent-v090.provenance.json").write_bytes(
82
+ (json.dumps(manifest, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
83
+ )
84
+ print(json.dumps(manifest, ensure_ascii=False, indent=2))
85
+
86
+
87
+ if __name__ == "__main__":
88
+ main()
@@ -1,6 +1,6 @@
1
1
  import assert from 'node:assert/strict'
2
2
  import { execFileSync } from 'node:child_process'
3
- import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
3
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
4
4
  import os from 'node:os'
5
5
  import path from 'node:path'
6
6
  import { fileURLToPath, pathToFileURL } from 'node:url'
@@ -63,10 +63,14 @@ function parseJsonArrayOutput(output, command) {
63
63
  throw new Error(`${command} did not return a JSON array`)
64
64
  }
65
65
 
66
- const packed = parseJsonArrayOutput(run(npm, ['pack', '--ignore-scripts', '--json'], root), 'npm pack')
67
- assert.equal(packed.length, 1, 'npm pack must produce exactly one artifact')
68
- const tarball = path.resolve(root, packed[0].filename)
69
- const listed = new Set(packed[0].files.map((file) => file.path))
66
+ // A release gate can reuse one immutable candidate across Node versions.
67
+ const suppliedTarball = process.argv[2] ? path.resolve(process.argv[2]) : null
68
+ const packed = suppliedTarball ? null : parseJsonArrayOutput(run(npm, ['pack', '--ignore-scripts', '--json'], root), 'npm pack')
69
+ if (packed) assert.equal(packed.length, 1, 'npm pack must produce exactly one artifact')
70
+ const tarball = suppliedTarball ?? path.resolve(root, packed[0].filename)
71
+ const listed = suppliedTarball
72
+ ? new Set(run('tar', ['-tzf', tarball], root).trim().split(/\r?\n/).map(file => file.replace(/^package\//, '')))
73
+ : new Set(packed[0].files.map((file) => file.path))
70
74
  const expectedFiles = [
71
75
  'package.json',
72
76
  'README.md',
@@ -77,6 +81,9 @@ const expectedFiles = [
77
81
  'dist/index.js',
78
82
  'dist/index.d.ts',
79
83
  'dist/client.js',
84
+ 'grammars/tree-sitter-arkts.wasm',
85
+ 'grammars/ARKTS-LICENSE',
86
+ 'grammars/README.md',
80
87
  'scripts/fix-wsl-links.mjs',
81
88
  'scripts/release-smoke.mjs',
82
89
  ]
@@ -158,6 +165,55 @@ try {
158
165
  assert.ok(JSON.stringify(contextPack).length <= 5000)
159
166
  assert.match(await invoke('code_health', { repoRoot: root }), /repo health/)
160
167
 
168
+ // Issue #2 must also be fixed in the installed tarball, not just TS sources.
169
+ const whitelistRoot = path.join(smokeDir, 'whitelist-repo')
170
+ for (const dir of ['.git', 'src', 'extensions/keep', 'extensions/other']) {
171
+ mkdirSync(path.join(whitelistRoot, dir), { recursive: true })
172
+ }
173
+ writeFileSync(path.join(whitelistRoot, '.gitignore'), '/extensions/*\n!/extensions/keep/\n!/extensions/keep/**\n')
174
+ writeFileSync(path.join(whitelistRoot, 'src/control.ts'), 'export function CONTROL_MARKER_fn() { return 3 }\n')
175
+ writeFileSync(path.join(whitelistRoot, 'extensions/keep/kept.ts'), 'export function KEPT_MARKER_fn() { return 1 }\n')
176
+ writeFileSync(path.join(whitelistRoot, 'extensions/other/skip.ts'), 'export function SKIPPED_fn() { return 2 }\n')
177
+ assert.match(await invoke('code_index', { action: 'build', repoRoot: whitelistRoot }), /files indexed: 2/)
178
+ const keptHits = await invoke('code_search', { query: 'KEPT_MARKER_fn', repoRoot: whitelistRoot })
179
+ assert.equal(keptHits[0]?.file, 'extensions/keep/kept.ts')
180
+ assert.deepEqual(await invoke('code_search', { query: 'SKIPPED_fn', repoRoot: whitelistRoot }), [])
181
+ const keptPack = await invoke('code_context', {
182
+ task: 'Explain KEPT_MARKER_fn', repoRoot: whitelistRoot, outputFormat: 'pack',
183
+ })
184
+ assert.ok(keptPack.items.some(item => item.name === 'KEPT_MARKER_fn' && item.code?.includes('return 1')))
185
+ const freshSource = path.join(whitelistRoot, 'extensions/keep/fresh.ts')
186
+ writeFileSync(freshSource, 'export function FRESH_MARKER_fn() { return 91 }\n')
187
+ assert.equal((await invoke('code_search', { query: 'FRESH_MARKER_fn', repoRoot: whitelistRoot }))[0]?.name, 'FRESH_MARKER_fn')
188
+ rmSync(freshSource)
189
+ assert.deepEqual(await invoke('code_search', { query: 'FRESH_MARKER_fn', repoRoot: whitelistRoot }), [])
190
+
191
+ // ArkTS must work from the actual installed package, without native grammar
192
+ // bindings or the source checkout. Keep the fixture tiny and mixed ETS/TS.
193
+ const arkRoot = path.join(smokeDir, 'arkts-repo')
194
+ mkdirSync(path.join(arkRoot, '.git'), { recursive: true })
195
+ writeFileSync(path.join(arkRoot, 'logic.ts'), 'export function label() { return "package ArkTS" }\n')
196
+ const card = '@ComponentV2\nexport struct PackageCard {\n @Local count: number = 0;\n build() { Column() { Text(label()) } }\n}\n'
197
+ writeFileSync(path.join(arkRoot, 'Card.ets'), "import { label } from './logic'\n" + card)
198
+ const arkIndex = await plugin.buildIndex(arkRoot)
199
+ assert.equal(arkIndex.files.length, 2)
200
+ assert.ok(arkIndex.files.every(file => file.diagnostic.status === 'parsed'))
201
+ assert.equal(arkIndex.files.find(file => file.path === 'Card.ets').lang, 'arkts')
202
+ assert.equal((await invoke('code_search', { query: 'PackageCard', repoRoot: arkRoot }))[0]?.file, 'Card.ets')
203
+ const arkPack = await invoke('code_context', { task: 'Explain PackageCard in Card.ets', repoRoot: arkRoot, outputFormat: 'pack', budgetChars: 5000 })
204
+ assert.ok(arkPack.items.some(item => item.name === 'PackageCard' && item.code?.includes('@ComponentV2')))
205
+ assert.ok(JSON.stringify(arkPack).length <= 5000)
206
+ assert.ok(arkPack.items.some(item => item.name === 'label' && item.provenance === 'explicit-import-binding'))
207
+ const arkFresh = path.join(arkRoot, 'Fresh.ets')
208
+ writeFileSync(arkFresh, 'export function ARK_PACKAGE_ADDED() {}\n')
209
+ assert.equal((await invoke('code_search', { query: 'ARK_PACKAGE_ADDED', repoRoot: arkRoot }))[0]?.name, 'ARK_PACKAGE_ADDED')
210
+ writeFileSync(arkFresh, 'export function ARK_PACKAGE_EDITED() {}\n')
211
+ assert.equal((await invoke('code_search', { query: 'ARK_PACKAGE_EDITED', repoRoot: arkRoot }))[0]?.name, 'ARK_PACKAGE_EDITED')
212
+ rmSync(arkFresh)
213
+ assert.deepEqual(await invoke('code_search', { query: 'ARK_PACKAGE_EDITED', repoRoot: arkRoot }), [])
214
+ writeFileSync(path.join(arkRoot, 'Broken.ets'), '@Component\nstruct Broken { build() {')
215
+ assert.match(await invoke('code_index', { repoRoot: arkRoot }), /index incomplete:/)
216
+
161
217
  for (const dispose of disposers.reverse()) dispose()
162
218
 
163
219
  const compactRegistered = []
@@ -183,5 +239,5 @@ try {
183
239
  console.log(`release smoke passed for ${packageJson.name}@${packageJson.version}`)
184
240
  } finally {
185
241
  rmSync(smokeDir, { recursive: true, force: true })
186
- rmSync(tarball, { force: true })
242
+ if (!suppliedTarball) rmSync(tarball, { force: true })
187
243
  }
@@ -0,0 +1,87 @@
1
+ """Render a 42s terminal replay from audited Desktop tool records (Pillow)."""
2
+ import hashlib
3
+ import json
4
+ import textwrap
5
+ from pathlib import Path
6
+ from PIL import Image, ImageDraw, ImageFont
7
+
8
+ ROOT = Path(__file__).resolve().parents[1]
9
+ source = ROOT / 'assets/desktop-v0.9-tool-evidence.json'
10
+ evidence = json.loads(source.read_text(encoding='utf-8'))
11
+ runs = {r['callSeq']: r for r in evidence['runs']}
12
+ clean, changed, small = (runs[k] for k in (47, 60, 73))
13
+ for run in (clean, changed, small):
14
+ assert len(run['renderedText'].encode('utf-16-le')) // 2 == run['actualTextChars']
15
+ assert run['actualTextChars'] <= run['budgetChars']
16
+
17
+ def block(run, prefix):
18
+ return next(p for p in run['renderedText'].split('\n\n') if p.startswith(prefix))
19
+
20
+ def call(run):
21
+ args = {k: v for k, v in run['arguments'].items() if k != 'repoRoot'}
22
+ return 'code_context(' + json.dumps(args, ensure_ascii=False, indent=2) + ')'
23
+
24
+ scenes = [
25
+ ('01 Task -> actual Agent tool call', '任务描述 -> 真实 Agent 工具调用',
26
+ call(clean) + '\n\nCaptured: 2026-10-05 | tool/call #47 -> tool/result #48\nTiny Git fixture | repoRoot omitted from display\n\nTask wording did not name loadConfig.\nThe Agent requested code_context to locate useful context.'),
27
+ ('02 Primary source + inclusion reason', '关键源码、准确行号和入选理由',
28
+ block(clean, 'primary:') + '\n\n' + clean['renderedText'].splitlines()[-1]),
29
+ ('03 Caller + relationship provenance', '相关调用,以及关系证据来源',
30
+ block(clean, 'caller:') + '\n\n' + block(clean, 'relationship: startup')),
31
+ ('04 Likely test + actual test source', '相关测试线索,以及测试源码',
32
+ block(clean, 'test:') + '\n\n' + block(clean, 'relationship: module') + '\n\nLikely tests are retrieval leads, not coverage proof.'),
33
+ ('05 External edit -> Git change context', '外部编辑后,查询看到 Git 变更',
34
+ call(changed) + '\n\n' + block(changed, 'primary: loadConfig') + '\n\n' + block(changed, 'primary: betaCaller')),
35
+ ('06 Deleted source + honest budget fallback', '删除项保留基线源码,小预算明确降级',
36
+ block(changed, 'change: removed') + '\n\n' + block(small, 'primary:') + '\n\nGaps: budget omitted some evidence\nBudget: 327 / 500 chars (trimmed low-priority context)'),
37
+ ('07 Context ready for the next code step', 'Agent 已获得可继续处理任务的上下文',
38
+ 'Review guide / 展示说明(不是 Agent 回复原文)\n\nloadConfig -> startup -> tests/config.spec.ts\nSource ranges + reasons + relationship provenance\nCurrent changes: modified / added / deleted\nClean result: 1294 / 5000 chars\nChange result: 2326 / 5000 chars\n\nNo edit or test-success claim in this replay.\n此回放展示上下文获取,不宣称已修复 Bug 或测试通过。\n\nTry a task in your repo. What context did it miss?\n欢迎试用:它漏掉了什么重要上下文?'),
39
+ ]
40
+
41
+ W, H = 1280, 900
42
+ mono_path = Path('C:/Windows/Fonts/consola.ttf')
43
+ cjk_path = Path('C:/Windows/Fonts/msyh.ttc')
44
+ mono = ImageFont.truetype(str(mono_path), 21)
45
+ ui = ImageFont.truetype(str(cjk_path), 23)
46
+ small_ui = ImageFont.truetype(str(cjk_path), 18)
47
+ title_font = ImageFont.truetype(str(mono_path), 28)
48
+ frames = []
49
+ for i, (title, subtitle, body) in enumerate(scenes):
50
+ for second in (0,):
51
+ im = Image.new('RGB', (W, H), '#0d1420')
52
+ d = ImageDraw.Draw(im)
53
+ d.text((34, 22), 'dsh-code-index v0.9.0 | Edit-ready Context Packs', font=title_font, fill='#f2f6fc')
54
+ d.text((34, 68), '真实 Agent 记录的终端回放 / Replay of real Agent logs', font=ui, fill='#5de4c7')
55
+ d.text((34, 107), 'Not a desktop screen recording | 非桌面录屏 | Edited pacing / 节奏经过剪辑', font=small_ui, fill='#a6b4ca')
56
+ d.rounded_rectangle((24, 151, W-24, H-71), radius=12, fill='#172235', outline='#33445e')
57
+ d.text((44, 169), title, font=title_font, fill='#86c5ff')
58
+ d.text((44, 211), subtitle, font=ui, fill='#e4ecf7')
59
+ y = 265
60
+ display_lines = [part for line in body.splitlines() for part in (textwrap.wrap(line, width=94, break_long_words=False, break_on_hyphens=False) or [''])]
61
+ for line in display_lines:
62
+ font = ui if any(ord(c) > 0x3000 for c in line) else mono
63
+ assert d.textlength(line, font=font) < W-88, f'Line too wide: {line}'
64
+ assert y + 28 < H-77, f'Scene {i+1} too tall'
65
+ color = '#5de4c7' if line.startswith(('Reason:', 'Relationship provenance:', 'Budget:', 'Change:')) else '#e4ecf7'
66
+ d.text((44, y), line, font=font, fill=color)
67
+ y += 27
68
+ elapsed = i*6 + second
69
+ d.text((34, H-57), 'DSH Desktop 0.2.0-rc.2 | Evidence: 2026-10-05 | Excerpts / 原文节选', font=small_ui, fill='#a6b4ca')
70
+ d.text((1090, H-57), f'{elapsed:02d} / 42 s', font=mono, fill='#a6b4ca')
71
+ d.rectangle((0, H-8, int(W*(elapsed+1)/42), H), fill='#5de4c7')
72
+ frames.append(im)
73
+
74
+ assets = ROOT / 'assets'
75
+ frames[1].save(assets / 'agent-context-demo.png')
76
+ frames[0].save(assets / 'agent-context-demo.gif', save_all=True, append_images=frames[1:], duration=6000, loop=0, optimize=True)
77
+ preview_dir = ROOT / '.promotion-work/demo-frames'
78
+ preview_dir.mkdir(parents=True, exist_ok=True)
79
+ for i in range(7):
80
+ frames[i].save(preview_dir / f'{i+1}.png')
81
+ cast_frames = [[i*6, 'o', '\x1b[2J\x1b[H' + title + '\r\n' + subtitle + '\r\n\r\n' + body.replace('\n', '\r\n')] for i, (title, subtitle, body) in enumerate(scenes)]
82
+ cast_frames.append([42, 'o', '\r\nReplay complete / 回放结束\r\n'])
83
+ header = {'version': 2, 'width': 110, 'height': 30, 'title': 'Real Desktop Agent log replay (not screen capture)', 'duration': 42}
84
+ (assets / 'agent-context-demo.cast').write_bytes(('\n'.join(json.dumps(x, ensure_ascii=False) for x in [header, *cast_frames]) + '\n').encode('utf-8'))
85
+ manifest = {'format': 'Terminal-style replay of actual persisted Agent records; not a screen capture', 'durationSeconds': 42, 'source': source.name, 'sourceSha256': hashlib.sha256(source.read_bytes()).hexdigest(), 'callSeqs': [47, 60, 73], 'edits': ['7 panels with 6-second holds; timing is editorial, not measured latency', 'repoRoot omitted from displayed arguments', 'Verbatim source/reason/provenance excerpts; not all returned items shown', 'First/last panel commentary is authored narration, not Agent response'], 'sceneTitles': [s[0] for s in scenes]}
86
+ (assets / 'agent-context-demo.provenance.json').write_bytes((json.dumps(manifest, ensure_ascii=False, indent=2) + '\n').encode('utf-8'))
87
+ print('Rendered 42-second bilingual replay from real Desktop callSeqs 47, 60, 73.')