@keyou007/agent-bot 0.1.21 → 0.1.22

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.
@@ -622,10 +622,12 @@ export class LocalFileViewerServer {
622
622
 
623
623
  let closed = false;
624
624
  let pendingUpdate: NodeJS.Timeout | undefined;
625
+ let previousStat: fs.Stats | undefined;
625
626
  const sendSnapshot = () => {
626
627
  if (closed || response.destroyed) return;
627
628
  try {
628
629
  const stat = fs.statSync(filePath);
630
+ previousStat = stat;
629
631
  if (!stat.isFile()) throw new Error("Path is no longer a file");
630
632
  const snapshot = this.renderFileSnapshot(filePath, stat);
631
633
  writeServerSentEvent(response, "update", JSON.stringify({
@@ -644,17 +646,27 @@ export class LocalFileViewerServer {
644
646
  pendingUpdate = setTimeout(sendSnapshot, 100);
645
647
  pendingUpdate.unref();
646
648
  };
647
- const watcher = (current: fs.Stats, previous: fs.Stats) => {
648
- if (
649
- current.mtimeMs === previous.mtimeMs
650
- && current.ctimeMs === previous.ctimeMs
651
- && current.size === previous.size
652
- && current.nlink === previous.nlink
653
- ) return;
654
- scheduleSnapshot();
655
- };
656
- fs.watchFile(filePath, { interval: 1_000, persistent: false }, watcher);
657
649
  sendSnapshot();
650
+ // Compare against the sent snapshot so edits during watcher startup cannot be missed.
651
+ let checking = false;
652
+ const poller = setInterval(() => {
653
+ if (closed || checking) return;
654
+ checking = true;
655
+ fs.stat(filePath, (error, stat) => {
656
+ checking = false;
657
+ if (closed) return;
658
+ const current = error ? undefined : stat;
659
+ if (
660
+ current?.mtimeMs === previousStat?.mtimeMs
661
+ && current?.ctimeMs === previousStat?.ctimeMs
662
+ && current?.size === previousStat?.size
663
+ && current?.nlink === previousStat?.nlink
664
+ ) return;
665
+ previousStat = current;
666
+ scheduleSnapshot();
667
+ });
668
+ }, 1_000);
669
+ poller.unref();
658
670
  const keepAlive = setInterval(() => {
659
671
  if (!closed && !response.destroyed) response.write(": keep-alive\n\n");
660
672
  }, 15_000);
@@ -665,7 +677,7 @@ export class LocalFileViewerServer {
665
677
  closed = true;
666
678
  if (pendingUpdate) clearTimeout(pendingUpdate);
667
679
  clearInterval(keepAlive);
668
- fs.unwatchFile(filePath, watcher);
680
+ clearInterval(poller);
669
681
  };
670
682
  request.once("aborted", cleanup);
671
683
  response.once("close", cleanup);
@@ -8,8 +8,8 @@ export function isFileUrl(value: string): boolean {
8
8
 
9
9
  export function rewriteMarkdownFileLinks(
10
10
  tokens: Token[],
11
- markdownPath: string,
12
- createPreviewUrl: (filePath: string) => URL,
11
+ markdownPath: string | undefined,
12
+ createPreviewUrl: (filePath: string) => URL | undefined,
13
13
  ): void {
14
14
  for (const token of tokens) {
15
15
  const attribute = token.type === "link_open" ? "href" : token.type === "image" ? "src" : undefined;
@@ -18,9 +18,13 @@ export function rewriteMarkdownFileLinks(
18
18
  const local = resolveLocalTarget(target, markdownPath);
19
19
  if (local) {
20
20
  const url = createPreviewUrl(local.filePath);
21
- url.hash = local.hash;
22
- if (token.type === "image") url.searchParams.set("raw", "1");
23
- token.attrSet(attribute, url.toString());
21
+ if (url) {
22
+ url.hash = local.hash;
23
+ if (token.type === "image") url.searchParams.set("raw", "1");
24
+ token.attrSet(attribute, url.toString());
25
+ } else {
26
+ token.attrs = token.attrs?.filter(([name]) => name !== attribute) ?? null;
27
+ }
24
28
  } else if (isFileUrl(target)) {
25
29
  // File URLs are accepted by the parser only to convert them to signed HTTP URLs.
26
30
  token.attrs = token.attrs?.filter(([name]) => name !== attribute) ?? null;
@@ -30,7 +34,7 @@ export function rewriteMarkdownFileLinks(
30
34
  }
31
35
  }
32
36
 
33
- function resolveLocalTarget(target: string, markdownPath: string): { filePath: string; hash: string } | undefined {
37
+ function resolveLocalTarget(target: string, markdownPath: string | undefined): { filePath: string; hash: string } | undefined {
34
38
  if (/^(?:#|\?|\/\/)/u.test(target)) return undefined;
35
39
  const windows = process.platform === "win32";
36
40
  const normalized = windows ? target.replace(/%5c/giu, "/").replaceAll("\\", "/") : target;
@@ -44,11 +48,12 @@ function resolveLocalTarget(target: string, markdownPath: string): { filePath: s
44
48
  ? normalized.slice(0, reference.index) + normalized.slice(reference.index + reference[0].length)
45
49
  : normalized;
46
50
  if (drivePath && !windows) return undefined;
51
+ if (!markdownPath && !drivePath && !isFileUrl(normalized) && !path.isAbsolute(normalized)) return undefined;
47
52
 
48
53
  try {
49
54
  const url = drivePath
50
55
  ? new URL(`file:///${withoutReference.replace(/^\//u, "")}`)
51
- : new URL(withoutReference, pathToFileURL(markdownPath));
56
+ : new URL(withoutReference, markdownPath ? pathToFileURL(markdownPath) : "file:///");
52
57
  if (url.protocol !== "file:") return undefined;
53
58
  const filePath = fileURLToPath(url);
54
59
  if (!path.isAbsolute(filePath) || /[\x00-\x1f\x7f]/u.test(filePath)) return undefined;
@@ -3,6 +3,7 @@ import MarkdownIt from "markdown-it";
3
3
  import type { ToolState } from "../runtime/types.js";
4
4
  import type { FileSummary, TurnActivity, TurnViewState, TurnViewStatus } from "../presentation/turnViewTypes.js";
5
5
  import { displayToolCommand, formatShellCommandForDisplay } from "../feishu/CardRenderer.js";
6
+ import { isFileUrl, rewriteMarkdownFileLinks } from "./MarkdownFileLinks.js";
6
7
 
7
8
  const MARKDOWN = new MarkdownIt({
8
9
  breaks: true,
@@ -10,6 +11,8 @@ const MARKDOWN = new MarkdownIt({
10
11
  linkify: true,
11
12
  typographer: false,
12
13
  });
14
+ const validateLink = MARKDOWN.validateLink.bind(MARKDOWN);
15
+ MARKDOWN.validateLink = (url) => isFileUrl(url) || validateLink(url);
13
16
  const defaultLinkOpen = MARKDOWN.renderer.rules.link_open
14
17
  ?? ((tokens, index, options, _environment, renderer) => renderer.renderToken(tokens, index, options));
15
18
  MARKDOWN.renderer.rules.link_open = (tokens, index, options, environment, renderer) => {
@@ -284,9 +287,10 @@ export function renderTurnPreviewSnapshot(
284
287
  if (!state.projectCwd || !path.isAbsolute(state.projectCwd)) return undefined;
285
288
  return localFileUrl(path.resolve(state.projectCwd, filePath));
286
289
  } : undefined;
287
- const timeline = renderTimeline(state.activities ?? [], resolveFileUrl, state.fullToolOutputs, state.fullToolErrors, language);
290
+ const renderText = (value: string): string => renderMarkdown(value, state.projectCwd, localFileUrl);
291
+ const timeline = renderTimeline(state.activities ?? [], renderText, resolveFileUrl, state.fullToolOutputs, state.fullToolErrors, language);
288
292
  const sections = [
289
- state.prompt ? renderMessageActivity("prompt", state.prompt, "user prompt") : "",
293
+ state.prompt ? renderMessageActivity("prompt", state.prompt, "user prompt", renderText) : "",
290
294
  state.plan.length > 0 ? renderPlan(state, language) : "",
291
295
  state.activitiesTruncated
292
296
  ? `<div class="notice">${escapeHtml(labels.truncated)}</div>`
@@ -295,9 +299,9 @@ export function renderTurnPreviewSnapshot(
295
299
  state.fileSummary.length > 0 ? renderFileSummary(state, language, resolveFileUrl) : "",
296
300
  state.error ? `<section class="result error-result"><h2>${escapeHtml(labels.error)}</h2><pre>${escapeHtml(state.error)}</pre></section>` : "",
297
301
  state.finalResponse
298
- ? `<section class="result final-result"><h2>${escapeHtml(labels.finalAnswer)}</h2><div class="markdown">${renderMarkdown(state.finalResponse)}</div></section>`
302
+ ? `<section class="result final-result"><h2>${escapeHtml(labels.finalAnswer)}</h2><div class="markdown">${renderText(state.finalResponse)}</div></section>`
299
303
  : state.assistantText
300
- ? `<section class="result"><h2>${escapeHtml(labels.generating)}</h2><div class="markdown">${renderMarkdown(state.assistantText)}</div></section>`
304
+ ? `<section class="result"><h2>${escapeHtml(labels.generating)}</h2><div class="markdown">${renderText(state.assistantText)}</div></section>`
301
305
  : "",
302
306
  ].filter(Boolean).join("");
303
307
  const terminal = isTerminal(state.status);
@@ -322,6 +326,7 @@ function renderPlan(state: TurnViewState, language: TurnPreviewLanguage): string
322
326
 
323
327
  function renderTimeline(
324
328
  activities: TurnActivity[],
329
+ renderText: (value: string) => string,
325
330
  localFileUrl?: (filePath: string) => string | undefined,
326
331
  fullToolOutputs?: Record<string, string>,
327
332
  fullToolErrors?: Record<string, string>,
@@ -329,12 +334,13 @@ function renderTimeline(
329
334
  ): string {
330
335
  return activities
331
336
  .filter((activity): activity is Exclude<TurnActivity, { kind: "reasoning" }> => activity.kind !== "reasoning")
332
- .map((activity) => renderActivity(activity, localFileUrl, fullToolOutputs, fullToolErrors, language))
337
+ .map((activity) => renderActivity(activity, renderText, localFileUrl, fullToolOutputs, fullToolErrors, language))
333
338
  .join("");
334
339
  }
335
340
 
336
341
  function renderActivity(
337
342
  activity: Exclude<TurnActivity, { kind: "reasoning" }>,
343
+ renderText: (value: string) => string,
338
344
  localFileUrl?: (filePath: string) => string | undefined,
339
345
  fullToolOutputs?: Record<string, string>,
340
346
  fullToolErrors?: Record<string, string>,
@@ -342,16 +348,16 @@ function renderActivity(
342
348
  ): string {
343
349
  switch (activity.kind) {
344
350
  case "assistant":
345
- return renderMessageActivity(activity.id, activity.text, "commentary");
351
+ return renderMessageActivity(activity.id, activity.text, "commentary", renderText);
346
352
  case "user":
347
- return renderMessageActivity(activity.id, activity.text, "user");
353
+ return renderMessageActivity(activity.id, activity.text, "user", renderText);
348
354
  case "tool":
349
355
  return renderToolActivity(activity.id, activity.tool, localFileUrl, fullToolOutputs, fullToolErrors, language);
350
356
  }
351
357
  }
352
358
 
353
- function renderMessageActivity(id: string, text: string, kind: string): string {
354
- return `<article class="activity message ${kind}" data-activity="${escapeAttribute(id)}"><div class="markdown">${renderMarkdown(text)}</div></article>`;
359
+ function renderMessageActivity(id: string, text: string, kind: string, renderText: (value: string) => string): string {
360
+ return `<article class="activity message ${kind}" data-activity="${escapeAttribute(id)}"><div class="markdown">${renderText(text)}</div></article>`;
355
361
  }
356
362
 
357
363
  function renderToolActivity(
@@ -608,8 +614,16 @@ function previewTitle(state: TurnViewState): string {
608
614
  return value.length > 80 ? `${value.slice(0, 79)}…` : value;
609
615
  }
610
616
 
611
- function renderMarkdown(value: string): string {
612
- return MARKDOWN.render(value);
617
+ function renderMarkdown(value: string, projectCwd?: string, localFileUrl?: (filePath: string) => string | undefined): string {
618
+ const environment = {};
619
+ const tokens = MARKDOWN.parse(value, environment);
620
+ // Only the directory is used to resolve relative links; no Markdown file is created.
621
+ const markdownPath = projectCwd && path.isAbsolute(projectCwd) ? path.join(projectCwd, "turn.md") : undefined;
622
+ rewriteMarkdownFileLinks(tokens, markdownPath, (filePath) => {
623
+ const url = localFileUrl?.(filePath);
624
+ return url ? new URL(url) : undefined;
625
+ });
626
+ return MARKDOWN.renderer.render(tokens, MARKDOWN.options, environment);
613
627
  }
614
628
 
615
629
  function statusLabel(status: TurnViewStatus, language: TurnPreviewLanguage): string {
@@ -673,7 +687,7 @@ function escapeAttribute(value: string): string {
673
687
  }
674
688
 
675
689
  const TURN_PREVIEW_CSS = `
676
- :root { color-scheme: light dark; font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; --page: #fff; --surface: #f5f6f7; --code: #fafafa; --text: #25272b; --muted: #62666d; --line: #e0e2e5; --accent: #245bc0; --success: #257449; --danger: #b52c34; }
690
+ :root { color-scheme: light dark; font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; --page: #fff; --surface: #f5f6f7; --code: #fafafa; --text: #25272b; --muted: #62666d; --line: #e0e2e5; --accent: #245bc0; --success: #257449; --danger: #b52c34; --scrollbar-thumb: #d4d7dc; --scrollbar-thumb-hover: #afb4bc; }
677
691
  * { box-sizing: border-box; letter-spacing: 0; }
678
692
  body { margin: 0; background: var(--page); color: var(--text); font-size: 13px; line-height: 1.55; }
679
693
  .page-header { position: sticky; top: 0; z-index: 3; background: var(--page); border-bottom: 1px solid var(--line); }
@@ -711,7 +725,7 @@ main { max-width: 1320px; margin: 0 auto; padding: 0 20px 40px; }
711
725
  .activity { min-width: 0; }
712
726
  .message { padding: 12px 0; border-bottom: 1px solid var(--line); }
713
727
  .markdown { min-width: 0; overflow-wrap: anywhere; }
714
- .commentary > .markdown { font-weight: 700; }
728
+ .message > .markdown, .result > .markdown { font-size: 14px; font-weight: 400; }
715
729
  .markdown > :first-child { margin-top: 0; }
716
730
  .markdown > :last-child { margin-bottom: 0; }
717
731
  .markdown p, .markdown ul, .markdown ol, .markdown pre, .markdown blockquote { margin: 0 0 6px; }
@@ -734,7 +748,22 @@ summary:hover { background: transparent; }
734
748
  summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
735
749
  .tool { margin: 8px 0; }
736
750
  .tool-step { border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: transparent; }
737
- .tool-header { display: flex; align-items: flex-start; gap: 8px; min-height: 32px; padding: 7px 10px; overflow-x: auto; background: transparent; font-weight: 400; }
751
+ .tool-header { display: flex; align-items: flex-start; gap: 8px; min-height: 32px; max-height: calc(15 * 12px * 1.55 + 14px); padding: 7px 10px; overflow: auto; background: transparent; font-weight: 400; }
752
+ .tool-header, .tool-body pre { --tool-scrollbar-thumb: var(--scrollbar-thumb); scrollbar-width: thin; scrollbar-color: var(--tool-scrollbar-thumb) transparent; }
753
+ @media (hover: hover) and (pointer: fine) {
754
+ .tool-header, .tool-body pre { --tool-scrollbar-thumb: transparent; }
755
+ .tool-header:is(:hover, :focus-visible, :focus-within),
756
+ .tool-body pre:is(:hover, :focus-visible, :focus-within) { --tool-scrollbar-thumb: var(--scrollbar-thumb); }
757
+ }
758
+ @supports selector(::-webkit-scrollbar) {
759
+ .tool-header, .tool-body pre { scrollbar-width: auto; scrollbar-color: auto; }
760
+ :is(.tool-header, .tool-body pre)::-webkit-scrollbar { width: 6px; height: 6px; }
761
+ :is(.tool-header, .tool-body pre)::-webkit-scrollbar-track,
762
+ :is(.tool-header, .tool-body pre)::-webkit-scrollbar-corner { background: transparent; }
763
+ :is(.tool-header, .tool-body pre)::-webkit-scrollbar-thumb { background: var(--tool-scrollbar-thumb); border-radius: 3px; }
764
+ :is(.tool-header, .tool-body pre)::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover); }
765
+ :is(.tool-header, .tool-body pre)::-webkit-scrollbar-button { display: none; width: 0; height: 0; }
766
+ }
738
767
  .tool-state { display: inline-flex; flex: none; align-items: center; justify-content: center; color: var(--muted); font-size: 11px; font-weight: 650; white-space: nowrap; }
739
768
  .tool-state.completed { color: var(--success); }
740
769
  .tool-state.running { color: var(--accent); }
@@ -786,7 +815,7 @@ summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px;
786
815
  pre { padding: 8px 10px; }
787
816
  }
788
817
  @media (prefers-color-scheme: dark) {
789
- :root { --page: #18191b; --surface: #232427; --code: #1d1e20; --text: #e0e1e4; --muted: #a0a3ab; --line: #35373c; --accent: #8aaff7; --success: #83c69c; --danger: #f58c94; }
818
+ :root { --page: #18191b; --surface: #232427; --code: #1d1e20; --text: #e0e1e4; --muted: #a0a3ab; --line: #35373c; --accent: #8aaff7; --success: #83c69c; --danger: #f58c94; --scrollbar-thumb: #45494f; --scrollbar-thumb-hover: #60666e; }
790
819
  .notice, .status.waiting_for_approval { color: #e1bc6f; }
791
820
  }
792
821
  `;