miantiao.me 0.1.0 → 0.1.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # miantiao.me
2
2
 
3
- 在终端浏览面条的文章、项目和标签,也可以直接向 AI 助手提问。界面使用 assistant-ui Ink,与 [miantiao.me](https://miantiao.me) 的主题、提示符和命令保持一致。
3
+ 在终端浏览面条的文章、项目和标签,也可以直接向 AI 助手提问。界面使用 assistant-ui Ink:铺满终端、底部输入框和独立滚动的内容区,与 [miantiao.me](https://miantiao.me) 的主题、欢迎页和会话块保持一致。
4
4
 
5
5
  ```sh
6
6
  npx miantiao.me
@@ -8,7 +8,7 @@ npx miantiao.me
8
8
 
9
9
  需要 Node.js 22.14 或更新版本,以及交互式终端。文章、项目、首页、关于和搜索索引从官网读取;官网部署后,下一次启动、导航或 `/refresh` 就能读取更新内容。已打开的终端无需重装包。TUI 功能升级仍通过 npm 版本发布。
10
10
 
11
- 输入普通文字可向 AI 助手提问。斜杠命令在本地执行:
11
+ 输入普通文字可向 AI 助手提问。输入 `/` 展开命令面板,可按命令名或中文说明筛选,用 ↑↓ 选择、Enter 确认、Esc 关闭。选择 `/search` 或 `/read` 后继续填写参数。斜杠命令在本地执行:
12
12
 
13
13
  | 命令 | 用途 |
14
14
  | ------------------------------------ | -------------------------- |
@@ -24,7 +24,7 @@ npx miantiao.me
24
24
  | `/clear` | 清空 AI 对话上下文 |
25
25
  | `/quit` | 退出 |
26
26
 
27
- Tab 在输入框和列表之间切换;↑↓ 选择条目,Enter 打开;Esc 取消加载或停止回复,Ctrl+C 退出。编号对应当前页面的结果列表,可用于 `/read 1` 或 `/open 1`。已完成的内容保存在终端滚动记录中。`/clear` 重置 AI 会话,终端已有的滚动记录仍可查看。
27
+ PgUp/PgDn 滚动正文,输入框始终固定在底部。Tab 在输入框和内容区之间切换;内容区按 `/` 可返回输入框打开命令面板。列表中用 ↑↓ 选择、Enter 打开,文章中用 ↑↓、Home、End 滚动。Esc 取消加载或停止回复,Ctrl+C 退出。编号对应当前页面的结果列表,可用于 `/read 1` 或 `/open 1`。`/clear` 清空 AI 会话。退出后恢复原终端画面。
28
28
 
29
29
  支持 GrokNight、GrokDay、TokyoNight、RosePine Moon、Oscura Midnight 和 `auto`。自动模式参考终端的 `COLORFGBG`;未提供时使用 GrokNight。可用 `/theme light` 或 `/theme dark` 显式选择。
30
30
 
@@ -4,8 +4,8 @@ import {
4
4
 
5
5
  // packages/cli/src/app.tsx
6
6
  import { AssistantRuntimeProvider } from "@assistant-ui/react-ink";
7
- import { Box as Box3, render, Text as Text3, useApp as useApp2, useInput as useInput3, useStdout } from "ink";
8
- import { useCallback as useCallback3, useEffect as useEffect4, useState as useState4, useSyncExternalStore } from "react";
7
+ import { Box as Box9, render, Text as Text9, useApp as useApp2, useInput as useInput6, useWindowSize as useWindowSize3 } from "ink";
8
+ import { useEffect as useEffect5, useState as useState6 } from "react";
9
9
 
10
10
  // src/terminal/schema.ts
11
11
  import { z } from "zod";
@@ -52,12 +52,22 @@ var terminalIndexSchema = z.object({
52
52
  tags: z.array(z.object({ name: z.string(), path: z.string(), count: z.number().int().nonnegative() })),
53
53
  searchIndexUrl: z.string()
54
54
  });
55
+ var sectionSchema = z.discriminatedUnion("kind", [
56
+ z.object({
57
+ kind: z.literal("welcome"),
58
+ markdown: z.string(),
59
+ logo: z.string().optional(),
60
+ menu: z.array(linkSchema.extend({ command: z.string() })).optional()
61
+ }),
62
+ z.object({ kind: z.literal("turn"), prompt: z.string(), markdown: z.string() })
63
+ ]);
55
64
  var terminalDocumentSchema = z.object({
56
65
  schemaVersion: z.literal(1),
57
66
  revision: z.string().min(1),
58
67
  path: z.string(),
59
68
  title: z.string(),
60
- markdown: z.string().min(1)
69
+ markdown: z.string().min(1),
70
+ sections: z.array(sectionSchema).optional()
61
71
  });
62
72
 
63
73
  // src/utils/search-core.ts
@@ -276,49 +286,12 @@ var TerminalClient = class {
276
286
  };
277
287
 
278
288
  // packages/cli/src/console.tsx
279
- import {
280
- ComposerPrimitive,
281
- LoadingPrimitive,
282
- ThreadPrimitive as ThreadPrimitive2,
283
- useAui,
284
- useAuiState as useAuiState2
285
- } from "@assistant-ui/react-ink";
286
- import { Box as Box2, Static, Text as Text2, useFocus, useInput } from "ink";
287
- import { useState } from "react";
288
-
289
- // src/agent/commands.ts
290
- var commands = [
291
- { id: "help", description: "\u67E5\u770B\u53EF\u7528\u547D\u4EE4" },
292
- { id: "theme", args: "[\u4E3B\u9898]", description: "\u5207\u6362\u4E3B\u9898\uFF0C\u4E0D\u5E26\u53C2\u6570\u65F6\u6253\u5F00\u9009\u62E9\u5668" },
293
- { id: "clear", description: "\u6E05\u7A7A\u5F53\u524D\u5BF9\u8BDD" },
294
- { id: "home", description: "\u6253\u5F00\u9996\u9875" },
295
- { id: "blog", description: "\u6D4F\u89C8\u5168\u90E8\u6587\u7AE0" },
296
- { id: "projects", description: "\u67E5\u770B\u6298\u817E\u7684\u9879\u76EE" },
297
- { id: "tags", description: "\u6309\u6807\u7B7E\u6D4F\u89C8\u6587\u7AE0" },
298
- { id: "search", args: "[\u5173\u952E\u8BCD]", description: "\u641C\u7D22\u6587\u7AE0" },
299
- { id: "about", description: "\u5173\u4E8E\u672C\u7AD9" },
300
- { id: "rss", description: "\u8BA2\u9605 RSS" }
301
- ];
302
- var pageCommands = {
303
- home: "/",
304
- blog: "/blog/",
305
- projects: "/projects/",
306
- tags: "/tags/",
307
- about: "/about/"
308
- };
309
- function parseCommand(input) {
310
- const match = input.trim().match(/^\/(\S+)(?:\s+([\s\S]*))?$/);
311
- if (!match)
312
- return null;
313
- return { name: match[1].toLowerCase(), args: (match[2] ?? "").trim() };
314
- }
315
- function getSearchPath(query) {
316
- return query ? `/search/?q=${encodeURIComponent(query)}` : "/search/";
317
- }
289
+ import { ThreadPrimitive as ThreadPrimitive2 } from "@assistant-ui/react-ink";
290
+ import { useRef as useRef5 } from "react";
318
291
 
319
292
  // packages/cli/src/chat.tsx
320
293
  import { ThreadPrimitive, useAuiState } from "@assistant-ui/react-ink";
321
- import { Box, Text } from "ink";
294
+ import { Box as Box4, Text as Text4 } from "ink";
322
295
 
323
296
  // packages/cli/src/theme.tsx
324
297
  import { createContext, useContext, useMemo } from "react";
@@ -423,43 +396,225 @@ function useTerminalStyle() {
423
396
  if (!style) throw new Error("TerminalTheme is required");
424
397
  return style;
425
398
  }
426
- function Markdown({ text }) {
399
+ function Markdown({ text, width: availableWidth }) {
427
400
  const { width, markdownTheme, highlighter } = useTerminalStyle();
428
401
  return /* @__PURE__ */ jsx(
429
402
  MarkdownText,
430
403
  {
431
404
  text,
432
- width: Math.max(10, width - 6),
405
+ width: Math.max(10, availableWidth ?? width - 3),
433
406
  theme: markdownTheme,
434
407
  highlighter,
435
408
  codeWrap: true,
409
+ hyperlinks: true,
436
410
  tableTruncate: false
437
411
  }
438
412
  );
439
413
  }
440
414
 
441
- // packages/cli/src/chat.tsx
415
+ // packages/cli/src/blocks.tsx
416
+ import { Box as Box3, Text as Text3, useBoxMetrics as useBoxMetrics3 } from "ink";
417
+ import { useRef as useRef3 } from "react";
418
+
419
+ // packages/cli/src/page-list.tsx
420
+ import { Box as Box2, Text as Text2, useBoxMetrics as useBoxMetrics2, useInput as useInput2 } from "ink";
421
+ import { useEffect, useRef as useRef2, useState as useState2 } from "react";
422
+
423
+ // packages/cli/src/scroll-area.tsx
424
+ import {
425
+ Box,
426
+ Text,
427
+ measureElement,
428
+ useBoxMetrics,
429
+ useFocus,
430
+ useInput
431
+ } from "ink";
432
+ import {
433
+ createContext as createContext2,
434
+ useContext as useContext2,
435
+ useImperativeHandle,
436
+ useMemo as useMemo2,
437
+ useRef,
438
+ useState
439
+ } from "react";
442
440
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
441
+ var ScrollContext = createContext2(null);
442
+ function useScrollArea() {
443
+ const value = useContext2(ScrollContext);
444
+ if (!value) throw new Error("ScrollArea is required");
445
+ return value;
446
+ }
447
+ function ScrollArea({
448
+ children,
449
+ selection,
450
+ ref
451
+ }) {
452
+ const { colors } = useTerminalStyle();
453
+ const { isFocused } = useFocus({ id: "content" });
454
+ const viewport = useRef(null);
455
+ const content = useRef(null);
456
+ const { clientHeight: height } = useBoxMetrics(viewport);
457
+ const { height: contentHeight } = useBoxMetrics(content);
458
+ const [anchor, setAnchor] = useState(0);
459
+ const maximum = Math.max(0, contentHeight - height);
460
+ const offset = anchor === "end" ? maximum : Math.min(anchor, maximum);
461
+ const context = useMemo2(() => ({ focused: isFocused, height }), [isFocused, height]);
462
+ useImperativeHandle(ref, () => ({
463
+ end: () => setAnchor("end"),
464
+ reveal: (element) => {
465
+ if (!viewport.current) return false;
466
+ const item = measureElement(element);
467
+ const top = item.y - measureElement(viewport.current).y;
468
+ const visible = top >= offset && top < offset + height;
469
+ if (top < offset || item.height > height) setAnchor(Math.max(0, top));
470
+ else if (top + item.height > offset + height)
471
+ setAnchor(Math.min(maximum, top + item.height - height));
472
+ return visible;
473
+ }
474
+ }), [offset, height, maximum]);
475
+ useInput((_input, key) => {
476
+ let next = offset;
477
+ if (key.pageUp) next -= Math.max(1, height - 1);
478
+ else if (key.pageDown) next += Math.max(1, height - 1);
479
+ else if (isFocused && !selection && key.upArrow) next--;
480
+ else if (isFocused && !selection && key.downArrow) next++;
481
+ else if (isFocused && !selection && key.home) next = 0;
482
+ else if (isFocused && !selection && key.end) next = maximum;
483
+ else return;
484
+ setAnchor(next >= maximum ? "end" : Math.max(0, next));
485
+ });
486
+ const track = Math.max(0, height);
487
+ const thumbSize = Math.min(track, Math.max(1, Math.round(height * height / Math.max(1, contentHeight))));
488
+ const thumbTop = maximum > 0 ? Math.round(offset / maximum * (height - thumbSize)) : 0;
489
+ const before = Array(thumbTop).fill("\u2502").join("\n");
490
+ const thumb = Array(thumbSize).fill("\u2503").join("\n");
491
+ const after = Array(Math.max(0, track - thumbTop - thumbSize)).fill("\u2502").join("\n");
492
+ return /* @__PURE__ */ jsx2(ScrollContext, { value: context, children: /* @__PURE__ */ jsxs(Box, { flexGrow: 1, flexShrink: 1, minHeight: 1, marginX: 2, children: [
493
+ /* @__PURE__ */ jsx2(
494
+ Box,
495
+ {
496
+ ref: viewport,
497
+ flexDirection: "column",
498
+ flexGrow: 1,
499
+ flexShrink: 1,
500
+ overflow: "hidden",
501
+ contentOffsetY: offset,
502
+ children: /* @__PURE__ */ jsx2(Box, { ref: content, flexDirection: "column", flexShrink: 0, width: "100%", children })
503
+ }
504
+ ),
505
+ /* @__PURE__ */ jsx2(Box, { width: 1, flexShrink: 0, "aria-hidden": true, children: /* @__PURE__ */ jsxs(Text, { color: colors.line, children: [
506
+ before,
507
+ before && "\n",
508
+ /* @__PURE__ */ jsx2(Text, { color: isFocused ? colors.accent : colors.muted, children: thumb }),
509
+ after && "\n",
510
+ after
511
+ ] }) })
512
+ ] }) });
513
+ }
514
+
515
+ // packages/cli/src/page-list.tsx
516
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
517
+ function PageList({ items, onSelect, scroll, compact = false }) {
518
+ const { focused } = useScrollArea();
519
+ const { colors } = useTerminalStyle();
520
+ const [selected, setSelected] = useState2(0);
521
+ const elements = useRef2(/* @__PURE__ */ new Map());
522
+ const container = useRef2(null);
523
+ const { hasMeasured } = useBoxMetrics2(container);
524
+ useEffect(() => {
525
+ const element = elements.current.get(items[selected].path);
526
+ if (focused && hasMeasured && element) scroll.current?.reveal(element);
527
+ }, [focused, hasMeasured, selected, items, scroll]);
528
+ useInput2((_input, key) => {
529
+ let next = selected;
530
+ if (key.upArrow) next--;
531
+ else if (key.downArrow) next++;
532
+ else if (key.home) next = 0;
533
+ else if (key.end) next = items.length - 1;
534
+ else if (key.return) {
535
+ const item = items[selected];
536
+ const element = elements.current.get(item.path);
537
+ if (element && scroll.current?.reveal(element) === false) return;
538
+ onSelect(item.path, item.external);
539
+ return;
540
+ } else return;
541
+ next = Math.max(0, Math.min(items.length - 1, next));
542
+ setSelected(next);
543
+ }, { isActive: focused });
544
+ return /* @__PURE__ */ jsx3(Box2, { ref: container, flexDirection: "column", marginTop: 1, children: items.map((item, index) => {
545
+ const active = focused && index === selected;
546
+ return /* @__PURE__ */ jsxs2(
547
+ Box2,
548
+ {
549
+ flexDirection: compact ? "row" : "column",
550
+ justifyContent: compact ? "space-between" : void 0,
551
+ marginBottom: compact ? 0 : 1,
552
+ backgroundColor: active ? colors.band : void 0,
553
+ ref: (element) => {
554
+ if (element) elements.current.set(item.path, element);
555
+ else elements.current.delete(item.path);
556
+ },
557
+ children: [
558
+ /* @__PURE__ */ jsxs2(Text2, { color: active ? colors.accent : colors.fg, bold: active, children: [
559
+ active ? "\u25B8" : compact ? " " : "\u2022",
560
+ " ",
561
+ compact ? "" : `${index + 1}. `,
562
+ cleanText(item.title)
563
+ ] }),
564
+ item.description && /* @__PURE__ */ jsx3(Text2, { color: colors.muted, children: cleanText(item.description) })
565
+ ]
566
+ },
567
+ item.path
568
+ );
569
+ }) });
570
+ }
571
+
572
+ // packages/cli/src/blocks.tsx
573
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
574
+ function Prompt({ text, command = false }) {
575
+ const { colors } = useTerminalStyle();
576
+ return /* @__PURE__ */ jsxs3(Box3, { backgroundColor: colors.band, paddingX: 2, children: [
577
+ /* @__PURE__ */ jsx4(Text3, { color: colors.prompt, children: "\u276F " }),
578
+ /* @__PURE__ */ jsx4(Text3, { color: command ? colors.skill : colors.fg, children: cleanText(text) })
579
+ ] });
580
+ }
581
+ function Welcome({ section, ...selection }) {
582
+ const { colors, width } = useTerminalStyle();
583
+ const content = useRef3(null);
584
+ const { clientWidth } = useBoxMetrics3(content);
585
+ return /* @__PURE__ */ jsxs3(Box3, { borderStyle: "round", borderColor: colors.line, paddingX: 2, paddingY: 1, gap: 4, children: [
586
+ section.logo && width >= 90 && /* @__PURE__ */ jsx4(Text3, { color: colors.skill, children: cleanText(section.logo) }),
587
+ /* @__PURE__ */ jsxs3(Box3, { ref: content, flexDirection: "column", flexGrow: 1, flexShrink: 1, minWidth: 0, children: [
588
+ /* @__PURE__ */ jsx4(Markdown, { text: section.markdown, width: clientWidth || width - 6 }),
589
+ section.menu && selection.items.length > 0 && /* @__PURE__ */ jsx4(PageList, { ...selection, compact: true })
590
+ ] })
591
+ ] });
592
+ }
593
+ function HomeSections({ page, ...selection }) {
594
+ return /* @__PURE__ */ jsx4(Box3, { flexDirection: "column", gap: 1, children: page.sections?.map((section) => section.kind === "welcome" ? /* @__PURE__ */ jsx4(Welcome, { section, items: page.items, ...selection }, "welcome") : /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", gap: 1, children: [
595
+ /* @__PURE__ */ jsx4(Prompt, { text: section.prompt }),
596
+ /* @__PURE__ */ jsx4(Box3, { flexDirection: "column", paddingLeft: 2, paddingRight: 1, children: /* @__PURE__ */ jsx4(Markdown, { text: section.markdown }) })
597
+ ] }, section.prompt)) });
598
+ }
599
+
600
+ // packages/cli/src/chat.tsx
601
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
443
602
  var toolTitles = { set_theme: "Theme", open_page: "Open" };
444
603
  function MessageBody({ message }) {
445
604
  const { colors } = useTerminalStyle();
446
605
  if (message.role === "user") {
447
- return /* @__PURE__ */ jsx2(Box, { marginY: 1, children: /* @__PURE__ */ jsxs(Text, { color: colors.prompt, children: [
448
- "\u276F",
449
- " ",
450
- message.content.filter((part) => part.type === "text").map((part) => cleanText(part.text)).join("\n")
451
- ] }) });
606
+ return /* @__PURE__ */ jsx5(Box4, { marginY: 1, children: /* @__PURE__ */ jsx5(Prompt, { text: message.content.filter((part) => part.type === "text").map((part) => part.text).join("\n") }) });
452
607
  }
453
608
  if (message.role !== "assistant") return null;
454
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingLeft: 2, marginBottom: 1, children: [
609
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", paddingLeft: 2, marginBottom: 1, children: [
455
610
  message.content.map((part, index) => {
456
611
  if (part.type === "text")
457
- return /* @__PURE__ */ jsx2(Markdown, { text: part.text }, index);
612
+ return /* @__PURE__ */ jsx5(Markdown, { text: part.text }, index);
458
613
  if (part.type === "reasoning") {
459
614
  const label = message.status.type === "running" ? "\u601D\u8003\u4E2D\u2026" : "\u601D\u8003";
460
615
  return (
461
616
  // eslint-disable-next-line react-doctor/no-array-index-as-key
462
- /* @__PURE__ */ jsxs(Text, { color: colors.muted, children: [
617
+ /* @__PURE__ */ jsxs4(Text4, { color: colors.muted, children: [
463
618
  "\u25C6 ",
464
619
  label
465
620
  ] }, index)
@@ -467,7 +622,7 @@ function MessageBody({ message }) {
467
622
  }
468
623
  if (part.type === "tool-call") {
469
624
  const title = toolTitles[part.toolName] ?? part.toolName;
470
- return /* @__PURE__ */ jsxs(Text, { color: colors.skill, children: [
625
+ return /* @__PURE__ */ jsxs4(Text4, { color: colors.skill, children: [
471
626
  "\u25C6 ",
472
627
  cleanText(title),
473
628
  " ",
@@ -476,144 +631,346 @@ function MessageBody({ message }) {
476
631
  }
477
632
  return null;
478
633
  }),
479
- message.status.type === "incomplete" && message.status.reason === "error" && /* @__PURE__ */ jsx2(Text, { color: colors.err, children: "\u25C6 AI \u6682\u65F6\u65E0\u6CD5\u56DE\u590D\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002" })
634
+ message.status.type === "incomplete" && message.status.reason === "error" && /* @__PURE__ */ jsx5(Text4, { color: colors.err, children: "\u25C6 AI \u6682\u65F6\u65E0\u6CD5\u56DE\u590D\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002" })
480
635
  ] });
481
636
  }
482
- function LiveMessage() {
637
+ function Message() {
483
638
  const message = useAuiState((state) => state.message);
484
- return message.role === "assistant" && message.status.type === "running" ? /* @__PURE__ */ jsx2(MessageBody, { message }) : null;
639
+ return /* @__PURE__ */ jsx5(MessageBody, { message });
485
640
  }
486
641
  function ChatThread() {
487
- return /* @__PURE__ */ jsx2(ThreadPrimitive.Messages, { components: { Message: LiveMessage } });
642
+ return /* @__PURE__ */ jsx5(ThreadPrimitive.Messages, { components: { Message } });
488
643
  }
489
644
 
490
- // packages/cli/src/console.tsx
491
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
492
- var pageSize = 8;
493
- function PageOutput({ page, prompt }) {
494
- const { colors } = useTerminalStyle();
495
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginY: 1, children: [
496
- /* @__PURE__ */ jsxs2(Text2, { color: colors.prompt, children: [
497
- "\u276F ",
498
- cleanText(prompt)
499
- ] }),
500
- /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingLeft: 2, marginTop: 1, children: [
501
- /* @__PURE__ */ jsxs2(Text2, { color: colors.skill, children: [
502
- "\u25C6 ",
503
- page.verb,
504
- " ",
505
- /* @__PURE__ */ jsx3(Text2, { color: colors.path, children: cleanText(page.title) })
506
- ] }),
507
- page.markdown && /* @__PURE__ */ jsx3(Markdown, { text: page.markdown })
508
- ] })
509
- ] });
645
+ // packages/cli/src/composer.tsx
646
+ import { ComposerPrimitive, LoadingPrimitive, useAui, useAuiState as useAuiState2 } from "@assistant-ui/react-ink";
647
+ import { Box as Box6, Text as Text6, useFocusManager, useInput as useInput4 } from "ink";
648
+ import { useEffect as useEffect2, useRef as useRef4 } from "react";
649
+
650
+ // src/agent/appearance.ts
651
+ var composerPlaceholder = "\u95EE\u70B9\u4EC0\u4E48\uFF0C\u6216\u8F93\u5165 / \u67E5\u770B\u547D\u4EE4";
652
+ var composerBrand = { mark: "<MT/>", model: "V3.14 Pro" };
653
+
654
+ // src/agent/commands.ts
655
+ var commands = [
656
+ { id: "help", description: "\u67E5\u770B\u53EF\u7528\u547D\u4EE4" },
657
+ { id: "theme", args: "[\u4E3B\u9898]", description: "\u5207\u6362\u4E3B\u9898\uFF0C\u4E0D\u5E26\u53C2\u6570\u65F6\u6253\u5F00\u9009\u62E9\u5668" },
658
+ { id: "clear", description: "\u6E05\u7A7A\u5F53\u524D\u5BF9\u8BDD" },
659
+ { id: "home", description: "\u6253\u5F00\u9996\u9875" },
660
+ { id: "blog", description: "\u6D4F\u89C8\u5168\u90E8\u6587\u7AE0" },
661
+ { id: "projects", description: "\u67E5\u770B\u6298\u817E\u7684\u9879\u76EE" },
662
+ { id: "tags", description: "\u6309\u6807\u7B7E\u6D4F\u89C8\u6587\u7AE0" },
663
+ { id: "search", args: "[\u5173\u952E\u8BCD]", description: "\u641C\u7D22\u6587\u7AE0" },
664
+ { id: "about", description: "\u5173\u4E8E\u672C\u7AD9" },
665
+ { id: "rss", description: "\u8BA2\u9605 RSS" }
666
+ ];
667
+ var pageCommands = {
668
+ home: "/",
669
+ blog: "/blog/",
670
+ projects: "/projects/",
671
+ tags: "/tags/",
672
+ about: "/about/"
673
+ };
674
+ function parseCommand(input) {
675
+ const match = input.trim().match(/^\/(\S+)(?:\s+([\s\S]*))?$/);
676
+ if (!match)
677
+ return null;
678
+ return { name: match[1].toLowerCase(), args: (match[2] ?? "").trim() };
679
+ }
680
+ function getSearchPath(query) {
681
+ return query ? `/search/?q=${encodeURIComponent(query)}` : "/search/";
510
682
  }
511
- function PageList({ page, onSelect }) {
512
- const { isFocused } = useFocus();
683
+
684
+ // packages/cli/src/command-menu.tsx
685
+ import { Box as Box5, Text as Text5, useInput as useInput3, useWindowSize } from "ink";
686
+ import { useImperativeHandle as useImperativeHandle2, useState as useState3 } from "react";
687
+
688
+ // packages/cli/src/commands.ts
689
+ var terminalCommands = [
690
+ ...commands.map((command) => command.id === "tags" ? { ...command, args: "[\u6807\u7B7E]" } : command),
691
+ { id: "read", args: "<\u7F16\u53F7\u3001slug \u6216\u8DEF\u5F84>", description: "\u9605\u8BFB\u6587\u7AE0\u5168\u6587" },
692
+ { id: "open", args: "[\u7F16\u53F7\u6216 URL]", description: "\u5728\u6D4F\u89C8\u5668\u6253\u5F00\u5F53\u524D\u9875\u9762\u6216\u94FE\u63A5" },
693
+ { id: "refresh", description: "\u4ECE\u5B98\u7F51\u5237\u65B0\u5F53\u524D\u9875\u9762" },
694
+ { id: "quit", description: "\u9000\u51FA" }
695
+ ];
696
+ function getCommandSuggestions(text) {
697
+ const match = text.match(/^\/([^\s]*)$/);
698
+ if (!match) return null;
699
+ const query = match[1].toLowerCase();
700
+ return terminalCommands.filter((command) => command.id.startsWith(query) || command.description.includes(query));
701
+ }
702
+ function numberedItem(input, page) {
703
+ if (!/^\d+$/.test(input)) return void 0;
704
+ const item = page.items[Number(input) - 1];
705
+ if (!item) throw new Error("\u5F53\u524D\u5217\u8868\u6CA1\u6709\u8FD9\u4E2A\u7F16\u53F7\uFF0C\u8BF7\u4F7F\u7528\u5217\u8868\u4E2D\u663E\u793A\u7684\u7F16\u53F7\u3002");
706
+ return item;
707
+ }
708
+ function resolveReadPath(input, page) {
709
+ if (!input) throw new Error("\u8F93\u5165 /read <\u7F16\u53F7\u3001slug \u6216\u7AD9\u5185\u8DEF\u5F84> \u9605\u8BFB\u6587\u7AE0\u3002");
710
+ const item = numberedItem(input, page);
711
+ if (item) {
712
+ if (item.external || !item.path.startsWith("/posts/"))
713
+ throw new Error("\u8FD9\u4E2A\u7F16\u53F7\u4E0D\u662F\u6587\u7AE0\uFF0C\u8BF7\u4F7F\u7528 /blog \u6216 /search \u67E5\u627E\u6587\u7AE0\u3002");
714
+ return item.path;
715
+ }
716
+ return input.startsWith("/") ? input : `/posts/${input.replace(/\/$/, "")}/`;
717
+ }
718
+ function resolveOpenTarget(input, page) {
719
+ return numberedItem(input, page)?.path ?? (input || page.path);
720
+ }
721
+ function resolveTagPath(input, index) {
722
+ const tag = index.tags.find((tag2) => tag2.name === input);
723
+ if (!tag) throw new Error("\u627E\u4E0D\u5230\u8FD9\u4E2A\u6807\u7B7E\uFF0C\u8BF7\u4F7F\u7528 /tags \u67E5\u770B\u53EF\u7528\u6807\u7B7E\u3002");
724
+ return tag.path;
725
+ }
726
+
727
+ // packages/cli/src/command-menu.tsx
728
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
729
+ function CommandMenu({
730
+ text,
731
+ focused,
732
+ onSelect,
733
+ ref
734
+ }) {
513
735
  const { colors } = useTerminalStyle();
514
- const [selected, setSelected] = useState(0);
515
- const start = Math.floor(selected / pageSize) * pageSize;
516
- useInput(
517
- (_input, key) => {
518
- if (key.upArrow) setSelected((value) => Math.max(0, value - 1));
519
- if (key.downArrow) setSelected((value) => Math.min(page.items.length - 1, value + 1));
520
- if (key.return) {
521
- const item = page.items[selected];
522
- if (item) onSelect(item.path, item.external);
523
- }
736
+ const { rows } = useWindowSize();
737
+ const [selected, setSelected] = useState3(0);
738
+ const [dismissed, setDismissed] = useState3(false);
739
+ const suggestions = getCommandSuggestions(text);
740
+ const matches = suggestions ?? [];
741
+ const open = focused && suggestions !== null && !dismissed;
742
+ const visibleCount = Math.max(1, Math.min(8, rows - 10));
743
+ const start = Math.max(0, selected - visibleCount + 1);
744
+ useImperativeHandle2(ref, () => ({
745
+ submit: (input) => {
746
+ if (!open || input !== text || !matches.length) return false;
747
+ onSelect(matches[selected]);
748
+ return true;
524
749
  },
525
- { isActive: isFocused }
526
- );
527
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingLeft: 2, marginBottom: 1, children: [
528
- page.items.slice(start, start + pageSize).map((item, offset) => {
529
- const active = isFocused && start + offset === selected;
530
- return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate-end", color: active ? colors.accent : colors.fg, children: [
531
- active ? "\u25B8" : "\u2022",
532
- " ",
533
- start + offset + 1,
534
- ". ",
535
- cleanText(item.title),
536
- item.description && /* @__PURE__ */ jsxs2(Text2, { color: colors.muted, children: [
537
- "\n ",
538
- cleanText(item.description)
539
- ] })
540
- ] }, item.path);
750
+ dismiss: () => {
751
+ if (!open) return false;
752
+ setDismissed(true);
753
+ return true;
754
+ }
755
+ }));
756
+ useInput3((_input, key) => {
757
+ if (!matches.length) return;
758
+ const direction = Number(key.downArrow) - Number(key.upArrow);
759
+ if (direction) setSelected((index) => (index + direction + matches.length) % matches.length);
760
+ }, { isActive: open });
761
+ if (!open) return null;
762
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", borderStyle: "round", borderColor: colors.line, paddingX: 1, marginBottom: 1, children: [
763
+ matches.length === 0 && /* @__PURE__ */ jsx6(Text5, { color: colors.muted, children: "\u6CA1\u6709\u5339\u914D\u7684\u547D\u4EE4" }),
764
+ matches.slice(start, start + visibleCount).map((command, index) => {
765
+ const active = start + index === selected;
766
+ return /* @__PURE__ */ jsxs5(Box5, { height: 1, backgroundColor: active ? colors.band : void 0, children: [
767
+ /* @__PURE__ */ jsx6(Box5, { width: 14, flexShrink: 0, children: /* @__PURE__ */ jsxs5(Text5, { color: active ? colors.accent : colors.skill, bold: active, children: [
768
+ active ? "\u25B8" : " ",
769
+ " /",
770
+ command.id
771
+ ] }) }),
772
+ /* @__PURE__ */ jsx6(Box5, { flexGrow: 1, flexShrink: 1, children: /* @__PURE__ */ jsx6(Text5, { color: colors.muted, wrap: "truncate-end", children: command.description }) })
773
+ ] }, command.id);
541
774
  }),
542
- /* @__PURE__ */ jsxs2(Text2, { color: colors.muted, children: [
543
- start + 1,
544
- "\u2013",
545
- Math.min(start + pageSize, page.items.length),
546
- " / ",
547
- page.items.length,
548
- " \xB7 Tab",
549
- " ",
550
- isFocused ? "\u56DE\u5230\u8F93\u5165" : "\u9009\u62E9\u5217\u8868"
551
- ] })
775
+ /* @__PURE__ */ jsx6(Text5, { color: colors.muted, wrap: "truncate-end", children: "\u2191\u2193 \u9009\u62E9 \xB7 Enter \u786E\u8BA4 \xB7 Esc \u5173\u95ED" })
552
776
  ] });
553
777
  }
554
- function Console({
778
+
779
+ // packages/cli/src/composer.tsx
780
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
781
+ function Composer({
555
782
  runtime,
556
783
  page,
557
784
  revision,
558
- output,
559
785
  loading,
560
786
  notice,
561
787
  onCommand,
562
- onSelect
788
+ onCancel,
789
+ onSubmit
563
790
  }) {
564
791
  const { colors } = useTerminalStyle();
565
792
  const aui = useAui();
566
- const isRunning = useAuiState2((state) => state.thread.isRunning);
567
- return /* @__PURE__ */ jsxs2(ThreadPrimitive2.Root, { children: [
568
- /* @__PURE__ */ jsx3(Static, { items: output, children: (item) => "page" in item ? /* @__PURE__ */ jsx3(PageOutput, { page: item.page, prompt: item.prompt }, item.id) : /* @__PURE__ */ jsx3(MessageBody, { message: item.message }, item.id) }),
569
- page.items.length > 0 && /* @__PURE__ */ jsx3(PageList, { page, onSelect }, `${page.path}:${revision}`),
570
- /* @__PURE__ */ jsx3(ChatThread, {}),
571
- (loading || isRunning) && /* @__PURE__ */ jsxs2(Box2, { children: [
572
- /* @__PURE__ */ jsx3(LoadingPrimitive.Spinner, {}),
573
- /* @__PURE__ */ jsxs2(Text2, { color: colors.muted, children: [
574
- " ",
575
- loading ? "\u6B63\u5728\u8BFB\u53D6\u5B98\u7F51\u2026" : "\u56DE\u590D\u4E2D\u2026",
576
- " \xB7 Esc \u505C\u6B62"
577
- ] })
793
+ const { activeId, focus, focusNext } = useFocusManager();
794
+ const running = useAuiState2((state) => state.thread.isRunning);
795
+ const text = useAuiState2((state) => state.composer.text);
796
+ const menu = useRef4(null);
797
+ const inputFocusId = useRef4(void 0);
798
+ const focused = activeId !== "content";
799
+ useEffect2(() => {
800
+ if (activeId && activeId !== "content") inputFocusId.current = activeId;
801
+ }, [activeId]);
802
+ useInput4((input, key) => {
803
+ if (key.escape) {
804
+ if (!menu.current?.dismiss()) onCancel();
805
+ if (inputFocusId.current) focus(inputFocusId.current);
806
+ }
807
+ if (input === "/" && activeId === "content") {
808
+ focusNext();
809
+ aui.composer.setText("/");
810
+ }
811
+ });
812
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginX: 2, flexShrink: 0, children: [
813
+ /* @__PURE__ */ jsx7(
814
+ CommandMenu,
815
+ {
816
+ ref: menu,
817
+ text,
818
+ focused,
819
+ onSelect: (command) => {
820
+ if (command.id === "search" || command.id === "read") {
821
+ aui.composer.setText(`/${command.id} `);
822
+ } else {
823
+ aui.composer.setText("");
824
+ void onCommand(command.id, "");
825
+ }
826
+ }
827
+ },
828
+ text
829
+ ),
830
+ (loading || running || notice) && /* @__PURE__ */ jsxs6(Box6, { paddingLeft: 2, height: 1, overflow: "hidden", children: [
831
+ (loading || running) && /* @__PURE__ */ jsx7(LoadingPrimitive.Spinner, {}),
832
+ /* @__PURE__ */ jsx7(Text6, { color: notice ? colors.warn : colors.muted, wrap: "truncate-end", children: notice ? cleanText(notice) : ` ${loading ? "\u6B63\u5728\u8BFB\u53D6\u5B98\u7F51\u2026" : "\u56DE\u590D\u4E2D\u2026"} \xB7 Esc \u505C\u6B62` })
578
833
  ] }),
579
- notice && /* @__PURE__ */ jsx3(Text2, { color: colors.warn, children: cleanText(notice) }),
580
- /* @__PURE__ */ jsxs2(Box2, { borderStyle: "single", borderColor: colors.line, paddingX: 1, children: [
581
- /* @__PURE__ */ jsx3(Text2, { color: colors.prompt, children: "\u276F " }),
582
- /* @__PURE__ */ jsx3(
834
+ /* @__PURE__ */ jsxs6(Box6, { borderStyle: "round", borderColor: focused ? colors["line-strong"] : colors.line, paddingX: 1, children: [
835
+ /* @__PURE__ */ jsx7(Text6, { color: colors.prompt, children: "\u276F " }),
836
+ /* @__PURE__ */ jsx7(
583
837
  ComposerPrimitive.Input,
584
838
  {
585
839
  autoFocus: true,
586
840
  submitOnEnter: true,
587
841
  flexGrow: 1,
588
- placeholder: "\u95EE\u6211\u4EFB\u4F55\u95EE\u9898\uFF0C\u6216\u8F93\u5165 /help",
589
- onSubmit: (text) => {
590
- const input = text.trim();
842
+ placeholder: composerPlaceholder,
843
+ onSubmit: (text2) => {
844
+ const input = text2.trim();
591
845
  if (!input) return;
592
- const command = parseCommand(input);
593
- if (command) {
846
+ if (menu.current?.submit(text2)) return;
847
+ if (input.startsWith("/")) {
848
+ const command = parseCommand(input);
594
849
  aui.composer.setText("");
595
- void onCommand(command.name, command.args);
596
- } else if (!isRunning) runtime.thread.composer.send();
850
+ void onCommand(command?.name ?? "help", command?.args ?? "");
851
+ } else if (!running) {
852
+ onSubmit();
853
+ runtime.thread.composer.send();
854
+ }
597
855
  }
598
856
  },
599
- page.path
600
- )
857
+ `${page.path}:${revision}`
858
+ ),
859
+ /* @__PURE__ */ jsx7(Box6, { position: "absolute", bottom: -1, right: 1, children: /* @__PURE__ */ jsxs6(Text6, { color: colors.muted, backgroundColor: colors.bg, children: [
860
+ " ",
861
+ composerBrand.mark,
862
+ " \xB7 ",
863
+ composerBrand.model,
864
+ " "
865
+ ] }) })
601
866
  ] }),
602
- /* @__PURE__ */ jsxs2(Text2, { color: colors.muted, children: [
603
- cleanText(page.path),
604
- " \xB7 /help \xB7 Tab \u5207\u6362 \xB7 Ctrl+C \u9000\u51FA"
867
+ /* @__PURE__ */ jsxs6(Text6, { color: colors.muted, wrap: "truncate-end", children: [
868
+ /* @__PURE__ */ jsx7(Text6, { color: colors["fg-soft"], bold: true, children: "Tab" }),
869
+ ":\u5207\u6362 \u2502 ",
870
+ /* @__PURE__ */ jsx7(Text6, { bold: true, children: "PgUp/PgDn" }),
871
+ ":\u6EDA\u52A8 \u2502 ",
872
+ /* @__PURE__ */ jsx7(Text6, { bold: true, children: "/help" }),
873
+ " \u2502 Ctrl+C:\u9000\u51FA"
605
874
  ] })
606
875
  ] });
607
876
  }
608
877
 
878
+ // packages/cli/src/page-content.tsx
879
+ import { Box as Box7, Text as Text7 } from "ink";
880
+ import { memo } from "react";
881
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
882
+ var PageContent = memo(function PageContent2({
883
+ page,
884
+ prompt,
885
+ onSelect,
886
+ scroll
887
+ }) {
888
+ const { colors } = useTerminalStyle();
889
+ if (page.sections) return /* @__PURE__ */ jsx8(HomeSections, { page, onSelect, scroll });
890
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", marginBottom: 1, children: [
891
+ page.path !== "/" && /* @__PURE__ */ jsx8(Prompt, { text: prompt, command: true }),
892
+ /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", paddingLeft: 2, paddingRight: 1, paddingTop: 1, children: [
893
+ page.path !== "/" && /* @__PURE__ */ jsxs7(Text7, { children: [
894
+ /* @__PURE__ */ jsx8(Text7, { color: colors.skill, children: "\u25C6 " }),
895
+ /* @__PURE__ */ jsx8(Text7, { color: colors.fg, bold: true, children: page.verb }),
896
+ /* @__PURE__ */ jsxs7(Text7, { color: colors.path, children: [
897
+ " ",
898
+ cleanText(page.title)
899
+ ] })
900
+ ] }),
901
+ page.markdown && /* @__PURE__ */ jsx8(Markdown, { text: page.markdown }),
902
+ page.items.length > 0 && /* @__PURE__ */ jsx8(PageList, { items: page.items, onSelect, scroll })
903
+ ] })
904
+ ] });
905
+ });
906
+
907
+ // packages/cli/src/terminal-layout.tsx
908
+ import { Box as Box8, Text as Text8, useWindowSize as useWindowSize2 } from "ink";
909
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
910
+ function TerminalLayout({
911
+ path,
912
+ nav = [],
913
+ children
914
+ }) {
915
+ const { columns, rows } = useWindowSize2();
916
+ const { colors } = useTerminalStyle();
917
+ const directory = `~/miantiao.me${path === "/" ? "" : path.replace(/\/$/, "")}`;
918
+ return /* @__PURE__ */ jsxs8(
919
+ Box8,
920
+ {
921
+ width: columns,
922
+ height: rows,
923
+ flexDirection: "column",
924
+ backgroundColor: colors.bg,
925
+ overflow: "hidden",
926
+ children: [
927
+ /* @__PURE__ */ jsxs8(Box8, { paddingX: 2, flexShrink: 0, children: [
928
+ /* @__PURE__ */ jsx9(Box8, { flexGrow: 1, flexShrink: 1, children: /* @__PURE__ */ jsx9(Text8, { color: colors.muted, wrap: "truncate-middle", children: cleanText(directory) }) }),
929
+ columns >= 70 && nav.map((link) => /* @__PURE__ */ jsx9(Box8, { marginLeft: 2, flexShrink: 0, children: /* @__PURE__ */ jsx9(Text8, { color: path.startsWith(link.href) ? colors.fg : colors.muted, children: cleanText(link.title) }) }, link.href))
930
+ ] }),
931
+ children
932
+ ]
933
+ }
934
+ );
935
+ }
936
+
937
+ // packages/cli/src/console.tsx
938
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
939
+ function Console({
940
+ runtime,
941
+ page,
942
+ prompt,
943
+ revision,
944
+ site,
945
+ loading,
946
+ notice,
947
+ onCommand,
948
+ onCancel,
949
+ onSelect
950
+ }) {
951
+ const scroll = useRef5(null);
952
+ return /* @__PURE__ */ jsx10(TerminalLayout, { path: page.path, nav: site.nav, children: /* @__PURE__ */ jsxs9(ThreadPrimitive2.Root, { flexGrow: 1, flexShrink: 1, minHeight: 0, children: [
953
+ /* @__PURE__ */ jsxs9(ScrollArea, { ref: scroll, selection: page.items.length > 0, children: [
954
+ /* @__PURE__ */ jsx10(PageContent, { page, prompt, onSelect, scroll }),
955
+ /* @__PURE__ */ jsx10(ChatThread, {})
956
+ ] }, `${page.path}:${revision}`),
957
+ /* @__PURE__ */ jsx10(
958
+ Composer,
959
+ {
960
+ runtime,
961
+ page,
962
+ revision,
963
+ loading,
964
+ notice,
965
+ onCommand,
966
+ onCancel,
967
+ onSubmit: () => scroll.current?.end()
968
+ }
969
+ )
970
+ ] }) });
971
+ }
972
+
609
973
  // packages/cli/src/pages.ts
610
- var terminalCommands = [
611
- ...commands.map((command) => command.id === "tags" ? { ...command, args: "[\u6807\u7B7E]" } : command),
612
- { id: "read", args: "<\u7F16\u53F7\u3001slug \u6216\u8DEF\u5F84>", description: "\u9605\u8BFB\u6587\u7AE0\u5168\u6587" },
613
- { id: "open", args: "[\u7F16\u53F7\u6216 URL]", description: "\u5728\u6D4F\u89C8\u5668\u6253\u5F00\u5F53\u524D\u9875\u9762\u6216\u94FE\u63A5" },
614
- { id: "refresh", description: "\u4ECE\u5B98\u7F51\u5237\u65B0\u5F53\u524D\u9875\u9762" },
615
- { id: "quit", description: "\u9000\u51FA" }
616
- ];
617
974
  function helpPage() {
618
975
  return {
619
976
  path: "/help/",
@@ -622,7 +979,7 @@ function helpPage() {
622
979
  items: [],
623
980
  markdown: terminalCommands.map(
624
981
  (command) => `- \`/${command.id}${command.args ? ` ${command.args}` : ""}\` \u2014 ${command.description}`
625
- ).join("\n") + "\n\nTab \u5207\u6362\u8F93\u5165\u548C\u5217\u8868\uFF1B\u2191\u2193 \u9009\u62E9\uFF0CEnter \u6253\u5F00\uFF1BEsc \u505C\u6B62\u56DE\u590D\u6216\u53D6\u6D88\u52A0\u8F7D\uFF1BCtrl+C \u9000\u51FA\u3002"
982
+ ).join("\n") + "\n\n/ \u6253\u5F00\u547D\u4EE4\u9762\u677F\uFF1B\u2191\u2193 \u9009\u62E9\u3001Enter \u786E\u8BA4\u3001Esc \u5173\u95ED\u3002PgUp/PgDn \u6EDA\u52A8\u6B63\u6587\uFF1BTab \u5207\u6362\u8F93\u5165\u548C\u5185\u5BB9\uFF1BEsc \u505C\u6B62\uFF1BCtrl+C \u9000\u51FA\u3002"
626
983
  };
627
984
  }
628
985
  function themePage() {
@@ -651,13 +1008,23 @@ async function loadPage(client, path, signal) {
651
1008
  page.path = result.document.path + url.search;
652
1009
  page.title = result.document.title;
653
1010
  page.markdown = result.document.markdown;
1011
+ page.sections = result.document.sections;
654
1012
  page.verb = "Read";
655
- if (page.path === index.pages.home.path)
656
- page.items = index.site.nav.map((link) => ({
657
- title: link.title,
658
- path: link.href,
659
- external: !link.href.startsWith("/")
660
- }));
1013
+ if (page.path === index.pages.home.path) {
1014
+ const welcome = page.sections?.find((section) => section.kind === "welcome");
1015
+ const menu = welcome?.menu ?? index.site.nav.map((link) => ({ ...link, command: void 0 }));
1016
+ const siteOrigins = new Set([client.site, index.site.url].map((site) => new URL(site).origin));
1017
+ page.items = menu.map((link) => {
1018
+ const destination = new URL(link.href, client.site);
1019
+ const external = !siteOrigins.has(destination.origin);
1020
+ return {
1021
+ title: link.title,
1022
+ description: link.command,
1023
+ path: external ? destination.href : destination.pathname + destination.search,
1024
+ external
1025
+ };
1026
+ });
1027
+ }
661
1028
  } else if (pathname === "/blog/") {
662
1029
  page.title = "\u5168\u90E8\u6587\u7AE0";
663
1030
  page.items = index.posts.map((post) => ({
@@ -706,12 +1073,12 @@ async function loadPage(client, path, signal) {
706
1073
  }
707
1074
 
708
1075
  // packages/cli/src/session.ts
709
- import { useApp, useInput as useInput2 } from "ink";
710
- import { useCallback as useCallback2, useEffect as useEffect3, useState as useState3 } from "react";
1076
+ import { useApp, useInput as useInput5 } from "ink";
1077
+ import { useState as useState5 } from "react";
711
1078
 
712
1079
  // packages/cli/src/agent.ts
713
1080
  import { useChatRuntime } from "@assistant-ui/ai-sdk";
714
- import { useEffect, useMemo as useMemo2 } from "react";
1081
+ import { useEffect as useEffect3, useMemo as useMemo3 } from "react";
715
1082
 
716
1083
  // src/agent/transport.ts
717
1084
  import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
@@ -857,7 +1224,7 @@ function createAgentTransport(handlers, options) {
857
1224
 
858
1225
  // packages/cli/src/agent.ts
859
1226
  function useTerminalAgent({ site, index, page, theme, setTheme, navigate }) {
860
- const transport = useMemo2(
1227
+ const transport = useMemo3(
861
1228
  () => createAgentTransport(
862
1229
  {
863
1230
  getContext: () => {
@@ -900,7 +1267,7 @@ function useTerminalAgent({ site, index, page, theme, setTheme, navigate }) {
900
1267
  ]
901
1268
  );
902
1269
  const runtime = useChatRuntime({ transport });
903
- useEffect(() => () => runtime.thread.cancelRun(), [runtime]);
1270
+ useEffect3(() => () => runtime.thread.cancelRun(), [runtime]);
904
1271
  return runtime;
905
1272
  }
906
1273
 
@@ -921,42 +1288,17 @@ async function openBrowser(input, site) {
921
1288
  });
922
1289
  }
923
1290
 
924
- // packages/cli/src/commands.ts
925
- function numberedItem(input, page) {
926
- if (!/^\d+$/.test(input)) return void 0;
927
- const item = page.items[Number(input) - 1];
928
- if (!item) throw new Error("\u5F53\u524D\u5217\u8868\u6CA1\u6709\u8FD9\u4E2A\u7F16\u53F7\uFF0C\u8BF7\u4F7F\u7528\u5217\u8868\u4E2D\u663E\u793A\u7684\u7F16\u53F7\u3002");
929
- return item;
930
- }
931
- function resolveReadPath(input, page) {
932
- if (!input) throw new Error("\u8F93\u5165 /read <\u7F16\u53F7\u3001slug \u6216\u7AD9\u5185\u8DEF\u5F84> \u9605\u8BFB\u6587\u7AE0\u3002");
933
- const item = numberedItem(input, page);
934
- if (item) {
935
- if (item.external || !item.path.startsWith("/posts/"))
936
- throw new Error("\u8FD9\u4E2A\u7F16\u53F7\u4E0D\u662F\u6587\u7AE0\uFF0C\u8BF7\u4F7F\u7528 /blog \u6216 /search \u67E5\u627E\u6587\u7AE0\u3002");
937
- return item.path;
938
- }
939
- return input.startsWith("/") ? input : `/posts/${input.replace(/\/$/, "")}/`;
940
- }
941
- function resolveOpenTarget(input, page) {
942
- return numberedItem(input, page)?.path ?? (input || page.path);
943
- }
944
- function resolveTagPath(input, index) {
945
- const tag = index.tags.find((tag2) => tag2.name === input);
946
- if (!tag) throw new Error("\u627E\u4E0D\u5230\u8FD9\u4E2A\u6807\u7B7E\uFF0C\u8BF7\u4F7F\u7528 /tags \u67E5\u770B\u53EF\u7528\u6807\u7B7E\u3002");
947
- return tag.path;
948
- }
949
-
950
1291
  // packages/cli/src/navigation.ts
951
- import { useCallback, useEffect as useEffect2, useRef, useState as useState2 } from "react";
952
- function useNavigation(client, initial, onPage) {
953
- const [state, setState] = useState2({
1292
+ import { useCallback, useEffect as useEffect4, useRef as useRef6, useState as useState4 } from "react";
1293
+ function useNavigation(client, initial) {
1294
+ const [state, setState] = useState4({
954
1295
  ...initial,
955
1296
  status: "idle",
956
- notice: client.warning
1297
+ notice: client.warning,
1298
+ prompt: "/home"
957
1299
  });
958
- const active = useRef(void 0);
959
- useEffect2(
1300
+ const active = useRef6(void 0);
1301
+ useEffect4(
960
1302
  () => () => {
961
1303
  active.current?.abort();
962
1304
  active.current = void 0;
@@ -974,10 +1316,9 @@ function useNavigation(client, initial, onPage) {
974
1316
  const show = useCallback(
975
1317
  (page, prompt) => {
976
1318
  cancel();
977
- setState((current) => ({ ...current, page, notice: "" }));
978
- onPage(page, prompt);
1319
+ setState((current) => ({ ...current, page, prompt, notice: "" }));
979
1320
  },
980
- [cancel, onPage]
1321
+ [cancel]
981
1322
  );
982
1323
  const navigate = useCallback(
983
1324
  async (path, prompt = path) => {
@@ -1001,11 +1342,10 @@ function useNavigation(client, initial, onPage) {
1001
1342
  }
1002
1343
  if (active.current !== request) return false;
1003
1344
  active.current = void 0;
1004
- setState({ ...next, status: "idle", notice: client.warning });
1005
- onPage(next.page, prompt);
1345
+ setState({ ...next, status: "idle", notice: client.warning, prompt });
1006
1346
  return true;
1007
1347
  },
1008
- [client, onPage]
1348
+ [client]
1009
1349
  );
1010
1350
  return { ...state, navigate, show, cancel, notify };
1011
1351
  }
@@ -1013,48 +1353,23 @@ function useNavigation(client, initial, onPage) {
1013
1353
  // packages/cli/src/session.ts
1014
1354
  function useTerminalSession(client, initial) {
1015
1355
  const { exit } = useApp();
1016
- const [theme, setTheme] = useState3("auto");
1017
- const [output, setOutput] = useState3([
1018
- { id: "home", page: initial.page, prompt: "/home" }
1019
- ]);
1020
- const appendPage = useCallback2((page2, prompt) => {
1021
- const entry = { id: crypto.randomUUID(), page: page2, prompt };
1022
- setOutput((items) => [...items, entry]);
1023
- }, []);
1024
- const { index, page, status, notice, navigate, show, cancel, notify } = useNavigation(
1025
- client,
1026
- initial,
1027
- appendPage
1028
- );
1356
+ const [theme, setTheme] = useState5("auto");
1357
+ const { index, page, prompt, status, notice, navigate, show, cancel, notify } = useNavigation(client, initial);
1029
1358
  const runtime = useTerminalAgent({ site: client.site, index, page, theme, setTheme, navigate });
1030
- useEffect3(() => {
1031
- const printed = /* @__PURE__ */ new Set();
1032
- const publish = () => {
1033
- const completed = runtime.thread.getState().messages.filter(
1034
- (message) => !printed.has(message.id) && (message.role !== "assistant" || message.status.type !== "running")
1035
- );
1036
- if (!completed.length) return;
1037
- const entries = completed.map((message) => ({ id: message.id, message }));
1038
- completed.forEach((message) => printed.add(message.id));
1039
- setOutput((items) => [...items, ...entries]);
1040
- };
1041
- publish();
1042
- return runtime.thread.subscribe(publish);
1043
- }, [runtime]);
1044
1359
  const stop = () => {
1045
1360
  cancel();
1046
1361
  runtime.thread.cancelRun();
1047
1362
  };
1048
- useInput2((input, key) => {
1049
- if (key.escape) {
1050
- stop();
1051
- notify("\u5DF2\u505C\u6B62\u3002");
1052
- }
1363
+ useInput5((input, key) => {
1053
1364
  if (key.ctrl && input === "c") {
1054
1365
  stop();
1055
1366
  exit();
1056
1367
  }
1057
1368
  });
1369
+ const onCancel = () => {
1370
+ stop();
1371
+ notify("\u5DF2\u505C\u6B62\u3002");
1372
+ };
1058
1373
  const onCommand = async (name, args) => {
1059
1374
  try {
1060
1375
  if (pageCommands[name] && !(name === "tags" && args)) {
@@ -1117,44 +1432,35 @@ function useTerminalSession(client, initial) {
1117
1432
  runtime,
1118
1433
  page,
1119
1434
  revision: index.revision,
1120
- output,
1435
+ prompt,
1436
+ site: index.site,
1121
1437
  loading: status === "loading",
1122
1438
  notice,
1123
1439
  onCommand,
1440
+ onCancel,
1124
1441
  onSelect,
1125
1442
  theme
1126
1443
  };
1127
1444
  }
1128
1445
 
1129
1446
  // packages/cli/src/app.tsx
1130
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1447
+ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
1131
1448
  function Session({
1132
1449
  client,
1133
1450
  initial,
1134
1451
  width
1135
1452
  }) {
1136
1453
  const { theme, ...session } = useTerminalSession(client, initial);
1137
- return /* @__PURE__ */ jsx4(TerminalTheme, { preference: theme, width, children: /* @__PURE__ */ jsx4(AssistantRuntimeProvider, { runtime: session.runtime, children: /* @__PURE__ */ jsx4(Console, { ...session }) }) });
1454
+ return /* @__PURE__ */ jsx11(TerminalTheme, { preference: theme, width, children: /* @__PURE__ */ jsx11(AssistantRuntimeProvider, { runtime: session.runtime, children: /* @__PURE__ */ jsx11(Console, { ...session }) }) });
1138
1455
  }
1139
1456
  function App({ site }) {
1140
1457
  const { exit } = useApp2();
1141
- const { stdout } = useStdout();
1142
- const [client] = useState4(() => new TerminalClient(site));
1143
- const [state, setState] = useState4({ status: "loading" });
1144
- const [attempt, setAttempt] = useState4(0);
1145
- const width = useSyncExternalStore(
1146
- useCallback3(
1147
- (notify) => {
1148
- stdout.on("resize", notify);
1149
- return () => {
1150
- stdout.off("resize", notify);
1151
- };
1152
- },
1153
- [stdout]
1154
- ),
1155
- () => "columns" in stdout && typeof stdout.columns === "number" ? stdout.columns : 80
1156
- );
1157
- useEffect4(() => {
1458
+ const { columns } = useWindowSize3();
1459
+ const [client] = useState6(() => new TerminalClient(site));
1460
+ const [state, setState] = useState6({ status: "loading" });
1461
+ const [attempt, setAttempt] = useState6(0);
1462
+ const width = Math.max(10, columns - 5);
1463
+ useEffect5(() => {
1158
1464
  const request = new AbortController();
1159
1465
  loadPage(client, "/", request.signal).then((content) => {
1160
1466
  if (!request.signal.aborted) setState({ status: "ready", content });
@@ -1167,7 +1473,7 @@ function App({ site }) {
1167
1473
  });
1168
1474
  return () => request.abort();
1169
1475
  }, [client, attempt]);
1170
- useInput3(
1476
+ useInput6(
1171
1477
  (input, key) => {
1172
1478
  if (key.ctrl && input === "c" || input === "q") exit();
1173
1479
  else if (state.status === "error" && (key.return || input === "r" || input === "/refresh")) {
@@ -1178,17 +1484,21 @@ function App({ site }) {
1178
1484
  { isActive: state.status !== "ready" }
1179
1485
  );
1180
1486
  if (state.status === "ready")
1181
- return /* @__PURE__ */ jsx4(Session, { client, initial: state.content, width });
1182
- return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
1183
- /* @__PURE__ */ jsxs3(Text3, { children: [
1487
+ return /* @__PURE__ */ jsx11(Session, { client, initial: state.content, width });
1488
+ return /* @__PURE__ */ jsx11(TerminalTheme, { preference: "auto", width, children: /* @__PURE__ */ jsx11(TerminalLayout, { path: "/", children: /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", flexGrow: 1, padding: 2, children: [
1489
+ /* @__PURE__ */ jsxs10(Text9, { children: [
1184
1490
  "\u25C6 ",
1185
1491
  state.status === "error" ? state.message : "\u6B63\u5728\u8BFB\u53D6\u5B98\u7F51\u2026"
1186
1492
  ] }),
1187
- /* @__PURE__ */ jsx4(Text3, { children: state.status === "error" ? "Enter \u91CD\u8BD5 \xB7 q \u9000\u51FA" : "Ctrl+C \u9000\u51FA" })
1188
- ] });
1493
+ /* @__PURE__ */ jsx11(Text9, { children: state.status === "error" ? "Enter \u91CD\u8BD5 \xB7 q \u9000\u51FA" : "Ctrl+C \u9000\u51FA" })
1494
+ ] }) }) });
1189
1495
  }
1190
1496
  async function run(site) {
1191
- const app = render(/* @__PURE__ */ jsx4(App, { site }), { exitOnCtrlC: false });
1497
+ const app = render(/* @__PURE__ */ jsx11(App, { site }), {
1498
+ exitOnCtrlC: false,
1499
+ alternateScreen: true,
1500
+ incrementalRendering: true
1501
+ });
1192
1502
  await app.waitUntilExit();
1193
1503
  }
1194
1504
  export {
package/dist/index.js CHANGED
@@ -16,14 +16,14 @@ try {
16
16
  "miantiao.me \u2014 \u7EC8\u7AEF\u91CC\u7684\u9762\u6761\n\n\u7528\u6CD5\uFF1Anpx miantiao.me [--site <URL>]\n\n -h, --help \u67E5\u770B\u5E2E\u52A9\n -v, --version \u67E5\u770B\u7248\u672C\n --site <URL> \u5B98\u7F51\u5730\u5740\uFF0C\u4E5F\u53EF\u6307\u5411\u672C\u5730\u9884\u89C8\n\n\u8FDB\u5165\u540E\u8F93\u5165 /help \u67E5\u770B\u547D\u4EE4\uFF0CCtrl+C \u9000\u51FA\u3002\n\u9700\u8981 Node.js 22.14 \u6216\u66F4\u65B0\u7248\u672C\uFF0C\u4EE5\u53CA\u53EF\u4EA4\u4E92\u7684\u7EC8\u7AEF\u3002"
17
17
  );
18
18
  } else if (values.version) {
19
- console.log("0.1.0");
19
+ console.log("0.1.2");
20
20
  } else {
21
21
  const site = new URL(values.site);
22
22
  if (!["https:", "http:"].includes(site.protocol) || site.username || site.password)
23
23
  throw new Error("--site \u5FC5\u987B\u4E3A\u4E0D\u542B\u51ED\u8BC1\u7684 HTTP \u6216 HTTPS \u5730\u5740\u3002");
24
24
  if (!process.stdin.isTTY || !process.stdout.isTTY)
25
25
  throw new Error("\u8BF7\u5728\u4EA4\u4E92\u5F0F\u7EC8\u7AEF\u8FD0\u884C npx miantiao.me\uFF1B\u4F7F\u7528 --help \u67E5\u770B\u5E2E\u52A9\u3002");
26
- const { run } = await import("./app-UWAFKI3E.js");
26
+ const { run } = await import("./app-IW4THTCR.js");
27
27
  await run(site.origin);
28
28
  }
29
29
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miantiao.me",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "在终端浏览 miantiao.me,与面条的 AI 助手对话。",
5
5
  "type": "module",
6
6
  "bin": {