@keyou007/agent-bot 0.1.17 → 0.1.18-alpha.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.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@keyou007/agent-bot",
3
- "version": "0.1.17",
3
+ "version": "0.1.18-alpha.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@keyou007/agent-bot",
9
- "version": "0.1.17",
9
+ "version": "0.1.18-alpha.0",
10
10
  "license": "GPL-3.0-only",
11
11
  "dependencies": {
12
12
  "@agentclientprotocol/sdk": "1.2.1",
@@ -16,6 +16,7 @@
16
16
  "better-sqlite3": "12.11.1",
17
17
  "dotenv": "17.4.2",
18
18
  "highlight.js": "11.12.0",
19
+ "markdown-it": "15.0.1",
19
20
  "pino": "10.3.1",
20
21
  "yaml": "2.9.0",
21
22
  "zod": "4.4.3"
@@ -1927,6 +1928,22 @@
1927
1928
  "url": "https://github.com/sponsors/epoberezkin"
1928
1929
  }
1929
1930
  },
1931
+ "node_modules/argparse": {
1932
+ "version": "3.0.1",
1933
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-3.0.1.tgz",
1934
+ "integrity": "sha512-nM4mHF/KM1v59ZNKX7zfusQz5wUAxR511YG8Vo6TyiV4aqhu++rbJW4v04xsWhpSsHFj66flT8P7znVpyO20xQ==",
1935
+ "funding": [
1936
+ {
1937
+ "type": "github",
1938
+ "url": "https://github.com/sponsors/puzrin"
1939
+ },
1940
+ {
1941
+ "type": "github",
1942
+ "url": "https://github.com/sponsors/nodeca"
1943
+ }
1944
+ ],
1945
+ "license": "PSF-2.0"
1946
+ },
1930
1947
  "node_modules/assertion-error": {
1931
1948
  "version": "2.0.1",
1932
1949
  "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
@@ -2245,6 +2262,18 @@
2245
2262
  "once": "^1.4.0"
2246
2263
  }
2247
2264
  },
2265
+ "node_modules/entities": {
2266
+ "version": "8.0.0",
2267
+ "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
2268
+ "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
2269
+ "license": "BSD-2-Clause",
2270
+ "engines": {
2271
+ "node": ">=20.19.0"
2272
+ },
2273
+ "funding": {
2274
+ "url": "https://github.com/fb55/entities?sponsor=1"
2275
+ }
2276
+ },
2248
2277
  "node_modules/es-define-property": {
2249
2278
  "version": "1.0.1",
2250
2279
  "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -3251,6 +3280,25 @@
3251
3280
  "url": "https://opencollective.com/parcel"
3252
3281
  }
3253
3282
  },
3283
+ "node_modules/linkify-it": {
3284
+ "version": "6.1.0",
3285
+ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-6.1.0.tgz",
3286
+ "integrity": "sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==",
3287
+ "funding": [
3288
+ {
3289
+ "type": "github",
3290
+ "url": "https://github.com/sponsors/puzrin"
3291
+ },
3292
+ {
3293
+ "type": "github",
3294
+ "url": "https://github.com/sponsors/markdown-it"
3295
+ }
3296
+ ],
3297
+ "license": "MIT",
3298
+ "dependencies": {
3299
+ "uc.micro": "^3.0.0"
3300
+ }
3301
+ },
3254
3302
  "node_modules/locate-path": {
3255
3303
  "version": "6.0.0",
3256
3304
  "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -3301,6 +3349,33 @@
3301
3349
  "@jridgewell/sourcemap-codec": "^1.5.5"
3302
3350
  }
3303
3351
  },
3352
+ "node_modules/markdown-it": {
3353
+ "version": "15.0.1",
3354
+ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-15.0.1.tgz",
3355
+ "integrity": "sha512-9/7gE95FNPkfUWrjJIoHZza2iLmuJlPD0UNMxPi7bxUrbCR525YZY0r+zyfes0dZI5ZZ/uNIXUJca0pJvtw41g==",
3356
+ "funding": [
3357
+ {
3358
+ "type": "github",
3359
+ "url": "https://github.com/sponsors/puzrin"
3360
+ },
3361
+ {
3362
+ "type": "github",
3363
+ "url": "https://github.com/sponsors/markdown-it"
3364
+ }
3365
+ ],
3366
+ "license": "MIT",
3367
+ "dependencies": {
3368
+ "argparse": "^3.0.0",
3369
+ "entities": "^8.0.0",
3370
+ "linkify-it": "^6.0.0",
3371
+ "mdurl": "^2.1.0",
3372
+ "punycode.js": "^2.3.1",
3373
+ "uc.micro": "^3.0.0"
3374
+ },
3375
+ "bin": {
3376
+ "markdown-it": "bin/markdown-it.mjs"
3377
+ }
3378
+ },
3304
3379
  "node_modules/math-intrinsics": {
3305
3380
  "version": "1.1.0",
3306
3381
  "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -3310,6 +3385,12 @@
3310
3385
  "node": ">= 0.4"
3311
3386
  }
3312
3387
  },
3388
+ "node_modules/mdurl": {
3389
+ "version": "2.1.0",
3390
+ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz",
3391
+ "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==",
3392
+ "license": "MIT"
3393
+ },
3313
3394
  "node_modules/mime-db": {
3314
3395
  "version": "1.52.0",
3315
3396
  "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -3752,6 +3833,15 @@
3752
3833
  "node": ">=6"
3753
3834
  }
3754
3835
  },
3836
+ "node_modules/punycode.js": {
3837
+ "version": "2.3.1",
3838
+ "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
3839
+ "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
3840
+ "license": "MIT",
3841
+ "engines": {
3842
+ "node": ">=6"
3843
+ }
3844
+ },
3755
3845
  "node_modules/qs": {
3756
3846
  "version": "6.15.3",
3757
3847
  "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
@@ -4277,6 +4367,12 @@
4277
4367
  "@typescript/typescript-win32-x64": "7.0.2"
4278
4368
  }
4279
4369
  },
4370
+ "node_modules/uc.micro": {
4371
+ "version": "3.0.0",
4372
+ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-3.0.0.tgz",
4373
+ "integrity": "sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw==",
4374
+ "license": "MIT"
4375
+ },
4280
4376
  "node_modules/undici-types": {
4281
4377
  "version": "6.21.0",
4282
4378
  "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keyou007/agent-bot",
3
- "version": "0.1.17",
3
+ "version": "0.1.18-alpha.0",
4
4
  "type": "module",
5
5
  "description": "Use Codex, TraeX, and compatible coding agents from Lark/Feishu.",
6
6
  "license": "GPL-3.0-only",
@@ -74,6 +74,7 @@
74
74
  "better-sqlite3": "12.11.1",
75
75
  "dotenv": "17.4.2",
76
76
  "highlight.js": "11.12.0",
77
+ "markdown-it": "15.0.1",
77
78
  "pino": "10.3.1",
78
79
  "yaml": "2.9.0",
79
80
  "zod": "4.4.3"
@@ -6,6 +6,7 @@ import hljs from "highlight.js/lib/common";
6
6
  import dos from "highlight.js/lib/languages/dos";
7
7
  import dockerfile from "highlight.js/lib/languages/dockerfile";
8
8
  import powershell from "highlight.js/lib/languages/powershell";
9
+ import MarkdownIt from "markdown-it";
9
10
  import {
10
11
  selectPreferredNetworkAddress,
11
12
  type NetworkConnectionKind,
@@ -19,10 +20,27 @@ const MAX_TEXT_PREVIEW_BYTES = 2 * 1024 * 1024;
19
20
  const MAX_CONTENT_SAMPLE_BYTES = 64 * 1024;
20
21
  const MAX_SYNTAX_HIGHLIGHT_BYTES = 512 * 1024;
21
22
  const DIRECTORY_PAGE_SIZE = 100;
23
+ const MARKDOWN_EXTENSIONS = new Set([".markdown", ".md", ".mdown", ".mkd", ".mkdn"]);
22
24
  hljs.registerLanguage("dos", dos);
23
25
  hljs.registerLanguage("dockerfile", dockerfile);
24
26
  hljs.registerLanguage("powershell", powershell);
25
27
 
28
+ const MARKDOWN_RENDERER = new MarkdownIt({
29
+ breaks: false,
30
+ html: false,
31
+ linkify: true,
32
+ typographer: false,
33
+ highlight(value, language) {
34
+ const highlighted = language && hljs.getLanguage(language)
35
+ ? hljs.highlight(value, { language, ignoreIllegals: true }).value
36
+ : escapeHtml(value);
37
+ const languageAttribute = language && hljs.getLanguage(language)
38
+ ? ` class="language-${escapeAttribute(language)}"`
39
+ : "";
40
+ return `<pre class="markdown-code-block hljs"><code${languageAttribute}>${highlighted}</code></pre>`;
41
+ },
42
+ });
43
+
26
44
  const HIGHLIGHT_LANGUAGE_BY_EXTENSION: Readonly<Record<string, string>> = {
27
45
  ".bash": "bash", ".bat": "dos", ".c": "c", ".cc": "cpp", ".cmd": "dos", ".cpp": "cpp",
28
46
  ".cs": "csharp", ".css": "css", ".cxx": "cpp", ".diff": "diff", ".go": "go", ".h": "cpp",
@@ -46,9 +64,31 @@ const VIEWER_CLIENT_SCRIPT = `(() => {
46
64
  const metadata = document.getElementById("viewer-metadata");
47
65
  const header = document.getElementById("viewer-header");
48
66
  const title = document.getElementById("viewer-title");
67
+ const viewSwitch = document.getElementById("viewer-view-switch");
68
+ const viewButtons = Array.from(document.querySelectorAll("[data-view-mode-button]"));
49
69
  if (!content || !metadata || !header || !title) return;
50
70
  const filePath = title.dataset.filePath || title.textContent || "";
51
71
 
72
+ const syncViewMode = () => {
73
+ const mode = document.body.dataset.viewMode === "code" ? "code" : "rendered";
74
+ for (const button of viewButtons) {
75
+ const active = button.dataset.viewModeButton === mode;
76
+ button.classList.toggle("is-active", active);
77
+ button.setAttribute("aria-selected", String(active));
78
+ }
79
+ for (const panel of content.querySelectorAll("[data-view-panel]")) {
80
+ panel.setAttribute("aria-hidden", String(panel.dataset.viewPanel !== mode));
81
+ }
82
+ };
83
+ const setViewMode = (mode) => {
84
+ document.body.dataset.viewMode = mode === "code" ? "code" : "rendered";
85
+ syncViewMode();
86
+ if (mode === "code") requestAnimationFrame(positionHashTarget);
87
+ };
88
+ for (const button of viewButtons) {
89
+ button.addEventListener("click", () => setViewMode(button.dataset.viewModeButton));
90
+ }
91
+
52
92
  title.addEventListener("pointerdown", (event) => {
53
93
  if (event.button !== 0) return;
54
94
  event.preventDefault();
@@ -83,6 +123,8 @@ const VIEWER_CLIENT_SCRIPT = `(() => {
83
123
  const positionHashTarget = () => {
84
124
  highlightHashTarget()?.scrollIntoView({ block: "start" });
85
125
  };
126
+ if (/^#L\\d+$/u.test(window.location.hash)) document.body.dataset.viewMode = "code";
127
+ syncViewMode();
86
128
  updateHeaderOffset();
87
129
  if (typeof ResizeObserver === "function") {
88
130
  new ResizeObserver(updateHeaderOffset).observe(header);
@@ -107,6 +149,7 @@ const VIEWER_CLIENT_SCRIPT = `(() => {
107
149
  const codeScrollLeft = content.querySelector(".code")?.scrollLeft ?? 0;
108
150
  content.innerHTML = nextContent;
109
151
  metadata.innerHTML = nextMetadata;
152
+ syncViewMode();
110
153
  highlightHashTarget();
111
154
  restoreScroll(top, left, atBottom, codeScrollLeft);
112
155
  };
@@ -116,6 +159,7 @@ const VIEWER_CLIENT_SCRIPT = `(() => {
116
159
  try {
117
160
  const update = JSON.parse(event.data);
118
161
  if (typeof update.content === "string" && typeof update.metadata === "string") {
162
+ if (viewSwitch) viewSwitch.hidden = update.viewMode !== "markdown";
119
163
  replaceContent(update.content, update.metadata);
120
164
  }
121
165
  } catch {
@@ -157,6 +201,7 @@ interface DirectoryViewerEntry {
157
201
  interface RenderedFileSnapshot {
158
202
  content: string;
159
203
  metadata: string[];
204
+ viewMode?: "markdown";
160
205
  }
161
206
 
162
207
  export interface LocalFileViewerServerOptions {
@@ -423,6 +468,7 @@ export class LocalFileViewerServer {
423
468
  { href: downloadUrl.toString(), label: "下载" },
424
469
  ],
425
470
  content: snapshot.content,
471
+ viewMode: snapshot.viewMode,
426
472
  liveUpdates: {
427
473
  eventsUrl: eventsUrl.toString(),
428
474
  scriptPath: `${this.basePath}/assets/viewer.js`.replace(/\/{2,}/gu, "/"),
@@ -442,7 +488,10 @@ export class LocalFileViewerServer {
442
488
  let content: string;
443
489
  if (classification.kind === "text") {
444
490
  const preview = readTextPreview(filePath, stat.size, classification.encoding);
445
- content = `${preview.truncated ? '<div class="notice">文件较大,仅显示开头 2 MiB。可使用下方按钮查看或下载完整文件。</div>' : ""}${renderText(preview.text, filePath)}`;
491
+ const previewContent = isMarkdownFile(filePath)
492
+ ? renderMarkdownDocument(preview.text, filePath)
493
+ : renderText(preview.text, filePath);
494
+ content = `${preview.truncated ? '<div class="notice">文件较大,仅显示开头 2 MiB。可使用下方按钮查看或下载完整文件。</div>' : ""}${previewContent}`;
446
495
  } else if (contentType.startsWith("image/")) {
447
496
  content = `<div class="media"><img src="${escapeAttribute(rawUrl.toString())}" alt="${escapeAttribute(fileName)}"></div>`;
448
497
  } else if (contentType === "application/pdf") {
@@ -457,6 +506,9 @@ export class LocalFileViewerServer {
457
506
  return {
458
507
  content,
459
508
  metadata: [formatFileSize(stat.size), stat.mtime.toLocaleString()],
509
+ ...(classification.kind === "text" && isMarkdownFile(filePath)
510
+ ? { viewMode: "markdown" as const }
511
+ : {}),
460
512
  };
461
513
  }
462
514
 
@@ -485,6 +537,7 @@ export class LocalFileViewerServer {
485
537
  writeServerSentEvent(response, "update", JSON.stringify({
486
538
  content: snapshot.content,
487
539
  metadata: renderMetadata(snapshot.metadata),
540
+ viewMode: snapshot.viewMode ?? null,
488
541
  }));
489
542
  } catch {
490
543
  writeServerSentEvent(response, "unavailable", JSON.stringify({
@@ -637,12 +690,19 @@ function renderViewerPage(input: {
637
690
  metadata: string[];
638
691
  actions: Array<{ href: string; label: string }>;
639
692
  content: string;
693
+ viewMode?: "markdown";
640
694
  liveUpdates?: { eventsUrl: string; scriptPath: string };
641
695
  }): string {
642
696
  const metadata = renderMetadata(input.metadata);
643
697
  const actions = input.actions.length > 0
644
698
  ? `<nav class="actions">${input.actions.map((action) => `<a href="${escapeAttribute(action.href)}">${escapeHtml(action.label)}</a>`).join("")}</nav>`
645
699
  : "";
700
+ const viewSwitch = input.viewMode === "markdown"
701
+ ? '<div class="view-switch" id="viewer-view-switch" role="tablist" aria-label="预览模式"><button class="is-active" type="button" role="tab" aria-selected="true" data-view-mode-button="rendered">预览</button><button type="button" role="tab" aria-selected="false" data-view-mode-button="code">代码</button></div>'
702
+ : "";
703
+ const toolbar = viewSwitch || actions
704
+ ? `<div class="toolbar">${viewSwitch}${actions}</div>`
705
+ : "";
646
706
  const liveAttributes = input.liveUpdates
647
707
  ? ` data-events-url="${escapeAttribute(input.liveUpdates.eventsUrl)}"`
648
708
  : "";
@@ -661,10 +721,15 @@ function renderViewerPage(input: {
661
721
  body { margin: 0; background: #f5f7fa; color: #1f2329; }
662
722
  header { position: sticky; top: 0; z-index: 2; padding: 7px 12px; background: rgba(255,255,255,.96); border-bottom: 1px solid #dfe3e8; }
663
723
  h1 { margin: 0; cursor: text; font: 600 13px/1.4 ui-monospace, SFMono-Regular, Consolas, monospace; overflow-wrap: anywhere; }
664
- .meta { display: flex; gap: 10px; align-items: center; min-height: 26px; margin-top: 3px; color: #646a73; font-size: 11px; }
724
+ .meta { display: flex; flex-wrap: wrap; gap: 6px 10px; align-items: center; min-height: 26px; margin-top: 3px; color: #646a73; font-size: 11px; }
665
725
  .metadata-values { display: contents; }
666
- .actions { margin-left: auto; display: flex; gap: 8px; }
726
+ .toolbar { margin-left: auto; display: flex; gap: 8px; align-items: center; }
727
+ .actions { display: flex; gap: 8px; }
667
728
  .actions a { color: #1456f0; text-decoration: none; padding: 3px 7px; border: 1px solid #c9d0db; border-radius: 4px; background: #fff; }
729
+ .view-switch { display: inline-flex; padding: 2px; border: 1px solid #c9d0db; border-radius: 5px; background: #f2f3f5; }
730
+ .view-switch[hidden] { display: none; }
731
+ .view-switch button { min-width: 46px; padding: 2px 8px; border: 0; border-radius: 3px; background: transparent; color: #646a73; cursor: pointer; font: inherit; }
732
+ .view-switch button.is-active { background: #fff; color: #1456f0; box-shadow: 0 1px 3px rgba(31,35,41,.16); }
668
733
  main { padding: 0; }
669
734
  .notice, .unsupported { margin: 8px; padding: 10px 12px; border: 1px solid #f3cf8f; background: #fff7e6; border-radius: 5px; }
670
735
  .code { margin: 0; padding: 6px 0; overflow: auto; border: 0; border-radius: 0; background: #fff; color: #1f2329; font-size: 13px; line-height: 1.5; tab-size: 2; }
@@ -681,6 +746,28 @@ function renderViewerPage(input: {
681
746
  .hljs-addition { background: #e6ffed; }
682
747
  .hljs-emphasis { font-style: italic; }
683
748
  .hljs-strong { font-weight: 700; }
749
+ body[data-view-mode="rendered"] [data-view-panel="code"], body[data-view-mode="code"] [data-view-panel="rendered"] { display: none; }
750
+ .markdown-body { max-width: 980px; margin: 0 auto; padding: 20px 28px 48px; background: #fff; font-size: 15px; line-height: 1.72; overflow-wrap: anywhere; }
751
+ .markdown-body > :first-child { margin-top: 0; }
752
+ .markdown-body > :last-child { margin-bottom: 0; }
753
+ .markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 { margin: 1.45em 0 .6em; line-height: 1.28; }
754
+ .markdown-body h1 { padding-bottom: .35em; border-bottom: 1px solid #dfe3e8; font-size: 2em; }
755
+ .markdown-body h2 { padding-bottom: .3em; border-bottom: 1px solid #eceff3; font-size: 1.5em; }
756
+ .markdown-body h3 { font-size: 1.25em; }
757
+ .markdown-body p, .markdown-body ul, .markdown-body ol, .markdown-body blockquote, .markdown-body table, .markdown-body pre { margin: 0 0 1em; }
758
+ .markdown-body ul, .markdown-body ol { padding-left: 1.8em; }
759
+ .markdown-body li + li { margin-top: .28em; }
760
+ .markdown-body blockquote { margin-left: 0; padding: .15em 1em; border-left: 4px solid #c9d0db; color: #646a73; }
761
+ .markdown-body a { color: #1456f0; text-decoration: none; }
762
+ .markdown-body a:hover { text-decoration: underline; }
763
+ .markdown-body code { padding: .14em .35em; border-radius: 4px; background: #f2f3f5; font-family: var(--viewer-code-font); font-size: .9em; }
764
+ .markdown-body .markdown-code-block { margin: 0 0 1em; padding: 14px 16px; overflow: auto; border-radius: 6px; background: #f6f8fa; line-height: 1.55; }
765
+ .markdown-body .markdown-code-block code { padding: 0; background: transparent; font-size: 13px; }
766
+ .markdown-body table { display: block; max-width: 100%; overflow: auto; border-collapse: collapse; }
767
+ .markdown-body th, .markdown-body td { padding: 7px 12px; border: 1px solid #dfe3e8; text-align: left; }
768
+ .markdown-body th { background: #f5f7fa; }
769
+ .markdown-body img { max-width: 100%; height: auto; }
770
+ .markdown-body hr { height: 1px; margin: 1.5em 0; border: 0; background: #dfe3e8; }
684
771
  .media { display: grid; place-items: center; min-height: 220px; }
685
772
  .media img, .media video { max-width: 100%; max-height: calc(100vh - 70px); }
686
773
  .media audio { width: min(720px, 100%); }
@@ -704,7 +791,19 @@ function renderViewerPage(input: {
704
791
  .meta, .pagination { color: #a6a9ad; }
705
792
  .actions a, .pagination a { color: #8ab4ff; }
706
793
  .actions a { border-color: #55585c; background: #292a2d; }
794
+ .view-switch { border-color: #55585c; background: #292a2d; }
795
+ .view-switch button { color: #a6a9ad; }
796
+ .view-switch button.is-active { background: #3a3b3d; color: #8ab4ff; box-shadow: none; }
707
797
  .code, .directory-list { background: #202124; color: #e8eaed; border-color: #3a3b3d; }
798
+ .markdown-body { background: #202124; color: #e8eaed; }
799
+ .markdown-body h1, .markdown-body h2 { border-color: #3a3b3d; }
800
+ .markdown-body blockquote { color: #a6a9ad; border-color: #55585c; }
801
+ .markdown-body a { color: #8ab4ff; }
802
+ .markdown-body code { background: #303134; }
803
+ .markdown-body .markdown-code-block { background: #282a2d; }
804
+ .markdown-body th, .markdown-body td { border-color: #55585c; }
805
+ .markdown-body th { background: #292a2d; }
806
+ .markdown-body hr { background: #3a3b3d; }
708
807
  .directory-entry { border-color: #3a3b3d; }
709
808
  .directory-entry:hover { background: #292f3d; }
710
809
  .line-number { color: #8b9098; border-color: #3a3b3d; }
@@ -720,10 +819,10 @@ function renderViewerPage(input: {
720
819
  }
721
820
  </style>
722
821
  </head>
723
- <body${liveAttributes}>
822
+ <body${input.viewMode === "markdown" ? ' data-view-mode="rendered"' : ""}${liveAttributes}>
724
823
  <header id="viewer-header">
725
824
  <h1 id="viewer-title" data-file-path="${escapeAttribute(input.filePath)}">${escapeHtml(input.filePath)}</h1>
726
- <div class="meta"><span class="metadata-values" id="viewer-metadata">${metadata}</span>${actions}</div>
825
+ <div class="meta"><span class="metadata-values" id="viewer-metadata">${metadata}</span>${toolbar}</div>
727
826
  </header>
728
827
  <main id="viewer-content">${input.content}</main>
729
828
  ${liveScript}
@@ -976,6 +1075,14 @@ function renderText(value: string, filePath: string): string {
976
1075
  }).join("")}</code></pre>`;
977
1076
  }
978
1077
 
1078
+ function renderMarkdownDocument(value: string, filePath: string): string {
1079
+ return `<article class="markdown-body" data-view-panel="rendered">${MARKDOWN_RENDERER.render(value)}</article><section data-view-panel="code">${renderText(value, filePath)}</section>`;
1080
+ }
1081
+
1082
+ function isMarkdownFile(filePath: string): boolean {
1083
+ return MARKDOWN_EXTENSIONS.has(path.extname(filePath).toLowerCase());
1084
+ }
1085
+
979
1086
  function syntaxLanguageFor(filePath: string): string | undefined {
980
1087
  const fileName = path.basename(filePath).toLowerCase();
981
1088
  return HIGHLIGHT_LANGUAGE_BY_FILENAME[fileName]
@@ -1757,7 +1757,7 @@ export class ProxySessionController {
1757
1757
  }
1758
1758
  switch (command.type) {
1759
1759
  case "shell":
1760
- await this.runShellCommand(contextKey, command.command, messageId);
1760
+ await this.runShellCommand(contextKey, command.command, messageId, incomingMessage);
1761
1761
  return;
1762
1762
  case "dir":
1763
1763
  await this.openDirectoryBrowser(contextKey, command.directory);
@@ -4963,8 +4963,9 @@ export class ProxySessionController {
4963
4963
  contextKey: string,
4964
4964
  command: string,
4965
4965
  sourceMessageId?: string,
4966
+ incomingMessage?: IncomingMessage,
4966
4967
  ): Promise<void> {
4967
- const cwd = this.currentSession(contextKey)?.cwd ?? this.config.defaults.cwd;
4968
+ const cwd = this.shellCommandCwd(contextKey, incomingMessage);
4968
4969
  const created = await this.shellCommandJobs.createJob({ contextKey, sourceMessageId, command, cwd });
4969
4970
  let cardMessageId: string | undefined;
4970
4971
  const initialState: ShellCommandCardView = {
@@ -5010,6 +5011,28 @@ export class ProxySessionController {
5010
5011
  }
5011
5012
  }
5012
5013
 
5014
+ private shellCommandCwd(contextKey: string, incomingMessage?: IncomingMessage): string {
5015
+ const current = this.currentSession(contextKey);
5016
+ if (current) return current.cwd;
5017
+
5018
+ if (incomingMessage?.threadContext) {
5019
+ const anchor = threadForkAnchorMessageIds(incomingMessage)
5020
+ .map((messageId) => this.store.findTurnAnchorByMessageId(messageId))
5021
+ .find((candidate) => candidate !== undefined);
5022
+ const source = anchor?.contextKey
5023
+ ? this.store.getSessionForContext(anchor.localSessionId, anchor.contextKey)
5024
+ : anchor
5025
+ ? this.store.getSession(anchor.localSessionId)
5026
+ : undefined;
5027
+ const sourceContextKey = anchor?.contextKey ?? source?.contextKey;
5028
+ if (source && sourceContextKey && baseChatContextKey(sourceContextKey) === baseChatContextKey(contextKey)) {
5029
+ return source.cwd;
5030
+ }
5031
+ }
5032
+
5033
+ return this.config.defaults.cwd;
5034
+ }
5035
+
5013
5036
  private monitorShellCommandJob(
5014
5037
  job: ShellCommandJobSnapshot,
5015
5038
  seededState?: ShellCommandCardView,