miantiao.me 0.1.1 → 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
- PgUp/PgDn 滚动正文,输入框始终固定在底部。Tab 在输入框和内容区之间切换;列表中用 ↑↓ 选择、Enter 打开,文章中用 ↑↓、Home、End 滚动。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 Box8, render, Text as Text8, useApp as useApp2, useInput as useInput4, useWindowSize as useWindowSize2 } from "ink";
8
- import { useEffect as useEffect4, useState as useState5 } 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";
@@ -287,7 +287,7 @@ var TerminalClient = class {
287
287
 
288
288
  // packages/cli/src/console.tsx
289
289
  import { ThreadPrimitive as ThreadPrimitive2 } from "@assistant-ui/react-ink";
290
- import { useRef as useRef4 } from "react";
290
+ import { useRef as useRef5 } from "react";
291
291
 
292
292
  // packages/cli/src/chat.tsx
293
293
  import { ThreadPrimitive, useAuiState } from "@assistant-ui/react-ink";
@@ -644,7 +644,8 @@ function ChatThread() {
644
644
 
645
645
  // packages/cli/src/composer.tsx
646
646
  import { ComposerPrimitive, LoadingPrimitive, useAui, useAuiState as useAuiState2 } from "@assistant-ui/react-ink";
647
- import { Box as Box5, Text as Text5, useFocusManager } from "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";
648
649
 
649
650
  // src/agent/appearance.ts
650
651
  var composerPlaceholder = "\u95EE\u70B9\u4EC0\u4E48\uFF0C\u6216\u8F93\u5165 / \u67E5\u770B\u547D\u4EE4";
@@ -680,8 +681,103 @@ function getSearchPath(query) {
680
681
  return query ? `/search/?q=${encodeURIComponent(query)}` : "/search/";
681
682
  }
682
683
 
683
- // packages/cli/src/composer.tsx
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
684
728
  import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
729
+ function CommandMenu({
730
+ text,
731
+ focused,
732
+ onSelect,
733
+ ref
734
+ }) {
735
+ const { colors } = useTerminalStyle();
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;
749
+ },
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);
774
+ }),
775
+ /* @__PURE__ */ jsx6(Text5, { color: colors.muted, wrap: "truncate-end", children: "\u2191\u2193 \u9009\u62E9 \xB7 Enter \u786E\u8BA4 \xB7 Esc \u5173\u95ED" })
776
+ ] });
777
+ }
778
+
779
+ // packages/cli/src/composer.tsx
780
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
685
781
  function Composer({
686
782
  runtime,
687
783
  page,
@@ -689,34 +785,69 @@ function Composer({
689
785
  loading,
690
786
  notice,
691
787
  onCommand,
788
+ onCancel,
692
789
  onSubmit
693
790
  }) {
694
791
  const { colors } = useTerminalStyle();
695
792
  const aui = useAui();
696
- const { activeId } = useFocusManager();
793
+ const { activeId, focus, focusNext } = useFocusManager();
697
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);
698
798
  const focused = activeId !== "content";
699
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginX: 2, flexShrink: 0, children: [
700
- (loading || running || notice) && /* @__PURE__ */ jsxs5(Box5, { paddingLeft: 2, height: 1, overflow: "hidden", children: [
701
- (loading || running) && /* @__PURE__ */ jsx6(LoadingPrimitive.Spinner, {}),
702
- /* @__PURE__ */ jsx6(Text5, { 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` })
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` })
703
833
  ] }),
704
- /* @__PURE__ */ jsxs5(Box5, { borderStyle: "round", borderColor: focused ? colors["line-strong"] : colors.line, paddingX: 1, children: [
705
- /* @__PURE__ */ jsx6(Text5, { color: colors.prompt, children: "\u276F " }),
706
- /* @__PURE__ */ jsx6(
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(
707
837
  ComposerPrimitive.Input,
708
838
  {
709
839
  autoFocus: true,
710
840
  submitOnEnter: true,
711
841
  flexGrow: 1,
712
842
  placeholder: composerPlaceholder,
713
- onSubmit: (text) => {
714
- const input = text.trim();
843
+ onSubmit: (text2) => {
844
+ const input = text2.trim();
715
845
  if (!input) return;
716
- const command = parseCommand(input);
717
- if (command) {
846
+ if (menu.current?.submit(text2)) return;
847
+ if (input.startsWith("/")) {
848
+ const command = parseCommand(input);
718
849
  aui.composer.setText("");
719
- void onCommand(command.name, command.args);
850
+ void onCommand(command?.name ?? "help", command?.args ?? "");
720
851
  } else if (!running) {
721
852
  onSubmit();
722
853
  runtime.thread.composer.send();
@@ -725,7 +856,7 @@ function Composer({
725
856
  },
726
857
  `${page.path}:${revision}`
727
858
  ),
728
- /* @__PURE__ */ jsx6(Box5, { position: "absolute", bottom: -1, right: 1, children: /* @__PURE__ */ jsxs5(Text5, { color: colors.muted, backgroundColor: colors.bg, children: [
859
+ /* @__PURE__ */ jsx7(Box6, { position: "absolute", bottom: -1, right: 1, children: /* @__PURE__ */ jsxs6(Text6, { color: colors.muted, backgroundColor: colors.bg, children: [
729
860
  " ",
730
861
  composerBrand.mark,
731
862
  " \xB7 ",
@@ -733,21 +864,21 @@ function Composer({
733
864
  " "
734
865
  ] }) })
735
866
  ] }),
736
- /* @__PURE__ */ jsxs5(Text5, { color: colors.muted, wrap: "truncate-end", children: [
737
- /* @__PURE__ */ jsx6(Text5, { color: colors["fg-soft"], bold: true, children: "Tab" }),
867
+ /* @__PURE__ */ jsxs6(Text6, { color: colors.muted, wrap: "truncate-end", children: [
868
+ /* @__PURE__ */ jsx7(Text6, { color: colors["fg-soft"], bold: true, children: "Tab" }),
738
869
  ":\u5207\u6362 \u2502 ",
739
- /* @__PURE__ */ jsx6(Text5, { bold: true, children: "PgUp/PgDn" }),
870
+ /* @__PURE__ */ jsx7(Text6, { bold: true, children: "PgUp/PgDn" }),
740
871
  ":\u6EDA\u52A8 \u2502 ",
741
- /* @__PURE__ */ jsx6(Text5, { bold: true, children: "/help" }),
872
+ /* @__PURE__ */ jsx7(Text6, { bold: true, children: "/help" }),
742
873
  " \u2502 Ctrl+C:\u9000\u51FA"
743
874
  ] })
744
875
  ] });
745
876
  }
746
877
 
747
878
  // packages/cli/src/page-content.tsx
748
- import { Box as Box6, Text as Text6 } from "ink";
879
+ import { Box as Box7, Text as Text7 } from "ink";
749
880
  import { memo } from "react";
750
- import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
881
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
751
882
  var PageContent = memo(function PageContent2({
752
883
  page,
753
884
  prompt,
@@ -755,96 +886,56 @@ var PageContent = memo(function PageContent2({
755
886
  scroll
756
887
  }) {
757
888
  const { colors } = useTerminalStyle();
758
- if (page.sections) return /* @__PURE__ */ jsx7(HomeSections, { page, onSelect, scroll });
759
- return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginBottom: 1, children: [
760
- page.path !== "/" && /* @__PURE__ */ jsx7(Prompt, { text: prompt, command: true }),
761
- /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", paddingLeft: 2, paddingRight: 1, paddingTop: 1, children: [
762
- page.path !== "/" && /* @__PURE__ */ jsxs6(Text6, { children: [
763
- /* @__PURE__ */ jsx7(Text6, { color: colors.skill, children: "\u25C6 " }),
764
- /* @__PURE__ */ jsx7(Text6, { color: colors.fg, bold: true, children: page.verb }),
765
- /* @__PURE__ */ jsxs6(Text6, { color: colors.path, children: [
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: [
766
897
  " ",
767
898
  cleanText(page.title)
768
899
  ] })
769
900
  ] }),
770
- page.markdown && /* @__PURE__ */ jsx7(Markdown, { text: page.markdown }),
771
- page.items.length > 0 && /* @__PURE__ */ jsx7(PageList, { items: page.items, onSelect, scroll })
901
+ page.markdown && /* @__PURE__ */ jsx8(Markdown, { text: page.markdown }),
902
+ page.items.length > 0 && /* @__PURE__ */ jsx8(PageList, { items: page.items, onSelect, scroll })
772
903
  ] })
773
904
  ] });
774
905
  });
775
906
 
776
- // packages/cli/src/window.tsx
777
- import { Box as Box7, Text as Text7, useWindowSize } from "ink";
778
- import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
779
- function windowWidth(columns) {
780
- return Math.min(120, columns >= 84 ? columns - 4 : columns);
781
- }
782
- function Window({
783
- name,
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({
784
911
  path,
785
912
  nav = [],
786
913
  children
787
914
  }) {
788
- const { columns, rows } = useWindowSize();
915
+ const { columns, rows } = useWindowSize2();
789
916
  const { colors } = useTerminalStyle();
790
- const width = windowWidth(columns);
791
- const margin = columns >= 80 && rows >= 24 ? 1 : 0;
792
917
  const directory = `~/miantiao.me${path === "/" ? "" : path.replace(/\/$/, "")}`;
793
- return /* @__PURE__ */ jsx8(
794
- Box7,
918
+ return /* @__PURE__ */ jsxs8(
919
+ Box8,
795
920
  {
796
921
  width: columns,
797
922
  height: rows,
798
923
  flexDirection: "column",
799
- alignItems: "center",
800
- paddingY: margin,
801
- backgroundColor: colors.page,
802
- children: /* @__PURE__ */ jsxs7(
803
- Box7,
804
- {
805
- width,
806
- height: Math.max(1, rows - margin * 2),
807
- flexDirection: "column",
808
- borderStyle: "round",
809
- borderColor: colors.line,
810
- backgroundColor: colors.bg,
811
- overflow: "hidden",
812
- children: [
813
- /* @__PURE__ */ jsxs7(
814
- Box7,
815
- {
816
- paddingX: 2,
817
- flexShrink: 0,
818
- borderStyle: "single",
819
- borderTop: false,
820
- borderLeft: false,
821
- borderRight: false,
822
- borderColor: colors.line,
823
- children: [
824
- /* @__PURE__ */ jsx8(Text7, { color: colors.err, children: "\u25CF " }),
825
- /* @__PURE__ */ jsx8(Text7, { color: colors.warn, children: "\u25CF " }),
826
- /* @__PURE__ */ jsx8(Text7, { color: colors.ok, children: "\u25CF " }),
827
- /* @__PURE__ */ jsx8(Box7, { flexGrow: 1, justifyContent: "center", paddingRight: 6, children: /* @__PURE__ */ jsxs7(Text7, { color: colors.muted, wrap: "truncate-end", children: [
828
- cleanText(name),
829
- " \u2014 miantiao.me"
830
- ] }) })
831
- ]
832
- }
833
- ),
834
- /* @__PURE__ */ jsxs7(Box7, { paddingX: 2, paddingTop: 1, flexShrink: 0, children: [
835
- /* @__PURE__ */ jsx8(Box7, { flexGrow: 1, flexShrink: 1, children: /* @__PURE__ */ jsx8(Text7, { color: colors.muted, wrap: "truncate-middle", children: cleanText(directory) }) }),
836
- width >= 70 && nav.map((link) => /* @__PURE__ */ jsx8(Box7, { marginLeft: 2, flexShrink: 0, children: /* @__PURE__ */ jsx8(Text7, { color: path.startsWith(link.href) ? colors.fg : colors.muted, children: cleanText(link.title) }) }, link.href))
837
- ] }),
838
- children
839
- ]
840
- }
841
- )
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
+ ]
842
933
  }
843
934
  );
844
935
  }
845
936
 
846
937
  // packages/cli/src/console.tsx
847
- import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
938
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
848
939
  function Console({
849
940
  runtime,
850
941
  page,
@@ -854,15 +945,16 @@ function Console({
854
945
  loading,
855
946
  notice,
856
947
  onCommand,
948
+ onCancel,
857
949
  onSelect
858
950
  }) {
859
- const scroll = useRef4(null);
860
- return /* @__PURE__ */ jsx9(Window, { name: site.name, path: page.path, nav: site.nav, children: /* @__PURE__ */ jsxs8(ThreadPrimitive2.Root, { flexGrow: 1, flexShrink: 1, minHeight: 0, children: [
861
- /* @__PURE__ */ jsxs8(ScrollArea, { ref: scroll, selection: page.items.length > 0, children: [
862
- /* @__PURE__ */ jsx9(PageContent, { page, prompt, onSelect, scroll }),
863
- /* @__PURE__ */ jsx9(ChatThread, {})
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, {})
864
956
  ] }, `${page.path}:${revision}`),
865
- /* @__PURE__ */ jsx9(
957
+ /* @__PURE__ */ jsx10(
866
958
  Composer,
867
959
  {
868
960
  runtime,
@@ -871,6 +963,7 @@ function Console({
871
963
  loading,
872
964
  notice,
873
965
  onCommand,
966
+ onCancel,
874
967
  onSubmit: () => scroll.current?.end()
875
968
  }
876
969
  )
@@ -878,13 +971,6 @@ function Console({
878
971
  }
879
972
 
880
973
  // packages/cli/src/pages.ts
881
- var terminalCommands = [
882
- ...commands.map((command) => command.id === "tags" ? { ...command, args: "[\u6807\u7B7E]" } : command),
883
- { id: "read", args: "<\u7F16\u53F7\u3001slug \u6216\u8DEF\u5F84>", description: "\u9605\u8BFB\u6587\u7AE0\u5168\u6587" },
884
- { id: "open", args: "[\u7F16\u53F7\u6216 URL]", description: "\u5728\u6D4F\u89C8\u5668\u6253\u5F00\u5F53\u524D\u9875\u9762\u6216\u94FE\u63A5" },
885
- { id: "refresh", description: "\u4ECE\u5B98\u7F51\u5237\u65B0\u5F53\u524D\u9875\u9762" },
886
- { id: "quit", description: "\u9000\u51FA" }
887
- ];
888
974
  function helpPage() {
889
975
  return {
890
976
  path: "/help/",
@@ -893,7 +979,7 @@ function helpPage() {
893
979
  items: [],
894
980
  markdown: terminalCommands.map(
895
981
  (command) => `- \`/${command.id}${command.args ? ` ${command.args}` : ""}\` \u2014 ${command.description}`
896
- ).join("\n") + "\n\nPgUp/PgDn \u6EDA\u52A8\u6B63\u6587\uFF1BTab \u5207\u6362\u8F93\u5165\u548C\u5185\u5BB9\uFF1B\u2191\u2193 \u9009\u62E9\uFF0CEnter \u6253\u5F00\uFF1BEsc \u505C\u6B62\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"
897
983
  };
898
984
  }
899
985
  function themePage() {
@@ -987,12 +1073,12 @@ async function loadPage(client, path, signal) {
987
1073
  }
988
1074
 
989
1075
  // packages/cli/src/session.ts
990
- import { useApp, useInput as useInput3 } from "ink";
991
- import { useState as useState4 } from "react";
1076
+ import { useApp, useInput as useInput5 } from "ink";
1077
+ import { useState as useState5 } from "react";
992
1078
 
993
1079
  // packages/cli/src/agent.ts
994
1080
  import { useChatRuntime } from "@assistant-ui/ai-sdk";
995
- import { useEffect as useEffect2, useMemo as useMemo3 } from "react";
1081
+ import { useEffect as useEffect3, useMemo as useMemo3 } from "react";
996
1082
 
997
1083
  // src/agent/transport.ts
998
1084
  import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
@@ -1181,7 +1267,7 @@ function useTerminalAgent({ site, index, page, theme, setTheme, navigate }) {
1181
1267
  ]
1182
1268
  );
1183
1269
  const runtime = useChatRuntime({ transport });
1184
- useEffect2(() => () => runtime.thread.cancelRun(), [runtime]);
1270
+ useEffect3(() => () => runtime.thread.cancelRun(), [runtime]);
1185
1271
  return runtime;
1186
1272
  }
1187
1273
 
@@ -1202,43 +1288,17 @@ async function openBrowser(input, site) {
1202
1288
  });
1203
1289
  }
1204
1290
 
1205
- // packages/cli/src/commands.ts
1206
- function numberedItem(input, page) {
1207
- if (!/^\d+$/.test(input)) return void 0;
1208
- const item = page.items[Number(input) - 1];
1209
- 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");
1210
- return item;
1211
- }
1212
- function resolveReadPath(input, page) {
1213
- if (!input) throw new Error("\u8F93\u5165 /read <\u7F16\u53F7\u3001slug \u6216\u7AD9\u5185\u8DEF\u5F84> \u9605\u8BFB\u6587\u7AE0\u3002");
1214
- const item = numberedItem(input, page);
1215
- if (item) {
1216
- if (item.external || !item.path.startsWith("/posts/"))
1217
- throw new Error("\u8FD9\u4E2A\u7F16\u53F7\u4E0D\u662F\u6587\u7AE0\uFF0C\u8BF7\u4F7F\u7528 /blog \u6216 /search \u67E5\u627E\u6587\u7AE0\u3002");
1218
- return item.path;
1219
- }
1220
- return input.startsWith("/") ? input : `/posts/${input.replace(/\/$/, "")}/`;
1221
- }
1222
- function resolveOpenTarget(input, page) {
1223
- return numberedItem(input, page)?.path ?? (input || page.path);
1224
- }
1225
- function resolveTagPath(input, index) {
1226
- const tag = index.tags.find((tag2) => tag2.name === input);
1227
- if (!tag) throw new Error("\u627E\u4E0D\u5230\u8FD9\u4E2A\u6807\u7B7E\uFF0C\u8BF7\u4F7F\u7528 /tags \u67E5\u770B\u53EF\u7528\u6807\u7B7E\u3002");
1228
- return tag.path;
1229
- }
1230
-
1231
1291
  // packages/cli/src/navigation.ts
1232
- import { useCallback, useEffect as useEffect3, useRef as useRef5, useState as useState3 } from "react";
1292
+ import { useCallback, useEffect as useEffect4, useRef as useRef6, useState as useState4 } from "react";
1233
1293
  function useNavigation(client, initial) {
1234
- const [state, setState] = useState3({
1294
+ const [state, setState] = useState4({
1235
1295
  ...initial,
1236
1296
  status: "idle",
1237
1297
  notice: client.warning,
1238
1298
  prompt: "/home"
1239
1299
  });
1240
- const active = useRef5(void 0);
1241
- useEffect3(
1300
+ const active = useRef6(void 0);
1301
+ useEffect4(
1242
1302
  () => () => {
1243
1303
  active.current?.abort();
1244
1304
  active.current = void 0;
@@ -1293,23 +1353,23 @@ function useNavigation(client, initial) {
1293
1353
  // packages/cli/src/session.ts
1294
1354
  function useTerminalSession(client, initial) {
1295
1355
  const { exit } = useApp();
1296
- const [theme, setTheme] = useState4("auto");
1356
+ const [theme, setTheme] = useState5("auto");
1297
1357
  const { index, page, prompt, status, notice, navigate, show, cancel, notify } = useNavigation(client, initial);
1298
1358
  const runtime = useTerminalAgent({ site: client.site, index, page, theme, setTheme, navigate });
1299
1359
  const stop = () => {
1300
1360
  cancel();
1301
1361
  runtime.thread.cancelRun();
1302
1362
  };
1303
- useInput3((input, key) => {
1304
- if (key.escape) {
1305
- stop();
1306
- notify("\u5DF2\u505C\u6B62\u3002");
1307
- }
1363
+ useInput5((input, key) => {
1308
1364
  if (key.ctrl && input === "c") {
1309
1365
  stop();
1310
1366
  exit();
1311
1367
  }
1312
1368
  });
1369
+ const onCancel = () => {
1370
+ stop();
1371
+ notify("\u5DF2\u505C\u6B62\u3002");
1372
+ };
1313
1373
  const onCommand = async (name, args) => {
1314
1374
  try {
1315
1375
  if (pageCommands[name] && !(name === "tags" && args)) {
@@ -1377,29 +1437,30 @@ function useTerminalSession(client, initial) {
1377
1437
  loading: status === "loading",
1378
1438
  notice,
1379
1439
  onCommand,
1440
+ onCancel,
1380
1441
  onSelect,
1381
1442
  theme
1382
1443
  };
1383
1444
  }
1384
1445
 
1385
1446
  // packages/cli/src/app.tsx
1386
- import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1447
+ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
1387
1448
  function Session({
1388
1449
  client,
1389
1450
  initial,
1390
1451
  width
1391
1452
  }) {
1392
1453
  const { theme, ...session } = useTerminalSession(client, initial);
1393
- return /* @__PURE__ */ jsx10(TerminalTheme, { preference: theme, width, children: /* @__PURE__ */ jsx10(AssistantRuntimeProvider, { runtime: session.runtime, children: /* @__PURE__ */ jsx10(Console, { ...session }) }) });
1454
+ return /* @__PURE__ */ jsx11(TerminalTheme, { preference: theme, width, children: /* @__PURE__ */ jsx11(AssistantRuntimeProvider, { runtime: session.runtime, children: /* @__PURE__ */ jsx11(Console, { ...session }) }) });
1394
1455
  }
1395
1456
  function App({ site }) {
1396
1457
  const { exit } = useApp2();
1397
- const { columns } = useWindowSize2();
1398
- const [client] = useState5(() => new TerminalClient(site));
1399
- const [state, setState] = useState5({ status: "loading" });
1400
- const [attempt, setAttempt] = useState5(0);
1401
- const width = Math.max(10, windowWidth(columns) - 7);
1402
- 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(() => {
1403
1464
  const request = new AbortController();
1404
1465
  loadPage(client, "/", request.signal).then((content) => {
1405
1466
  if (!request.signal.aborted) setState({ status: "ready", content });
@@ -1412,7 +1473,7 @@ function App({ site }) {
1412
1473
  });
1413
1474
  return () => request.abort();
1414
1475
  }, [client, attempt]);
1415
- useInput4(
1476
+ useInput6(
1416
1477
  (input, key) => {
1417
1478
  if (key.ctrl && input === "c" || input === "q") exit();
1418
1479
  else if (state.status === "error" && (key.return || input === "r" || input === "/refresh")) {
@@ -1423,17 +1484,17 @@ function App({ site }) {
1423
1484
  { isActive: state.status !== "ready" }
1424
1485
  );
1425
1486
  if (state.status === "ready")
1426
- return /* @__PURE__ */ jsx10(Session, { client, initial: state.content, width });
1427
- return /* @__PURE__ */ jsx10(TerminalTheme, { preference: "auto", width, children: /* @__PURE__ */ jsx10(Window, { name: "miantiao.me", path: "/", children: /* @__PURE__ */ jsxs9(Box8, { flexDirection: "column", flexGrow: 1, padding: 2, children: [
1428
- /* @__PURE__ */ jsxs9(Text8, { 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: [
1429
1490
  "\u25C6 ",
1430
1491
  state.status === "error" ? state.message : "\u6B63\u5728\u8BFB\u53D6\u5B98\u7F51\u2026"
1431
1492
  ] }),
1432
- /* @__PURE__ */ jsx10(Text8, { children: state.status === "error" ? "Enter \u91CD\u8BD5 \xB7 q \u9000\u51FA" : "Ctrl+C \u9000\u51FA" })
1493
+ /* @__PURE__ */ jsx11(Text9, { children: state.status === "error" ? "Enter \u91CD\u8BD5 \xB7 q \u9000\u51FA" : "Ctrl+C \u9000\u51FA" })
1433
1494
  ] }) }) });
1434
1495
  }
1435
1496
  async function run(site) {
1436
- const app = render(/* @__PURE__ */ jsx10(App, { site }), {
1497
+ const app = render(/* @__PURE__ */ jsx11(App, { site }), {
1437
1498
  exitOnCtrlC: false,
1438
1499
  alternateScreen: true,
1439
1500
  incrementalRendering: true
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.1");
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-ZZYXIU6G.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.1",
3
+ "version": "0.1.2",
4
4
  "description": "在终端浏览 miantiao.me,与面条的 AI 助手对话。",
5
5
  "type": "module",
6
6
  "bin": {