miantiao.me 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3274 @@
1
+ import {
2
+ define_TERMINAL_CODE_COLORS_default,
3
+ define_TERMINAL_HOME_default,
4
+ define_TERMINAL_PALETTES_default
5
+ } from "./chunk-WZFV5ZVF.js";
6
+
7
+ // packages/cli/src/app.tsx
8
+ import { AssistantRuntimeProvider } from "@assistant-ui/react-ink";
9
+ import { Box as Box23, render, Text as Text26, useApp as useApp2, useInput as useInput11, useWindowSize as useWindowSize3 } from "ink";
10
+ import { useEffect as useEffect11, useState as useState14 } from "react";
11
+ import { MemoryRouter } from "react-router";
12
+
13
+ // src/shared/terminal.ts
14
+ import { z } from "zod";
15
+ var terminalApiPath = "/api/terminal/v2";
16
+ var linkSchema = z.object({ title: z.string(), href: z.string() });
17
+ var pageSchema = z.object({
18
+ path: z.string(),
19
+ title: z.string(),
20
+ documentUrl: z.string()
21
+ });
22
+ var terminalProjectSchema = z.object({
23
+ id: z.string(),
24
+ title: z.string(),
25
+ description: z.string(),
26
+ url: z.string(),
27
+ repo: z.string(),
28
+ stars: z.number()
29
+ });
30
+ var terminalPostSchema = z.object({
31
+ slug: z.string(),
32
+ title: z.string(),
33
+ description: z.string(),
34
+ date: z.string(),
35
+ tags: z.array(z.string()),
36
+ path: z.string(),
37
+ url: z.string(),
38
+ documentUrl: z.string()
39
+ });
40
+ var terminalIndexSchema = z.object({
41
+ schemaVersion: z.literal(2),
42
+ revision: z.string().min(1),
43
+ site: z.object({
44
+ name: z.string(),
45
+ description: z.string(),
46
+ url: z.string(),
47
+ feedUrl: z.string(),
48
+ sponsorUrl: z.string(),
49
+ pageSize: z.number().int().positive(),
50
+ author: z.object({ name: z.string(), roles: z.array(z.string()) }),
51
+ nav: z.array(linkSchema),
52
+ socials: z.array(linkSchema),
53
+ friends: z.array(linkSchema)
54
+ }),
55
+ ai: z.object({ baseUrl: z.string(), model: z.string() }),
56
+ pages: z.object({ home: pageSchema, about: pageSchema }),
57
+ posts: z.array(terminalPostSchema),
58
+ projects: z.array(terminalProjectSchema),
59
+ tags: z.array(
60
+ z.object({ name: z.string(), path: z.string(), count: z.number().int().nonnegative() })
61
+ ),
62
+ searchIndexUrl: z.string()
63
+ });
64
+ var listSchema = z.discriminatedUnion("kind", [
65
+ z.object({
66
+ kind: z.literal("projects"),
67
+ before: z.string(),
68
+ after: z.string(),
69
+ items: z.array(terminalProjectSchema)
70
+ }),
71
+ z.object({
72
+ kind: z.literal("posts"),
73
+ before: z.string(),
74
+ after: z.string(),
75
+ items: z.array(terminalPostSchema)
76
+ })
77
+ ]);
78
+ var sectionSchema = z.union([
79
+ z.object({
80
+ kind: z.literal("welcome"),
81
+ markdown: z.string(),
82
+ logo: z.string(),
83
+ menu: z.array(linkSchema.extend({ command: z.string() }))
84
+ }),
85
+ z.object({ kind: z.literal("turn"), prompt: z.string(), list: listSchema }),
86
+ z.object({ kind: z.literal("turn"), prompt: z.string(), markdown: z.string() })
87
+ ]);
88
+ var terminalDocumentSchema = z.object({
89
+ schemaVersion: z.literal(2),
90
+ revision: z.string().min(1),
91
+ path: z.string(),
92
+ title: z.string(),
93
+ content: z.union([z.string().min(1), z.array(sectionSchema).nonempty()])
94
+ });
95
+
96
+ // src/shared/search/manager.ts
97
+ import { Document } from "flexsearch";
98
+ var SearchManager = class _SearchManager {
99
+ index;
100
+ dataById;
101
+ constructor(searchData) {
102
+ this.index = new Document({
103
+ document: {
104
+ id: "id",
105
+ index: [
106
+ { field: "searchTitle", tokenize: "forward", resolution: 9 },
107
+ { field: "searchDescription", tokenize: "strict", resolution: 7 },
108
+ {
109
+ field: "searchContent",
110
+ tokenize: "forward",
111
+ resolution: 5,
112
+ context: {
113
+ depth: 1,
114
+ resolution: 3,
115
+ bidirectional: true
116
+ }
117
+ },
118
+ { field: "searchTags", tokenize: "strict", resolution: 9 }
119
+ ]
120
+ },
121
+ cache: 100,
122
+ fastupdate: false
123
+ });
124
+ this.dataById = new Map(searchData.map((doc) => [doc.id, doc]));
125
+ searchData.forEach((doc) => this.index.add(doc));
126
+ }
127
+ static fromData(data) {
128
+ return new _SearchManager(data);
129
+ }
130
+ search(query, limit = 20) {
131
+ const trimmedQuery = query.trim();
132
+ if (!trimmedQuery || limit <= 0) return [];
133
+ const phraseTerms = /[\u4E00-\u9FFF]{4,}/.test(trimmedQuery) ? trimmedQuery.toLowerCase().match(/[\u4E00-\u9FFF]+|[a-z0-9]+/g) : null;
134
+ const matchesPhrase = (document) => {
135
+ if (!phraseTerms) return true;
136
+ const fields = [document.title, document.description, document.content, ...document.tags];
137
+ const words = new Set(
138
+ fields.flatMap((field) => field.toLowerCase().match(/[a-z0-9]+/g) ?? [])
139
+ );
140
+ return phraseTerms.every(
141
+ (term) => /^[a-z0-9]+$/.test(term) ? words.has(term) : fields.some((field) => field.includes(term))
142
+ );
143
+ };
144
+ const results = this.index.search(trimmedQuery, {
145
+ limit,
146
+ suggest: true
147
+ });
148
+ const seenIds = /* @__PURE__ */ new Set();
149
+ const documents = [];
150
+ for (const fieldResult of results) {
151
+ for (const docId of fieldResult.result) {
152
+ const id = String(docId);
153
+ if (seenIds.has(id)) continue;
154
+ const document = this.dataById.get(id);
155
+ if (!document || !matchesPhrase(document)) continue;
156
+ seenIds.add(id);
157
+ documents.push(document);
158
+ if (documents.length >= limit) return documents;
159
+ }
160
+ }
161
+ if (phraseTerms) {
162
+ for (const document of this.dataById.values()) {
163
+ if (seenIds.has(document.id) || !matchesPhrase(document)) continue;
164
+ seenIds.add(document.id);
165
+ documents.push(document);
166
+ if (documents.length >= limit) break;
167
+ }
168
+ }
169
+ return documents;
170
+ }
171
+ };
172
+
173
+ // src/shared/search/schema.ts
174
+ import { z as z2 } from "zod";
175
+ var searchIndexSchema = z2.array(
176
+ z2.object({
177
+ id: z2.string(),
178
+ title: z2.string(),
179
+ description: z2.string(),
180
+ content: z2.string(),
181
+ url: z2.string(),
182
+ date: z2.string(),
183
+ tags: z2.array(z2.string()),
184
+ searchTitle: z2.string(),
185
+ searchDescription: z2.string(),
186
+ searchContent: z2.string(),
187
+ searchTags: z2.string()
188
+ })
189
+ );
190
+
191
+ // packages/cli/src/content/client.ts
192
+ var ContentError = class extends Error {
193
+ constructor(message, status) {
194
+ super(message);
195
+ this.status = status;
196
+ }
197
+ status;
198
+ };
199
+ var TerminalClient = class {
200
+ constructor(site, fetcher = fetch) {
201
+ this.site = site;
202
+ this.fetcher = fetcher;
203
+ }
204
+ site;
205
+ fetcher;
206
+ cache = /* @__PURE__ */ new Map();
207
+ pending = /* @__PURE__ */ new Map();
208
+ revision;
209
+ requestOrder = 0;
210
+ searchLoad;
211
+ warning = "";
212
+ async request(path, schema, signal) {
213
+ signal?.throwIfAborted();
214
+ const url = new URL(path, this.site).href;
215
+ const running = this.pending.get(url);
216
+ if (running && running.signal === signal) return running.promise;
217
+ const promise = this.fetchDocument(url, schema, signal);
218
+ this.pending.set(url, { promise, signal });
219
+ try {
220
+ return await promise;
221
+ } finally {
222
+ if (this.pending.get(url)?.promise === promise) this.pending.delete(url);
223
+ }
224
+ }
225
+ async fetchDocument(url, schema, signal) {
226
+ const order = ++this.requestOrder;
227
+ const cached = this.cache.get(url);
228
+ const headers = new Headers({ Accept: "application/json" });
229
+ if (cached?.etag) headers.set("If-None-Match", cached.etag);
230
+ else if (cached?.modified) headers.set("If-Modified-Since", cached.modified);
231
+ try {
232
+ const response = await this.fetcher(url, {
233
+ headers,
234
+ signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(15e3)]) : AbortSignal.timeout(15e3)
235
+ });
236
+ signal?.throwIfAborted();
237
+ if (response.status >= 500) {
238
+ await response.body?.cancel();
239
+ throw new Error(`HTTP ${response.status}`);
240
+ }
241
+ let result;
242
+ if (response.status === 304 && cached) result = cached;
243
+ else {
244
+ if (!response.ok) {
245
+ await response.body?.cancel();
246
+ throw new ContentError(
247
+ response.status === 404 ? "\u5B98\u7F51\u5C1A\u672A\u63D0\u4F9B\u8BE5\u5185\u5BB9\uFF0C\u6216\u5185\u5BB9\u5DF2\u66F4\u65B0\uFF1B\u8BF7\u4F7F\u7528 /refresh \u91CD\u8BD5\u3002" : `\u5B98\u7F51\u5185\u5BB9\u8BF7\u6C42\u5931\u8D25\uFF08HTTP ${response.status}\uFF09\u3002`,
248
+ response.status
249
+ );
250
+ }
251
+ const body = await response.json();
252
+ signal?.throwIfAborted();
253
+ const value = schema.safeParse(body);
254
+ if (!value.success)
255
+ throw new ContentError("\u5B98\u7F51\u5185\u5BB9\u683C\u5F0F\u4E0E\u5F53\u524D CLI \u4E0D\u517C\u5BB9\uFF0C\u8BF7\u8FD0\u884C npx miantiao.me@latest\u3002");
256
+ result = {
257
+ value: value.data,
258
+ etag: response.headers.get("etag"),
259
+ modified: response.headers.get("last-modified")
260
+ };
261
+ }
262
+ if ((this.cache.get(url)?.order ?? 0) < order) this.cache.set(url, { ...result, order });
263
+ return result.value;
264
+ } catch (error) {
265
+ if (signal?.aborted || error instanceof ContentError) throw error;
266
+ if (error instanceof SyntaxError)
267
+ throw new ContentError("\u5B98\u7F51\u8FD4\u56DE\u4E86\u65E0\u6CD5\u8BFB\u53D6\u7684\u5185\u5BB9\uFF0C\u8BF7\u7A0D\u540E /refresh \u91CD\u8BD5\u3002");
268
+ if (cached) {
269
+ this.warning = "\u7F51\u7EDC\u4E0D\u53EF\u7528\uFF0C\u663E\u793A\u672C\u6B21\u4F1A\u8BDD\u5DF2\u8BFB\u53D6\u7684\u5185\u5BB9\uFF1B/refresh \u91CD\u8BD5\u3002";
270
+ return cached.value;
271
+ }
272
+ throw new ContentError("\u65E0\u6CD5\u8FDE\u63A5\u5B98\u7F51\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u540E\u4F7F\u7528 /refresh \u91CD\u8BD5\u3002");
273
+ }
274
+ }
275
+ async getIndex(signal) {
276
+ this.warning = "";
277
+ const path = `${terminalApiPath}/index.json`;
278
+ const loaded = await this.request(path, terminalIndexSchema, signal);
279
+ const indexUrl = new URL(path, this.site).href;
280
+ const index = this.cache.get(indexUrl)?.value ?? loaded;
281
+ if (this.revision && this.revision !== index.revision) {
282
+ for (const key of this.cache.keys()) {
283
+ if (key !== indexUrl) this.cache.delete(key);
284
+ }
285
+ this.cancelSearch();
286
+ }
287
+ this.revision = index.revision;
288
+ return index;
289
+ }
290
+ async getDocument(path, index, signal) {
291
+ for (let attempt = 0; attempt < 2; attempt++) {
292
+ const page = [...Object.values(index.pages), ...index.posts].find(
293
+ (item) => item.path === path
294
+ );
295
+ if (!page) throw new ContentError("\u627E\u4E0D\u5230\u8FD9\u4E2A\u9875\u9762\uFF0C\u8BF7\u4F7F\u7528 /blog \u6216 /search \u67E5\u627E\u3002");
296
+ try {
297
+ const document = await this.request(page.documentUrl, terminalDocumentSchema, signal);
298
+ if (document.revision === index.revision && document.path === path)
299
+ return { index, document };
300
+ } catch (error) {
301
+ if (!(error instanceof ContentError) || error.status !== 404) throw error;
302
+ }
303
+ if (attempt === 0) {
304
+ index = await this.getIndex(signal);
305
+ this.cache.delete(new URL(page.documentUrl, this.site).href);
306
+ }
307
+ }
308
+ throw new ContentError("\u5B98\u7F51\u6B63\u5728\u66F4\u65B0\uFF0C\u7D22\u5F15\u4E0E\u6B63\u6587\u6682\u65F6\u4E0D\u4E00\u81F4\uFF1B\u8BF7\u7A0D\u540E /refresh\u3002");
309
+ }
310
+ preloadSearch(index) {
311
+ if (this.searchLoad?.revision === index.revision && this.searchLoad.url === index.searchIndexUrl)
312
+ return this.searchLoad.promise;
313
+ this.cancelSearch();
314
+ const controller = new AbortController();
315
+ const load = {
316
+ revision: index.revision,
317
+ url: index.searchIndexUrl,
318
+ controller,
319
+ promise: this.request(index.searchIndexUrl, searchIndexSchema, controller.signal).then(
320
+ (data) => SearchManager.fromData(data)
321
+ )
322
+ };
323
+ this.searchLoad = load;
324
+ void load.promise.catch(() => {
325
+ if (this.searchLoad === load) this.searchLoad = void 0;
326
+ });
327
+ return load.promise;
328
+ }
329
+ cancelSearch() {
330
+ this.searchLoad?.controller.abort();
331
+ this.searchLoad = void 0;
332
+ }
333
+ async search(query, index, signal) {
334
+ signal?.throwIfAborted();
335
+ const manager = await this.preloadSearch(index);
336
+ signal?.throwIfAborted();
337
+ return manager.search(query);
338
+ }
339
+ };
340
+
341
+ // packages/cli/src/content/document-page.ts
342
+ function documentPage(index, document, site, search = "") {
343
+ const page = {
344
+ path: document.path + search,
345
+ title: document.title,
346
+ markdown: typeof document.content === "string" ? document.content : void 0,
347
+ sections: Array.isArray(document.content) ? document.content : void 0,
348
+ verb: "Read",
349
+ items: [],
350
+ command: document.path === "/" ? "/home" : document.path.replace(/\/$/, "")
351
+ };
352
+ const post = index.posts.find((post2) => post2.path === document.path);
353
+ if (post) {
354
+ page.post = post;
355
+ page.tool = { verb: "Read", target: `${post.path.replace(/^\//, "").replace(/\/$/, "")}.md` };
356
+ page.description = post.description;
357
+ }
358
+ if (document.path === index.pages.home.path) {
359
+ const menu = page.sections?.flatMap((section) => section.kind === "welcome" ? section.menu : []) ?? [];
360
+ const origins = new Set([site, index.site.url].map((url) => new URL(url).origin));
361
+ page.items = menu.map((link) => {
362
+ const destination = new URL(link.href, site);
363
+ const external = !origins.has(destination.origin);
364
+ return {
365
+ title: link.title,
366
+ description: link.command,
367
+ path: external ? destination.href : destination.pathname + destination.search,
368
+ external
369
+ };
370
+ });
371
+ }
372
+ return page;
373
+ }
374
+
375
+ // packages/cli/src/content/home.ts
376
+ function bundledHome(site) {
377
+ if (typeof define_TERMINAL_HOME_default === "undefined" || new URL(site).origin !== new URL(define_TERMINAL_HOME_default.index.site.url).origin)
378
+ return;
379
+ const { index, document } = define_TERMINAL_HOME_default;
380
+ return {
381
+ index,
382
+ page: { ...documentPage(index, document, site), postsStatus: "loading" }
383
+ };
384
+ }
385
+ function withHomePosts(page, index) {
386
+ const posts = index.posts.slice(0, 5);
387
+ return {
388
+ ...page,
389
+ postsStatus: void 0,
390
+ sections: page.sections?.map((section) => {
391
+ if (!("list" in section) || section.list.kind !== "posts") return section;
392
+ return {
393
+ ...section,
394
+ list: {
395
+ ...section.list,
396
+ items: posts,
397
+ before: section.list.before.replace(
398
+ /^◆[^\n]+/m,
399
+ `\u25C6 **Read** posts/ (\u6700\u65B0 ${posts.length} of ${index.posts.length})`
400
+ )
401
+ }
402
+ };
403
+ })
404
+ };
405
+ }
406
+
407
+ // packages/cli/src/terminal/console.tsx
408
+ import { ThreadPrimitive as ThreadPrimitive2 } from "@assistant-ui/react-ink";
409
+ import { useRef as useRef11 } from "react";
410
+
411
+ // packages/cli/src/components/chat.tsx
412
+ import { ThreadPrimitive, useAuiState } from "@assistant-ui/react-ink";
413
+ import { Box as Box11, Text as Text11 } from "ink";
414
+
415
+ // src/shared/theme.ts
416
+ var themes = [
417
+ {
418
+ id: "ayu-dark",
419
+ name: "Ayu Dark",
420
+ description: "\u6696\u8272\u70B9\u7F00\u7684\u6DF1\u8272\u4E3B\u9898",
421
+ dark: true,
422
+ color: "#0f1419"
423
+ },
424
+ {
425
+ id: "ayu-light",
426
+ name: "Ayu Light",
427
+ description: "\u660E\u4EAE\u67D4\u548C\u7684\u6D45\u8272\u4E3B\u9898",
428
+ dark: false,
429
+ color: "#fcfcfc"
430
+ },
431
+ {
432
+ id: "solarized-light",
433
+ name: "Solarized Light",
434
+ description: "\u6696\u7EB8\u8272\u80CC\u666F\u4E0E\u4F4E\u9971\u548C\u914D\u8272",
435
+ dark: false,
436
+ color: "#fdf6e3"
437
+ },
438
+ { id: "nord", name: "Nord", description: "\u6E05\u51B7\u7684\u84DD\u7070\u6697\u8272", dark: true, color: "#2e3440" },
439
+ {
440
+ id: "dracula",
441
+ name: "Dracula",
442
+ description: "\u7D2B\u7070\u80CC\u666F\u4E0E\u660E\u4EAE\u70B9\u7F00",
443
+ dark: true,
444
+ color: "#282a36"
445
+ },
446
+ { id: "gruvbox", name: "Gruvbox", description: "\u6E29\u6696\u7684\u590D\u53E4\u6697\u8272", dark: true, color: "#282828" }
447
+ ];
448
+ var themeOptions = [
449
+ ...themes,
450
+ { id: "auto", name: "Auto", description: "\u8DDF\u968F\u7CFB\u7EDF\u5207\u6362 Ayu \u6DF1\u6D45\u8272" }
451
+ ];
452
+ function parseThemePreference(input) {
453
+ const name = input?.trim().toLowerCase();
454
+ if (!name) return void 0;
455
+ return themeOptions.find((option) => option.id === name)?.id;
456
+ }
457
+ function resolveTheme(preference, prefersDark2) {
458
+ return preference === "auto" ? prefersDark2 ? "ayu-dark" : "ayu-light" : preference;
459
+ }
460
+ function getThemeName(id) {
461
+ return themeOptions.find((option) => option.id === id)?.name ?? id;
462
+ }
463
+
464
+ // src/shared/agent/tool-header.ts
465
+ function getToolHeader({ toolName, args }) {
466
+ switch (toolName) {
467
+ case "set_theme": {
468
+ const theme = typeof args?.theme === "string" ? args.theme : "";
469
+ const preference = parseThemePreference(theme);
470
+ return { verb: "Theme", target: preference ? getThemeName(preference) : theme };
471
+ }
472
+ case "open_page":
473
+ return { verb: "Open", target: typeof args?.path === "string" ? args.path : "" };
474
+ default:
475
+ return { verb: toolName, target: "" };
476
+ }
477
+ }
478
+
479
+ // packages/cli/src/terminal/theme.tsx
480
+ import { createContext, useContext, useMemo } from "react";
481
+
482
+ // packages/cli/src/components/markdown/highlighter.ts
483
+ import { Chalk } from "chalk";
484
+ import { useEffect, useState } from "react";
485
+ var ansi = new Chalk({ level: 3 });
486
+ var languages = [
487
+ "typescript",
488
+ "javascript",
489
+ "tsx",
490
+ "jsx",
491
+ "python",
492
+ "bash",
493
+ "json",
494
+ "html",
495
+ "css",
496
+ "rust",
497
+ "go",
498
+ "java"
499
+ ];
500
+ async function createCodeHighlighter(colors) {
501
+ const { createHighlighter, createCssVariablesTheme } = await import("shiki");
502
+ const theme = createCssVariablesTheme({
503
+ name: "css-variables",
504
+ variablePrefix: "--shiki-",
505
+ fontStyle: true
506
+ });
507
+ const highlighter = await createHighlighter({ themes: [theme], langs: languages });
508
+ return {
509
+ highlight(code, language) {
510
+ const lang = highlighter.getLoadedLanguages().find((loaded) => loaded === language);
511
+ if (!lang) return code;
512
+ return highlighter.codeToTokensBase(code, { lang, theme }).map(
513
+ (line) => line.map((token) => {
514
+ let style = ansi.hex(colors[token.color ?? ""] ?? colors["var(--shiki-foreground)"]);
515
+ const fontStyle = token.fontStyle ?? 0;
516
+ if (fontStyle & 1) style = style.italic;
517
+ if (fontStyle & 2) style = style.bold;
518
+ if (fontStyle & 4) style = style.underline;
519
+ return style(token.content);
520
+ }).join("")
521
+ ).join("\n");
522
+ },
523
+ dispose: () => highlighter.dispose()
524
+ };
525
+ }
526
+ function useCodeHighlighter(id) {
527
+ const [result, setResult] = useState();
528
+ useEffect(() => {
529
+ let disposed = false;
530
+ let current;
531
+ createCodeHighlighter(define_TERMINAL_CODE_COLORS_default[id]).then((highlighter) => {
532
+ if (disposed) highlighter.dispose();
533
+ else {
534
+ current = highlighter;
535
+ setResult({ id, highlight: highlighter.highlight });
536
+ }
537
+ }).catch(() => {
538
+ });
539
+ return () => {
540
+ disposed = true;
541
+ current?.dispose();
542
+ };
543
+ }, [id]);
544
+ return result?.id === id ? result.highlight : void 0;
545
+ }
546
+
547
+ // packages/cli/src/terminal/theme.tsx
548
+ import { stripVTControlCharacters } from "node:util";
549
+ import { jsx } from "react/jsx-runtime";
550
+ function cleanText(text) {
551
+ return stripVTControlCharacters(text).replace(
552
+ /[\p{Cc}\p{Cf}]/gu,
553
+ (character) => /[\n\t]/.test(character) ? character : ""
554
+ );
555
+ }
556
+ function prefersDark() {
557
+ const background = process.env.COLORFGBG?.split(";").at(-1);
558
+ return background === void 0 || Number(background) < 8;
559
+ }
560
+ var StyleContext = createContext(null);
561
+ function TerminalTheme({
562
+ preference,
563
+ width,
564
+ children
565
+ }) {
566
+ const id = resolveTheme(preference, prefersDark());
567
+ const highlighter = useCodeHighlighter(id);
568
+ const style = useMemo(() => {
569
+ const colors = define_TERMINAL_PALETTES_default[id];
570
+ return {
571
+ colors,
572
+ width,
573
+ highlighter,
574
+ markdownTheme: {
575
+ heading: { color: colors.h1, bold: true },
576
+ strong: { color: colors["fg-soft"], bold: true },
577
+ inlineCode: { color: colors.code, bold: true },
578
+ code: { color: colors.code },
579
+ link: { color: colors.link, underline: true },
580
+ quote: { color: colors.muted },
581
+ hr: { color: colors.line },
582
+ listMarker: { color: colors.muted },
583
+ tableHeader: { color: colors.fg, bold: true }
584
+ }
585
+ };
586
+ }, [id, width, highlighter]);
587
+ return /* @__PURE__ */ jsx(StyleContext, { value: style, children });
588
+ }
589
+ function useTerminalStyle() {
590
+ const style = useContext(StyleContext);
591
+ if (!style) throw new Error("TerminalTheme is required");
592
+ return style;
593
+ }
594
+
595
+ // packages/cli/src/components/markdown/markdown.tsx
596
+ import { Box as Box7, useBoxMetrics } from "ink";
597
+ import { lexer as lexer2 } from "marked";
598
+ import { useMemo as useMemo2, useRef as useRef3 } from "react";
599
+
600
+ // packages/cli/src/components/markdown/blocks.tsx
601
+ import { MarkdownText } from "@assistant-ui/react-ink-markdown";
602
+ import { Box as Box6, Text as Text7 } from "ink";
603
+ import { lexer } from "marked";
604
+
605
+ // packages/cli/src/components/home-links.tsx
606
+ import { Text as Text2 } from "ink";
607
+
608
+ // packages/cli/src/components/ui/grid.tsx
609
+ import { chunk } from "es-toolkit";
610
+ import { Box } from "ink";
611
+ import { jsx as jsx2 } from "react/jsx-runtime";
612
+ function Grid({
613
+ children,
614
+ columns = 2,
615
+ gap = 2,
616
+ rowGap = 0,
617
+ width
618
+ }) {
619
+ const cellWidth = Math.max(1, Math.floor((width - gap * (columns - 1)) / columns));
620
+ return /* @__PURE__ */ jsx2(Box, { flexDirection: "column", rowGap, children: chunk(children, columns).map((row) => /* @__PURE__ */ jsx2(Box, { columnGap: gap, children: row.map((child) => /* @__PURE__ */ jsx2(Box, { width: cellWidth, flexShrink: 0, children: child }, child.key)) }, row[0].key)) });
621
+ }
622
+
623
+ // packages/cli/src/components/ui/link.tsx
624
+ import { Text, useFocus, useFocusManager, useInput } from "ink";
625
+ import InkLink from "ink-link";
626
+ import { useId, useLayoutEffect as useLayoutEffect2 } from "react";
627
+
628
+ // packages/cli/src/terminal/mouse.tsx
629
+ import { Box as Box2, useStdin, useStdout } from "ink";
630
+ import {
631
+ createContext as createContext2,
632
+ useContext as useContext2,
633
+ useEffect as useEffect2,
634
+ useLayoutEffect,
635
+ useRef,
636
+ useState as useState2
637
+ } from "react";
638
+
639
+ // packages/cli/src/terminal/mouse-hit.ts
640
+ import { measureElement } from "ink";
641
+ function bounds(element) {
642
+ const rect = measureElement(element);
643
+ for (let parent = element.parentNode; parent; parent = parent.parentNode) {
644
+ rect.x -= Math.trunc(parent.style.contentOffsetX ?? 0);
645
+ rect.y -= Math.trunc(parent.style.contentOffsetY ?? 0);
646
+ }
647
+ return rect;
648
+ }
649
+ function contains(element, x, y) {
650
+ const rect = bounds(element);
651
+ if (x < rect.x || x >= rect.x + rect.width || y < rect.y || y >= rect.y + rect.height)
652
+ return false;
653
+ for (let parent = element; parent; parent = parent.parentNode) {
654
+ if (parent.isHidden || parent.style.display === "none") return false;
655
+ const clip = bounds(parent);
656
+ if ((parent.style.overflowX ?? parent.style.overflow) === "hidden" && (x < clip.x || x >= clip.x + clip.width))
657
+ return false;
658
+ if ((parent.style.overflowY ?? parent.style.overflow) === "hidden" && (y < clip.y || y >= clip.y + clip.height))
659
+ return false;
660
+ }
661
+ return true;
662
+ }
663
+ function paintOrder(a, b) {
664
+ const path = (element) => {
665
+ const nodes = [];
666
+ for (let node = element; node; node = node.parentNode)
667
+ nodes.unshift(node);
668
+ return nodes;
669
+ };
670
+ const left = path(a);
671
+ const right = path(b);
672
+ const index = left.findIndex((node, i) => node !== right[i]);
673
+ if (index < 0) return left.length - right.length;
674
+ if (!right[index]) return 1;
675
+ const siblings = left[index].parentNode?.childNodes ?? [];
676
+ return siblings.indexOf(left[index]) - siblings.indexOf(right[index]);
677
+ }
678
+
679
+ // packages/cli/src/terminal/mouse-input.ts
680
+ function mouseParser(onEvent) {
681
+ let pending = "";
682
+ return (chunk2) => {
683
+ const sequences = (pending + chunk2.toString()).split("\x1B").slice(1);
684
+ for (const sequence of sequences) {
685
+ const match = /^\[<(\d+);(\d+);(\d+)([Mm])/.exec(sequence);
686
+ if (!match) continue;
687
+ onEvent({
688
+ button: Number(match[1]),
689
+ x: Number(match[2]) - 1,
690
+ y: Number(match[3]) - 1,
691
+ release: match[4] === "m"
692
+ });
693
+ }
694
+ const tail = sequences.at(-1);
695
+ pending = tail !== void 0 && /^(?:\[(?:<[\d;]*)?)?$/.test(tail) ? "\x1B" + tail : "";
696
+ };
697
+ }
698
+
699
+ // packages/cli/src/terminal/mouse.tsx
700
+ import { jsx as jsx3 } from "react/jsx-runtime";
701
+ var MouseContext = createContext2(null);
702
+ function MouseProvider({ children }) {
703
+ const { stdin } = useStdin();
704
+ const { stdout } = useStdout();
705
+ const [regions] = useState2(() => /* @__PURE__ */ new Set());
706
+ useEffect2(() => {
707
+ if (!("isTTY" in stdin && stdin.isTTY) || !("isTTY" in stdout && stdout.isTTY) || process.env.TERM === "dumb" || process.env.NO_MOUSE)
708
+ return;
709
+ let pressed;
710
+ const handleInput = mouseParser((event) => {
711
+ if (event.button & 28) return;
712
+ const wheel = !!(event.button & 64);
713
+ const candidates = [...regions].filter(
714
+ (region) => region.mouseEnabled !== false && (wheel ? region.onWheel : region.onClick) && region.ref.current && contains(region.ref.current, event.x, event.y)
715
+ );
716
+ const target = candidates.sort((a, b) => paintOrder(a.ref.current, b.ref.current)).at(-1);
717
+ if (wheel) {
718
+ if ((event.button & 3) < 2) target?.onWheel?.(event.button & 1 ? 1 : -1);
719
+ } else if ((event.button & 3) === 0) {
720
+ if (!event.release) pressed = target;
721
+ else {
722
+ if (target && target === pressed) target.onClick?.();
723
+ pressed = void 0;
724
+ }
725
+ }
726
+ });
727
+ const disable = () => stdout.write("\x1B[?1000l\x1B[?1006l");
728
+ stdout.write("\x1B[?1000h\x1B[?1006h");
729
+ stdin.on("data", handleInput);
730
+ process.once("exit", disable);
731
+ return () => {
732
+ stdin.off("data", handleInput);
733
+ process.off("exit", disable);
734
+ disable();
735
+ };
736
+ }, [stdin, stdout, regions]);
737
+ return /* @__PURE__ */ jsx3(MouseContext, { value: regions, children });
738
+ }
739
+ function MouseArea({
740
+ onClick,
741
+ onWheel,
742
+ mouseEnabled = true,
743
+ ref,
744
+ ...props
745
+ }) {
746
+ const ownRef = useRef(null);
747
+ const position = ref ?? ownRef;
748
+ const regions = useContext2(MouseContext);
749
+ const [region] = useState2(() => ({ ref: position }));
750
+ useLayoutEffect(() => {
751
+ Object.assign(region, { ref: position, onClick, onWheel, mouseEnabled });
752
+ });
753
+ useLayoutEffect(() => {
754
+ regions?.add(region);
755
+ return () => {
756
+ regions?.delete(region);
757
+ };
758
+ }, [regions, region]);
759
+ return /* @__PURE__ */ jsx3(Box2, { ref: position, ...props });
760
+ }
761
+
762
+ // packages/cli/src/components/ui/link.tsx
763
+ import { jsx as jsx4 } from "react/jsx-runtime";
764
+ function Link({
765
+ href,
766
+ children,
767
+ onOpen,
768
+ onFocus,
769
+ isActive = true,
770
+ inline = false,
771
+ underline = true,
772
+ ...textProps
773
+ }) {
774
+ const id = useId();
775
+ const { isFocused } = useFocus({ id, isActive: isActive && !!href });
776
+ const { focus, focusNext, focusPrevious } = useFocusManager();
777
+ useLayoutEffect2(() => {
778
+ if (isFocused) onFocus?.();
779
+ }, [isFocused, onFocus]);
780
+ useInput(
781
+ (_input, key) => {
782
+ if (key.eventType === "release") return;
783
+ if (key.return) onOpen();
784
+ else if (key.downArrow || key.rightArrow) focusNext();
785
+ else if (key.upArrow || key.leftArrow) focusPrevious();
786
+ },
787
+ { isActive: isActive && isFocused }
788
+ );
789
+ const text = /* @__PURE__ */ jsx4(Text, { ...textProps, inverse: isFocused, underline: isFocused || underline, children });
790
+ const link = href ? /* @__PURE__ */ jsx4(InkLink, { url: href, fallback: false, children: text }) : text;
791
+ return inline ? link : /* @__PURE__ */ jsx4(
792
+ MouseArea,
793
+ {
794
+ mouseEnabled: isActive && !!href,
795
+ onClick: () => {
796
+ focus(id);
797
+ onOpen();
798
+ },
799
+ flexShrink: 1,
800
+ minWidth: 0,
801
+ children: link
802
+ }
803
+ );
804
+ }
805
+
806
+ // packages/cli/src/terminal/link.tsx
807
+ import {
808
+ createContext as createContext3,
809
+ useContext as useContext3,
810
+ useLayoutEffect as useLayoutEffect3,
811
+ useRef as useRef2
812
+ } from "react";
813
+ import { jsx as jsx5 } from "react/jsx-runtime";
814
+ var LinkNavigation = createContext3(null);
815
+ var LinkScope = createContext3(true);
816
+ var LinkPosition = createContext3(null);
817
+ function resolveLink(href, site, origin = site) {
818
+ if (!URL.canParse(href, site)) return;
819
+ const url = new URL(href, site);
820
+ if (!["http:", "https:", "mailto:"].includes(url.protocol)) return;
821
+ const external = !!url.hash || /\.[a-z\d]{2,8}$/i.test(url.pathname) || ![new URL(site).origin, new URL(origin).origin].includes(url.origin);
822
+ return {
823
+ url: url.href,
824
+ path: external ? url.href : url.pathname + url.search + url.hash,
825
+ external
826
+ };
827
+ }
828
+ function LinkRow({
829
+ children,
830
+ reveal = false,
831
+ onClick,
832
+ ...props
833
+ }) {
834
+ const position = useRef2(null);
835
+ const navigation = useContext3(LinkNavigation);
836
+ const active = useContext3(LinkScope);
837
+ useLayoutEffect3(() => {
838
+ if (reveal && position.current) navigation?.scroll.current?.reveal(position.current);
839
+ }, [reveal, navigation]);
840
+ return /* @__PURE__ */ jsx5(LinkPosition, { value: position, children: /* @__PURE__ */ jsx5(MouseArea, { ref: position, mouseEnabled: active, onClick, ...props, children }) });
841
+ }
842
+ function TerminalLink({
843
+ href,
844
+ children,
845
+ color,
846
+ bold,
847
+ underline = true,
848
+ inline = false
849
+ }) {
850
+ const navigation = useContext3(LinkNavigation);
851
+ const active = useContext3(LinkScope);
852
+ const position = useContext3(LinkPosition);
853
+ const { colors } = useTerminalStyle();
854
+ const target = resolveLink(
855
+ cleanText(href),
856
+ navigation?.site ?? "https://miantiao.me",
857
+ navigation?.origin
858
+ );
859
+ return /* @__PURE__ */ jsx5(
860
+ Link,
861
+ {
862
+ href: target?.url,
863
+ inline,
864
+ color: color ?? colors.link,
865
+ bold,
866
+ underline,
867
+ isActive: active && !!navigation,
868
+ onFocus: () => {
869
+ if (position?.current) navigation?.scroll.current?.reveal(position.current);
870
+ },
871
+ onOpen: () => {
872
+ if (target) navigation?.onSelect(target.path, target.external);
873
+ },
874
+ children
875
+ }
876
+ );
877
+ }
878
+
879
+ // packages/cli/src/components/home-links.tsx
880
+ import { jsx as jsx6, jsxs } from "react/jsx-runtime";
881
+ function homeLinks(list) {
882
+ if (list.ordered || !list.items.length) return;
883
+ const links = list.items.map((item) => {
884
+ if (item.tokens.length !== 1) return;
885
+ const block = item.tokens[0];
886
+ const inline = "tokens" in block ? block.tokens : void 0;
887
+ if (!inline) return;
888
+ const link = inline[0];
889
+ if (link?.type !== "link" || !URL.canParse(link.href)) return;
890
+ const host = inline.slice(1).map((token) => "text" in token ? token.text : "").join("").trim();
891
+ return host === new URL(link.href).hostname ? link : void 0;
892
+ });
893
+ if (links.every((link) => link !== void 0)) return links;
894
+ }
895
+ function HomeLinks({ items, width }) {
896
+ const { colors } = useTerminalStyle();
897
+ const columns = Math.min(3, Math.max(1, Math.floor((width + 2) / 30)));
898
+ return /* @__PURE__ */ jsx6(Grid, { columns, width, children: items.map((link) => /* @__PURE__ */ jsxs(LinkRow, { children: [
899
+ /* @__PURE__ */ jsx6(TerminalLink, { href: link.href, color: colors.fg, underline: false, children: cleanText(link.text) }),
900
+ /* @__PURE__ */ jsxs(Text2, { color: colors.muted, wrap: "truncate-end", children: [
901
+ " ",
902
+ new URL(link.href).hostname
903
+ ] })
904
+ ] }, link.href)) });
905
+ }
906
+
907
+ // packages/cli/src/components/markdown/inline.tsx
908
+ import { Text as Text3 } from "ink";
909
+ import { jsx as jsx7 } from "react/jsx-runtime";
910
+ function Inline({ tokens, plainLinks = false }) {
911
+ const { colors } = useTerminalStyle();
912
+ return tokens.map((token, index) => {
913
+ if (token.type === "link" || token.type === "image")
914
+ return /* @__PURE__ */ jsx7(
915
+ TerminalLink,
916
+ {
917
+ inline: true,
918
+ href: token.href,
919
+ color: plainLinks ? colors.fg : void 0,
920
+ underline: !plainLinks,
921
+ children: "tokens" in token && token.tokens ? /* @__PURE__ */ jsx7(Inline, { tokens: token.tokens, plainLinks }) : cleanText(token.text)
922
+ },
923
+ index
924
+ );
925
+ if (token.type === "br") return "\n";
926
+ if (!("text" in token)) return token.raw;
927
+ const children = "tokens" in token && token.tokens ? /* @__PURE__ */ jsx7(Inline, { tokens: token.tokens, plainLinks }) : cleanText(token.text);
928
+ let color = plainLinks ? colors.muted : void 0;
929
+ if (token.type === "codespan") color = colors.code;
930
+ else if (token.type === "strong") color = colors["fg-soft"];
931
+ return /* @__PURE__ */ jsx7(
932
+ Text3,
933
+ {
934
+ bold: token.type === "strong" || token.type === "codespan",
935
+ italic: token.type === "em",
936
+ strikethrough: token.type === "del",
937
+ color,
938
+ children
939
+ },
940
+ index
941
+ );
942
+ });
943
+ }
944
+ function hasLink(token) {
945
+ if (token.type === "link" || token.type === "image") return true;
946
+ if (token.type === "list")
947
+ return token.items.some((item) => item.tokens.some((child) => hasLink(child)));
948
+ if (token.type === "table")
949
+ return [token.header, ...token.rows].some(
950
+ (row) => row.some((cell) => cell.tokens.some((child) => hasLink(child)))
951
+ );
952
+ return "tokens" in token && !!token.tokens?.some((child) => hasLink(child));
953
+ }
954
+
955
+ // packages/cli/src/components/tool.tsx
956
+ import { Box as Box3, Text as Text4 } from "ink";
957
+ import { jsx as jsx8, jsxs as jsxs2 } from "react/jsx-runtime";
958
+ function parseTool(text) {
959
+ const match = /^◆\s+\*\*([^*]+)\*\*\s+(\S+)(?:\s+\((.+)\))?\s*$/.exec(text.trim());
960
+ if (match) return { verb: match[1], target: match[2], detail: match[3] };
961
+ }
962
+ function Tool({
963
+ verb,
964
+ target,
965
+ detail,
966
+ aside,
967
+ state
968
+ }) {
969
+ const { colors } = useTerminalStyle();
970
+ const description = state === "running" ? "\u6267\u884C\u4E2D\u2026" : detail;
971
+ return /* @__PURE__ */ jsxs2(Box3, { children: [
972
+ /* @__PURE__ */ jsx8(Text4, { color: state === "error" ? colors.err : colors.muted, children: state === "error" ? "\u2717 " : "\u25C6 " }),
973
+ /* @__PURE__ */ jsx8(Box3, { flexGrow: 1, flexShrink: 1, minWidth: 0, children: /* @__PURE__ */ jsxs2(Text4, { children: [
974
+ /* @__PURE__ */ jsx8(Text4, { color: colors.fg, bold: true, children: cleanText(verb) }),
975
+ /* @__PURE__ */ jsxs2(Text4, { color: colors.path, children: [
976
+ " ",
977
+ cleanText(target)
978
+ ] }),
979
+ description && /* @__PURE__ */ jsxs2(Text4, { color: colors.muted, children: [
980
+ " (",
981
+ cleanText(description),
982
+ ")"
983
+ ] })
984
+ ] }) }),
985
+ aside && /* @__PURE__ */ jsx8(Box3, { paddingLeft: 2, flexShrink: 0, children: /* @__PURE__ */ jsx8(Text4, { color: colors.muted, children: cleanText(aside) }) })
986
+ ] });
987
+ }
988
+
989
+ // packages/cli/src/components/ui/table.tsx
990
+ import { Box as Box4, Text as Text5 } from "ink";
991
+ import stringWidth from "string-width";
992
+ import { jsx as jsx9, jsxs as jsxs3 } from "react/jsx-runtime";
993
+ function Table({ header, rows, width }) {
994
+ const { colors } = useTerminalStyle();
995
+ const count = header.length;
996
+ if (!count) return null;
997
+ const natural = header.map(
998
+ (cell, column) => Math.max(
999
+ 1,
1000
+ ...[cell, ...rows.map((row) => row[column])].map((value) => stringWidth(value?.text ?? ""))
1001
+ )
1002
+ );
1003
+ const available = Math.max(count, width - count * 3 - 1);
1004
+ const widths = natural.map((size) => Math.min(size, Math.floor(available / count)));
1005
+ let remaining = available - widths.reduce((total, size) => total + size, 0);
1006
+ while (remaining > 0) {
1007
+ const column = natural.findIndex((size, index) => size > widths[index]);
1008
+ if (column < 0) break;
1009
+ widths[column]++;
1010
+ remaining--;
1011
+ }
1012
+ if (width < count * 4 + 1) {
1013
+ return /* @__PURE__ */ jsx9(Box4, { flexDirection: "column", gap: 1, children: rows.map((row, index) => /* @__PURE__ */ jsx9(LinkRow, { flexDirection: "column", children: row.map((cell, column) => /* @__PURE__ */ jsxs3(Text5, { color: colors["fg-soft"], children: [
1014
+ /* @__PURE__ */ jsxs3(Text5, { bold: true, children: [
1015
+ header[column]?.text,
1016
+ ": "
1017
+ ] }),
1018
+ cell.content
1019
+ ] }, column)) }, index)) });
1020
+ }
1021
+ return /* @__PURE__ */ jsx9(
1022
+ Box4,
1023
+ {
1024
+ borderStyle: "single",
1025
+ borderColor: colors.line,
1026
+ flexDirection: "column",
1027
+ alignSelf: "flex-start",
1028
+ children: [header, ...rows].map((row, rowIndex) => /* @__PURE__ */ jsx9(LinkRow, { children: row.map((cell, column) => /* @__PURE__ */ jsx9(
1029
+ Box4,
1030
+ {
1031
+ width: widths[column] + 2 + Number(column < count - 1),
1032
+ paddingX: 1,
1033
+ borderStyle: "single",
1034
+ borderTop: false,
1035
+ borderBottom: false,
1036
+ borderLeft: false,
1037
+ borderRight: column < count - 1,
1038
+ borderColor: colors.line,
1039
+ children: /* @__PURE__ */ jsx9(Text5, { bold: rowIndex === 0, color: rowIndex === 0 ? colors.fg : colors["fg-soft"], children: cell.content })
1040
+ },
1041
+ column
1042
+ )) }, rowIndex))
1043
+ }
1044
+ );
1045
+ }
1046
+
1047
+ // packages/cli/src/components/ui/thinking-block.tsx
1048
+ import { Box as Box5, Text as Text6, useFocus as useFocus2, useInput as useInput2 } from "ink";
1049
+ import { useContext as useContext4, useId as useId2, useState as useState4 } from "react";
1050
+
1051
+ // src/shared/hooks/useElapsedTime.ts
1052
+ import { useEffect as useEffect3, useState as useState3 } from "react";
1053
+ function useElapsedTime(active, live = false) {
1054
+ const [seconds, setSeconds] = useState3(0);
1055
+ useEffect3(() => {
1056
+ if (!active)
1057
+ return;
1058
+ const startedAt = Date.now();
1059
+ const elapsed = () => (Date.now() - startedAt) / 1e3;
1060
+ const finish = () => setSeconds(elapsed());
1061
+ setSeconds(0);
1062
+ if (!live)
1063
+ return finish;
1064
+ const timer = setInterval(finish, 1e3);
1065
+ return () => {
1066
+ clearInterval(timer);
1067
+ finish();
1068
+ };
1069
+ }, [active, live]);
1070
+ return seconds;
1071
+ }
1072
+
1073
+ // packages/cli/src/components/ui/thinking-block.tsx
1074
+ import { jsx as jsx10, jsxs as jsxs4 } from "react/jsx-runtime";
1075
+ function Summary({
1076
+ label,
1077
+ collapsed,
1078
+ streaming,
1079
+ onToggle
1080
+ }) {
1081
+ const { colors } = useTerminalStyle();
1082
+ const active = useContext4(LinkScope);
1083
+ const id = useId2();
1084
+ const { isFocused, focus } = useFocus2({ id, isActive: active });
1085
+ useInput2(
1086
+ (input, key) => {
1087
+ if (key.eventType !== "release" && (key.return || input === " ")) onToggle();
1088
+ },
1089
+ { isActive: active && isFocused }
1090
+ );
1091
+ return /* @__PURE__ */ jsx10(
1092
+ LinkRow,
1093
+ {
1094
+ reveal: isFocused,
1095
+ onClick: () => {
1096
+ focus(id);
1097
+ onToggle();
1098
+ },
1099
+ "aria-role": "button",
1100
+ "aria-state": { expanded: !collapsed, busy: streaming },
1101
+ children: /* @__PURE__ */ jsxs4(Text6, { color: colors.muted, inverse: isFocused, children: [
1102
+ "\u25C6 ",
1103
+ label,
1104
+ " ",
1105
+ collapsed ? "\u25B8" : "\u25BE"
1106
+ ] })
1107
+ }
1108
+ );
1109
+ }
1110
+ function ThinkingBlock({
1111
+ content = "",
1112
+ streaming = false,
1113
+ label,
1114
+ children
1115
+ }) {
1116
+ const { colors } = useTerminalStyle();
1117
+ const active = useContext4(LinkScope);
1118
+ const [collapsed, setCollapsed] = useState4(true);
1119
+ const seconds = useElapsedTime(streaming);
1120
+ return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
1121
+ /* @__PURE__ */ jsx10(
1122
+ Summary,
1123
+ {
1124
+ label: label ?? (streaming ? "\u601D\u8003\u4E2D\u2026" : `\u601D\u8003 ${seconds.toFixed(1)}s`),
1125
+ collapsed,
1126
+ streaming,
1127
+ onToggle: () => setCollapsed((value) => !value)
1128
+ }
1129
+ ),
1130
+ /* @__PURE__ */ jsx10(LinkScope, { value: active && !collapsed, children: /* @__PURE__ */ jsx10(
1131
+ Box5,
1132
+ {
1133
+ display: collapsed ? "none" : "flex",
1134
+ flexDirection: "column",
1135
+ gap: 1,
1136
+ borderStyle: "single",
1137
+ borderTop: false,
1138
+ borderRight: false,
1139
+ borderBottom: false,
1140
+ borderColor: colors.dim,
1141
+ paddingLeft: 2,
1142
+ marginTop: 1,
1143
+ children: children ?? /* @__PURE__ */ jsx10(Text6, { color: colors.muted, children: cleanText(content) })
1144
+ }
1145
+ ) })
1146
+ ] });
1147
+ }
1148
+
1149
+ // packages/cli/src/components/markdown/blocks.tsx
1150
+ import { jsx as jsx11, jsxs as jsxs5 } from "react/jsx-runtime";
1151
+ function PlainMarkdown({ text, width }) {
1152
+ const { colors, markdownTheme, highlighter } = useTerminalStyle();
1153
+ return /* @__PURE__ */ jsx11(Text7, { color: colors["fg-soft"], children: /* @__PURE__ */ jsx11(
1154
+ MarkdownText,
1155
+ {
1156
+ text,
1157
+ width,
1158
+ theme: markdownTheme,
1159
+ highlighter,
1160
+ codeWrap: true,
1161
+ hyperlinks: true,
1162
+ tableTruncate: false
1163
+ }
1164
+ ) });
1165
+ }
1166
+ function Blocks({
1167
+ tokens,
1168
+ width,
1169
+ home = false,
1170
+ plainLinks = false,
1171
+ tone
1172
+ }) {
1173
+ const { colors } = useTerminalStyle();
1174
+ return tokens.filter((token) => token.type !== "space").map((token, index) => {
1175
+ const tool2 = token.type === "paragraph" ? parseTool(token.text) : void 0;
1176
+ if (tool2) return /* @__PURE__ */ jsx11(Tool, { ...tool2 }, index);
1177
+ if (!home && !tone && !["heading", "table", "blockquote", "code"].includes(token.type) && !hasLink(token))
1178
+ return /* @__PURE__ */ jsx11(PlainMarkdown, { text: token.raw, width }, index);
1179
+ if (token.type === "code")
1180
+ return /* @__PURE__ */ jsx11(Box6, { backgroundColor: colors.well, paddingX: 2, children: /* @__PURE__ */ jsx11(PlainMarkdown, { text: token.raw, width: Math.max(1, width - 4) }) }, index);
1181
+ if (token.type === "list") {
1182
+ const links = home ? homeLinks(token) : void 0;
1183
+ if (links) return /* @__PURE__ */ jsx11(HomeLinks, { items: links, width }, index);
1184
+ return /* @__PURE__ */ jsx11(Box6, { flexDirection: "column", gap: !home && token.loose ? 1 : 0, children: token.items.map((item, itemIndex) => /* @__PURE__ */ jsxs5(Box6, { children: [
1185
+ /* @__PURE__ */ jsx11(Text7, { color: colors.muted, children: token.ordered ? `${Number(token.start) + itemIndex}. ` : "\u2022 " }),
1186
+ /* @__PURE__ */ jsx11(Box6, { flexDirection: "column", flexShrink: 1, gap: 1, minWidth: 0, children: /* @__PURE__ */ jsx11(
1187
+ Blocks,
1188
+ {
1189
+ tokens: item.tokens,
1190
+ width: width - 2,
1191
+ home,
1192
+ plainLinks: home
1193
+ }
1194
+ ) })
1195
+ ] }, itemIndex)) }, index);
1196
+ }
1197
+ if (token.type === "blockquote") {
1198
+ if (token.text.startsWith("[!DETAILS]")) {
1199
+ const content = lexer(token.text.slice("[!DETAILS]".length).trim()).filter((child) => child.type !== "space");
1200
+ const summary = content[0];
1201
+ const label = summary?.type === "paragraph" ? summary.tokens.map((child) => "text" in child ? child.text : child.raw).join("") : "\u8BE6\u7EC6\u5185\u5BB9";
1202
+ return /* @__PURE__ */ jsx11(ThinkingBlock, { label: cleanText(label), children: /* @__PURE__ */ jsx11(
1203
+ Blocks,
1204
+ {
1205
+ tokens: content.slice(1),
1206
+ width: width - 3,
1207
+ home,
1208
+ plainLinks
1209
+ }
1210
+ ) }, index);
1211
+ }
1212
+ const alert = /^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*\n?/i.exec(token.text);
1213
+ return /* @__PURE__ */ jsx11(
1214
+ Box6,
1215
+ {
1216
+ borderStyle: "single",
1217
+ borderTop: false,
1218
+ borderRight: false,
1219
+ borderBottom: false,
1220
+ borderColor: alert ? /WARNING|CAUTION/i.test(alert[1]) ? colors.warn : colors.accent : colors.dim,
1221
+ paddingLeft: 2,
1222
+ flexDirection: "column",
1223
+ gap: 1,
1224
+ children: /* @__PURE__ */ jsx11(
1225
+ Blocks,
1226
+ {
1227
+ tokens: alert ? lexer(token.text.slice(alert[0].length)) : token.tokens,
1228
+ width: width - 3,
1229
+ home,
1230
+ plainLinks,
1231
+ tone: alert ? void 0 : colors.muted
1232
+ }
1233
+ )
1234
+ },
1235
+ index
1236
+ );
1237
+ }
1238
+ if (token.type === "table") {
1239
+ const cells = (row) => row.map((cell) => ({
1240
+ text: cleanText(cell.text),
1241
+ content: /* @__PURE__ */ jsx11(Inline, { tokens: cell.tokens })
1242
+ }));
1243
+ return /* @__PURE__ */ jsx11(
1244
+ Table,
1245
+ {
1246
+ header: cells(token.header),
1247
+ rows: token.rows.map(cells),
1248
+ width
1249
+ },
1250
+ index
1251
+ );
1252
+ }
1253
+ let color = tone ?? colors["fg-soft"];
1254
+ if (token.type === "heading") {
1255
+ const level = Math.max(1, token.depth - 1);
1256
+ color = colors[level <= 3 ? `h${level}` : "muted"];
1257
+ if (home && token.depth === 1) color = colors.fg;
1258
+ }
1259
+ return /* @__PURE__ */ jsx11(LinkRow, { children: /* @__PURE__ */ jsx11(Text7, { bold: token.type === "heading", color, children: /* @__PURE__ */ jsx11(
1260
+ Inline,
1261
+ {
1262
+ tokens: "tokens" in token ? token.tokens ?? [] : [],
1263
+ plainLinks
1264
+ }
1265
+ ) }) }, index);
1266
+ });
1267
+ }
1268
+
1269
+ // packages/cli/src/components/markdown/markdown.tsx
1270
+ import { jsx as jsx12 } from "react/jsx-runtime";
1271
+ function Markdown({
1272
+ text,
1273
+ width: availableWidth,
1274
+ home = false,
1275
+ compact = false
1276
+ }) {
1277
+ const { width } = useTerminalStyle();
1278
+ const container = useRef3(null);
1279
+ const { clientWidth } = useBoxMetrics(container);
1280
+ const tokens = useMemo2(() => lexer2(text), [text]);
1281
+ const available = Math.max(1, availableWidth ?? (clientWidth || width - 6));
1282
+ const native = tokens.some(
1283
+ (token) => home && token.type === "list" || ["heading", "table", "blockquote", "code"].includes(token.type) || hasLink(token)
1284
+ );
1285
+ return /* @__PURE__ */ jsx12(Box7, { ref: container, flexDirection: "column", gap: compact ? 0 : 1, minWidth: 0, children: native ? /* @__PURE__ */ jsx12(Blocks, { tokens, width: available, home }) : /* @__PURE__ */ jsx12(PlainMarkdown, { text, width: available }) });
1286
+ }
1287
+
1288
+ // packages/cli/src/components/blocks.tsx
1289
+ import { Box as Box10, Text as Text10 } from "ink";
1290
+
1291
+ // packages/cli/src/components/page-list.tsx
1292
+ import { Box as Box8, Text as Text8, useBoxMetrics as useBoxMetrics2 } from "ink";
1293
+ import { useRef as useRef4 } from "react";
1294
+
1295
+ // src/shared/time.ts
1296
+ import { parseISO } from "date-fns/parseISO";
1297
+ var shortDateFormatter = new Intl.DateTimeFormat("en-CA", {
1298
+ timeZone: "Asia/Shanghai",
1299
+ year: "numeric",
1300
+ month: "2-digit",
1301
+ day: "2-digit"
1302
+ });
1303
+ function formatShortDate(date) {
1304
+ const value = date instanceof Date ? date : parseISO(/^\d{4}-\d{2}-\d{2}$/.test(date) ? `${date}T00:00:00+08:00` : date);
1305
+ return shortDateFormatter.format(value);
1306
+ }
1307
+
1308
+ // packages/cli/src/components/page-list.tsx
1309
+ import { jsx as jsx13, jsxs as jsxs6 } from "react/jsx-runtime";
1310
+ function PageList({ items, compact = false }) {
1311
+ const { colors, width } = useTerminalStyle();
1312
+ const container = useRef4(null);
1313
+ const { clientWidth } = useBoxMetrics2(container);
1314
+ const stacked = !compact && (clientWidth || width - 6) < 55;
1315
+ return /* @__PURE__ */ jsx13(
1316
+ Box8,
1317
+ {
1318
+ ref: container,
1319
+ flexDirection: "column",
1320
+ marginTop: compact ? 1 : 0,
1321
+ maxWidth: compact ? 44 : void 0,
1322
+ children: items.map((item) => /* @__PURE__ */ jsxs6(
1323
+ LinkRow,
1324
+ {
1325
+ flexDirection: compact ? "row" : "column",
1326
+ justifyContent: compact ? "space-between" : void 0,
1327
+ marginBottom: compact ? 0 : 1,
1328
+ children: [
1329
+ /* @__PURE__ */ jsxs6(Box8, { flexDirection: stacked ? "column" : "row", children: [
1330
+ item.date && /* @__PURE__ */ jsxs6(Text8, { color: colors.muted, children: [
1331
+ formatShortDate(item.date),
1332
+ " "
1333
+ ] }),
1334
+ /* @__PURE__ */ jsxs6(Box8, { flexShrink: 1, minWidth: 0, children: [
1335
+ !compact && /* @__PURE__ */ jsx13(Text8, { color: colors.muted, children: "\u2022 " }),
1336
+ /* @__PURE__ */ jsx13(Box8, { flexShrink: 1, minWidth: 0, children: /* @__PURE__ */ jsx13(TerminalLink, { href: item.path, color: colors.fg, bold: compact, underline: false, children: cleanText(item.title) }) })
1337
+ ] })
1338
+ ] }),
1339
+ item.description && /* @__PURE__ */ jsx13(Box8, { paddingLeft: compact ? 0 : item.date && !stacked ? 13 : 2, children: /* @__PURE__ */ jsx13(Text8, { color: colors.muted, children: cleanText(item.description) }) })
1340
+ ]
1341
+ },
1342
+ item.path
1343
+ ))
1344
+ }
1345
+ );
1346
+ }
1347
+
1348
+ // packages/cli/src/components/home-list.tsx
1349
+ import { Box as Box9, Text as Text9, useBoxMetrics as useBoxMetrics3 } from "ink";
1350
+ import { useContext as useContext5, useRef as useRef5 } from "react";
1351
+ import { jsx as jsx14, jsxs as jsxs7 } from "react/jsx-runtime";
1352
+ function formatStars(stars) {
1353
+ return stars >= 1e3 ? `${(stars / 1e3).toFixed(1)}k` : String(stars);
1354
+ }
1355
+ function HomeList({ list }) {
1356
+ const { colors, width: availableWidth } = useTerminalStyle();
1357
+ const active = useContext5(LinkScope);
1358
+ const container = useRef5(null);
1359
+ const { clientWidth } = useBoxMetrics3(container);
1360
+ const width = clientWidth || availableWidth - 6;
1361
+ if (list.kind === "posts")
1362
+ return /* @__PURE__ */ jsx14(Box9, { ref: container, flexDirection: "column", children: Array.from({ length: Math.max(5, list.items.length) }, (_, slot) => slot).map((slot) => {
1363
+ const post = list.items[slot];
1364
+ return /* @__PURE__ */ jsx14(LinkScope, { value: active && !!post, children: /* @__PURE__ */ jsxs7(
1365
+ LinkRow,
1366
+ {
1367
+ display: post ? "flex" : "none",
1368
+ flexDirection: width < 55 ? "column" : "row",
1369
+ children: [
1370
+ /* @__PURE__ */ jsxs7(Text9, { color: colors.muted, children: [
1371
+ post ? formatShortDate(post.date) : "",
1372
+ " "
1373
+ ] }),
1374
+ /* @__PURE__ */ jsxs7(Box9, { flexShrink: 1, minWidth: 0, children: [
1375
+ /* @__PURE__ */ jsx14(Text9, { color: colors.muted, children: "\u2022 " }),
1376
+ /* @__PURE__ */ jsx14(Box9, { flexShrink: 1, minWidth: 0, children: /* @__PURE__ */ jsx14(TerminalLink, { href: post?.path ?? "/", color: colors.fg, underline: false, children: post ? cleanText(post.title) : "" }) })
1377
+ ] })
1378
+ ]
1379
+ }
1380
+ ) }, slot);
1381
+ }) });
1382
+ const nameWidth = 20;
1383
+ return /* @__PURE__ */ jsx14(Box9, { ref: container, flexDirection: "column", children: list.items.map((project) => /* @__PURE__ */ jsxs7(LinkRow, { flexDirection: width >= 55 ? "row" : "column", children: [
1384
+ /* @__PURE__ */ jsxs7(Box9, { flexShrink: 0, children: [
1385
+ /* @__PURE__ */ jsx14(Box9, { width: nameWidth, flexShrink: 1, children: /* @__PURE__ */ jsx14(TerminalLink, { href: project.url, color: colors.fg, bold: true, underline: false, children: cleanText(project.title) }) }),
1386
+ /* @__PURE__ */ jsx14(Box9, { width: 10, flexShrink: 0, children: project.repo ? /* @__PURE__ */ jsxs7(
1387
+ TerminalLink,
1388
+ {
1389
+ href: `https://github.com/miantiao-me/${project.repo}`,
1390
+ color: colors.warn,
1391
+ underline: false,
1392
+ children: [
1393
+ "\u2605 ",
1394
+ formatStars(project.stars)
1395
+ ]
1396
+ }
1397
+ ) : /* @__PURE__ */ jsxs7(Text9, { color: colors.warn, children: [
1398
+ "\u2605 ",
1399
+ formatStars(project.stars)
1400
+ ] }) })
1401
+ ] }),
1402
+ /* @__PURE__ */ jsx14(Box9, { flexGrow: 1, flexShrink: 1, children: /* @__PURE__ */ jsx14(Text9, { color: colors["fg-soft"], children: cleanText(project.description) }) })
1403
+ ] }, project.url)) });
1404
+ }
1405
+
1406
+ // packages/cli/src/components/blocks.tsx
1407
+ import { Fragment, jsx as jsx15, jsxs as jsxs8 } from "react/jsx-runtime";
1408
+ function Prompt({
1409
+ text,
1410
+ command = false,
1411
+ aside
1412
+ }) {
1413
+ const { colors } = useTerminalStyle();
1414
+ return /* @__PURE__ */ jsxs8(Box10, { width: "100%", backgroundColor: colors.band, paddingX: 3, paddingY: 1, children: [
1415
+ /* @__PURE__ */ jsx15(Text10, { color: colors.prompt, children: "\u276F " }),
1416
+ /* @__PURE__ */ jsx15(Box10, { flexGrow: 1, flexShrink: 1, minWidth: 0, children: /* @__PURE__ */ jsx15(Text10, { color: command ? colors.skill : colors.fg, children: cleanText(text) }) }),
1417
+ aside && /* @__PURE__ */ jsx15(Box10, { paddingLeft: 2, flexShrink: 0, children: /* @__PURE__ */ jsx15(Text10, { color: colors.muted, children: cleanText(aside) }) })
1418
+ ] });
1419
+ }
1420
+ function Welcome({
1421
+ section,
1422
+ ...selection
1423
+ }) {
1424
+ const { colors, width } = useTerminalStyle();
1425
+ return /* @__PURE__ */ jsxs8(
1426
+ Box10,
1427
+ {
1428
+ borderStyle: "round",
1429
+ borderColor: colors.line,
1430
+ paddingX: 2,
1431
+ paddingY: 1,
1432
+ gap: 5,
1433
+ alignItems: "center",
1434
+ children: [
1435
+ section.logo && width >= 90 && /* @__PURE__ */ jsx15(Text10, { color: colors.muted, children: cleanText(section.logo) }),
1436
+ /* @__PURE__ */ jsxs8(Box10, { flexDirection: "column", flexGrow: 1, flexShrink: 1, minWidth: 0, children: [
1437
+ /* @__PURE__ */ jsx15(Markdown, { text: section.markdown, home: true, compact: true }),
1438
+ selection.items.length > 0 && /* @__PURE__ */ jsx15(PageList, { ...selection, compact: true })
1439
+ ] })
1440
+ ]
1441
+ }
1442
+ );
1443
+ }
1444
+ function HomeSections({ page }) {
1445
+ const { colors } = useTerminalStyle();
1446
+ return /* @__PURE__ */ jsx15(Box10, { flexDirection: "column", gap: 1, children: page.sections?.map(
1447
+ (section) => section.kind === "welcome" ? /* @__PURE__ */ jsx15(Welcome, { section, items: page.items }, "welcome") : /* @__PURE__ */ jsxs8(Box10, { flexDirection: "column", gap: 1, children: [
1448
+ /* @__PURE__ */ jsx15(Prompt, { text: section.prompt }),
1449
+ /* @__PURE__ */ jsx15(Box10, { flexDirection: "column", paddingX: 3, gap: 1, children: "list" in section ? /* @__PURE__ */ jsxs8(Fragment, { children: [
1450
+ section.list.before && /* @__PURE__ */ jsx15(Markdown, { text: section.list.before, home: true }),
1451
+ section.list.kind === "posts" && !section.list.items.length && /* @__PURE__ */ jsx15(Text10, { color: colors.muted, children: page.postsStatus === "error" ? "\u6587\u7AE0\u6682\u65F6\u65E0\u6CD5\u52A0\u8F7D\uFF0C\u8F93\u5165 /refresh \u91CD\u8BD5\u3002" : page.postsStatus === "loading" ? "\u6B63\u5728\u52A0\u8F7D\u6587\u7AE0\u2026" : "\u6682\u65E0\u6587\u7AE0\u3002" }),
1452
+ /* @__PURE__ */ jsx15(HomeList, { list: section.list }),
1453
+ section.list.after && /* @__PURE__ */ jsx15(Markdown, { text: section.list.after, home: true })
1454
+ ] }) : /* @__PURE__ */ jsx15(Markdown, { text: section.markdown, home: true }) })
1455
+ ] }, section.prompt)
1456
+ ) });
1457
+ }
1458
+
1459
+ // packages/cli/src/components/chat.tsx
1460
+ import { jsx as jsx16, jsxs as jsxs9 } from "react/jsx-runtime";
1461
+ function MessageBody({ message }) {
1462
+ const { colors } = useTerminalStyle();
1463
+ if (message.role === "user") {
1464
+ return /* @__PURE__ */ jsx16(Box11, { flexDirection: "column", marginY: 1, children: /* @__PURE__ */ jsx16(
1465
+ Prompt,
1466
+ {
1467
+ aside: message.createdAt.toLocaleTimeString("zh-CN", {
1468
+ hour: "2-digit",
1469
+ minute: "2-digit"
1470
+ }),
1471
+ text: message.content.filter((part) => part.type === "text").map((part) => part.text).join("\n")
1472
+ }
1473
+ ) });
1474
+ }
1475
+ if (message.role !== "assistant") return null;
1476
+ return /* @__PURE__ */ jsxs9(Box11, { flexDirection: "column", paddingX: 3, marginBottom: 1, children: [
1477
+ message.content.map((part, index) => {
1478
+ if (part.type === "text") return /* @__PURE__ */ jsx16(Markdown, { text: part.text }, index);
1479
+ if (part.type === "reasoning") {
1480
+ return /* @__PURE__ */ jsx16(
1481
+ ThinkingBlock,
1482
+ {
1483
+ content: part.text,
1484
+ streaming: message.status.type === "running" && index === message.content.length - 1
1485
+ },
1486
+ index
1487
+ );
1488
+ }
1489
+ if (part.type === "tool-call") {
1490
+ const { verb, target } = getToolHeader(part);
1491
+ const failed = part.isError || part.result === void 0 && message.status.type === "incomplete";
1492
+ return /* @__PURE__ */ jsx16(
1493
+ Tool,
1494
+ {
1495
+ verb,
1496
+ target,
1497
+ detail: failed ? "\u6267\u884C\u5931\u8D25" : part.result === void 0 ? void 0 : cleanText(String(part.result)),
1498
+ state: part.result === void 0 && message.status.type === "running" ? "running" : failed ? "error" : void 0
1499
+ },
1500
+ part.toolCallId
1501
+ );
1502
+ }
1503
+ return null;
1504
+ }),
1505
+ message.status.type === "incomplete" && message.status.reason === "error" && /* @__PURE__ */ jsx16(Text11, { color: colors.err, children: "\u25C6 AI \u6682\u65F6\u65E0\u6CD5\u56DE\u590D\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002" })
1506
+ ] });
1507
+ }
1508
+ function Message() {
1509
+ const message = useAuiState((state) => state.message);
1510
+ return /* @__PURE__ */ jsx16(MessageBody, { message });
1511
+ }
1512
+ function ChatThread() {
1513
+ return /* @__PURE__ */ jsx16(ThreadPrimitive.Messages, { components: { Message } });
1514
+ }
1515
+
1516
+ // packages/cli/src/components/composer.tsx
1517
+ import { useAui, useAuiState as useAuiState2 } from "@assistant-ui/react-ink";
1518
+ import {
1519
+ Box as Box19,
1520
+ Text as Text19,
1521
+ useBoxMetrics as useBoxMetrics7,
1522
+ useFocus as useFocus5,
1523
+ useFocusManager as useFocusManager2,
1524
+ useInput as useInput8
1525
+ } from "ink";
1526
+ import { useEffect as useEffect6, useEffectEvent, useRef as useRef9 } from "react";
1527
+
1528
+ // src/shared/agent/appearance.ts
1529
+ var composerPlaceholder = "\u95EE\u70B9\u4EC0\u4E48\uFF0C\u6216\u8F93\u5165 / \u67E5\u770B\u547D\u4EE4";
1530
+ var composerBrand = { mark: "<MT/>", model: "V3.14 Pro" };
1531
+
1532
+ // src/shared/tags.ts
1533
+ function getTagSlug(tag) {
1534
+ return encodeURIComponent(tag);
1535
+ }
1536
+ function getTagPath(tag) {
1537
+ return `/tags/${getTagSlug(tag)}/`;
1538
+ }
1539
+
1540
+ // src/shared/agent/commands.ts
1541
+ var commands = [
1542
+ { id: "help", description: "\u67E5\u770B\u53EF\u7528\u547D\u4EE4" },
1543
+ { id: "theme", args: "[\u4E3B\u9898]", description: "\u5207\u6362\u4E3B\u9898\uFF0C\u4E0D\u5E26\u53C2\u6570\u65F6\u6253\u5F00\u9009\u62E9\u5668" },
1544
+ { id: "clear", description: "\u6E05\u7A7A\u5F53\u524D\u5BF9\u8BDD" },
1545
+ { id: "home", description: "\u6253\u5F00\u9996\u9875" },
1546
+ { id: "blog", description: "\u6D4F\u89C8\u5168\u90E8\u6587\u7AE0" },
1547
+ { id: "projects", description: "\u67E5\u770B\u6298\u817E\u7684\u9879\u76EE" },
1548
+ { id: "tags", args: "[\u6807\u7B7E]", description: "\u6309\u6807\u7B7E\u6D4F\u89C8\u6587\u7AE0" },
1549
+ { id: "search", args: "[\u5173\u952E\u8BCD]", description: "\u641C\u7D22\u6587\u7AE0" },
1550
+ { id: "about", description: "\u5173\u4E8E\u672C\u7AD9" },
1551
+ { id: "rss", description: "\u8BA2\u9605 RSS" }
1552
+ ];
1553
+ var pageCommands = {
1554
+ home: "/",
1555
+ blog: "/blog/",
1556
+ projects: "/projects/",
1557
+ tags: "/tags/",
1558
+ about: "/about/"
1559
+ };
1560
+ function getCommandPath(name, args = "") {
1561
+ if (name === "blog" && /^[1-9]\d*$/.test(args))
1562
+ return args === "1" ? "/blog/" : `/blog/${args}/`;
1563
+ if (name === "search")
1564
+ return getSearchPath(args);
1565
+ if (name === "tags" && args)
1566
+ return getTagPath(args);
1567
+ return Object.hasOwn(pageCommands, name) ? pageCommands[name] : void 0;
1568
+ }
1569
+ function parseCommand(input) {
1570
+ const match = input.trim().match(/^\/(\S+)(?:\s+([\s\S]*))?$/);
1571
+ if (!match)
1572
+ return null;
1573
+ return { name: match[1].toLowerCase(), args: (match[2] ?? "").trim() };
1574
+ }
1575
+ function getSearchPath(query) {
1576
+ return query ? `/search/?q=${encodeURIComponent(query)}` : "/search/";
1577
+ }
1578
+
1579
+ // packages/cli/src/components/command-menu.tsx
1580
+ import { Box as Box12, Text as Text12, useInput as useInput3 } from "ink";
1581
+ import { useImperativeHandle, useState as useState5 } from "react";
1582
+
1583
+ // packages/cli/src/app/commands.ts
1584
+ var terminalCommands = [
1585
+ ...commands,
1586
+ { id: "read", args: "<slug \u6216\u8DEF\u5F84>", description: "\u9605\u8BFB\u6587\u7AE0\u5168\u6587" },
1587
+ { id: "open", args: "[URL]", description: "\u5728\u6D4F\u89C8\u5668\u6253\u5F00\u5F53\u524D\u9875\u9762\u6216\u94FE\u63A5" },
1588
+ { id: "refresh", description: "\u4ECE\u5B98\u7F51\u5237\u65B0\u5F53\u524D\u9875\u9762" },
1589
+ { id: "quit", description: "\u9000\u51FA" }
1590
+ ];
1591
+ function getCommandSuggestions(text) {
1592
+ const match = text.match(/^\/([^\s]*)$/);
1593
+ if (!match) return null;
1594
+ const query = match[1].toLowerCase();
1595
+ return terminalCommands.filter(
1596
+ (command) => command.id.includes(query) || command.description.toLowerCase().includes(query)
1597
+ );
1598
+ }
1599
+ function numberedItem(input, page) {
1600
+ if (!/^\d+$/.test(input)) return void 0;
1601
+ const item = page.items[Number(input) - 1];
1602
+ if (!item) throw new Error("\u8BE5\u7F16\u53F7\u8D85\u51FA\u5F53\u524D\u5217\u8868\u8303\u56F4\uFF0C\u8BF7\u4F7F\u7528\u6587\u7AE0 slug \u6216\u8DEF\u5F84\u3002");
1603
+ return item;
1604
+ }
1605
+ function resolveReadPath(input, page) {
1606
+ if (!input) throw new Error("\u8F93\u5165 /read <slug \u6216\u7AD9\u5185\u8DEF\u5F84> \u9605\u8BFB\u6587\u7AE0\u3002");
1607
+ const item = numberedItem(input, page);
1608
+ if (item) {
1609
+ if (item.external || !item.path.startsWith("/posts/"))
1610
+ throw new Error("\u8FD9\u4E2A\u7F16\u53F7\u4E0D\u662F\u6587\u7AE0\uFF0C\u8BF7\u4F7F\u7528 /blog \u6216 /search \u67E5\u627E\u6587\u7AE0\u3002");
1611
+ return item.path;
1612
+ }
1613
+ return input.startsWith("/") ? input : `/posts/${input.replace(/\/$/, "")}/`;
1614
+ }
1615
+ function resolveOpenTarget(input, page) {
1616
+ return numberedItem(input, page)?.path ?? (input || page.path);
1617
+ }
1618
+ function resolveTagPath(input, index) {
1619
+ if (!index.tags.length) return getTagPath(input);
1620
+ const tag = index.tags.find((tag2) => tag2.name === input);
1621
+ if (!tag) throw new Error("\u627E\u4E0D\u5230\u8FD9\u4E2A\u6807\u7B7E\uFF0C\u8BF7\u4F7F\u7528 /tags \u67E5\u770B\u53EF\u7528\u6807\u7B7E\u3002");
1622
+ return tag.path;
1623
+ }
1624
+
1625
+ // packages/cli/src/components/command-menu.tsx
1626
+ import { jsx as jsx17, jsxs as jsxs10 } from "react/jsx-runtime";
1627
+ function CommandMenu({
1628
+ text,
1629
+ focused,
1630
+ availableHeight,
1631
+ onSelect,
1632
+ ref
1633
+ }) {
1634
+ const { colors } = useTerminalStyle();
1635
+ const [selected, setSelected] = useState5(0);
1636
+ const [dismissed, setDismissed] = useState5(false);
1637
+ const suggestions = getCommandSuggestions(text);
1638
+ const matches = suggestions ?? [];
1639
+ const open = focused && suggestions !== null && !dismissed;
1640
+ const visibleCount = Math.max(1, Math.min(8, availableHeight - 4));
1641
+ const start = Math.max(0, selected - visibleCount + 1);
1642
+ useImperativeHandle(ref, () => ({
1643
+ isOpen: () => open,
1644
+ submit: (input) => {
1645
+ if (!open || input !== text || !matches.length) return false;
1646
+ onSelect(matches[selected]);
1647
+ return true;
1648
+ },
1649
+ dismiss: () => {
1650
+ if (!open) return false;
1651
+ setDismissed(true);
1652
+ return true;
1653
+ }
1654
+ }));
1655
+ function moveSelection(direction) {
1656
+ if (matches.length)
1657
+ setSelected((index) => (index + direction + matches.length) % matches.length);
1658
+ }
1659
+ useInput3(
1660
+ (_input, key) => {
1661
+ const direction = Number(key.downArrow) - Number(key.upArrow);
1662
+ if (direction) moveSelection(direction);
1663
+ },
1664
+ { isActive: open }
1665
+ );
1666
+ if (!open) return null;
1667
+ return /* @__PURE__ */ jsxs10(
1668
+ MouseArea,
1669
+ {
1670
+ onClick: () => {
1671
+ },
1672
+ onWheel: moveSelection,
1673
+ flexDirection: "column",
1674
+ borderStyle: "round",
1675
+ borderColor: colors.line,
1676
+ backgroundColor: colors.bg,
1677
+ paddingX: 1,
1678
+ marginBottom: 1,
1679
+ children: [
1680
+ matches.length === 0 && /* @__PURE__ */ jsx17(Text12, { color: colors.muted, children: "\u6CA1\u6709\u5339\u914D\u7684\u547D\u4EE4" }),
1681
+ matches.slice(start, start + visibleCount).map((command, index) => {
1682
+ const active = start + index === selected;
1683
+ return /* @__PURE__ */ jsxs10(
1684
+ MouseArea,
1685
+ {
1686
+ onClick: () => onSelect(command),
1687
+ height: 1,
1688
+ backgroundColor: active ? colors.band : void 0,
1689
+ children: [
1690
+ /* @__PURE__ */ jsx17(Box12, { width: 14, flexShrink: 0, children: /* @__PURE__ */ jsxs10(Text12, { color: colors.skill, children: [
1691
+ active ? "\u25B8" : " ",
1692
+ " /",
1693
+ command.id
1694
+ ] }) }),
1695
+ /* @__PURE__ */ jsx17(Box12, { flexGrow: 1, flexShrink: 1, children: /* @__PURE__ */ jsx17(Text12, { color: colors.muted, wrap: "truncate-end", children: command.description }) })
1696
+ ]
1697
+ },
1698
+ command.id
1699
+ );
1700
+ }),
1701
+ /* @__PURE__ */ jsx17(Text12, { color: colors.muted, wrap: "truncate-end", children: "\u2191\u2193 \u9009\u62E9 \xB7 Enter \u786E\u8BA4 \xB7 Tab \u5207\u6362\u7126\u70B9 \xB7 Esc \u5173\u95ED" })
1702
+ ]
1703
+ }
1704
+ );
1705
+ }
1706
+
1707
+ // packages/cli/src/components/composer-input.tsx
1708
+ import { Box as Box13, Text as Text13, useBoxMetrics as useBoxMetrics4, useInput as useInput4, useWindowSize } from "ink";
1709
+ import { useRef as useRef6, useState as useState6 } from "react";
1710
+
1711
+ // packages/cli/src/terminal/input-editor.ts
1712
+ import stringWidth2 from "string-width";
1713
+ var segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
1714
+ function editorLines(text, width) {
1715
+ const lines = [];
1716
+ let start = 0;
1717
+ let columns = 0;
1718
+ for (const { segment, index } of segmenter.segment(text)) {
1719
+ if (segment === "\n") {
1720
+ lines.push({ start, end: index, text: text.slice(start, index) });
1721
+ if (columns >= width) lines.push({ start: index, end: index, text: "" });
1722
+ start = index + 1;
1723
+ columns = 0;
1724
+ continue;
1725
+ }
1726
+ const size = stringWidth2(segment);
1727
+ if (columns && columns + size > width) {
1728
+ lines.push({ start, end: index, text: text.slice(start, index) });
1729
+ start = index;
1730
+ columns = 0;
1731
+ }
1732
+ columns += size;
1733
+ }
1734
+ lines.push({ start, end: text.length, text: text.slice(start) });
1735
+ if (columns >= width) lines.push({ start: text.length, end: text.length, text: "" });
1736
+ return lines;
1737
+ }
1738
+ function cursorRow(lines, cursor) {
1739
+ return lines.findLastIndex((line) => line.start <= cursor);
1740
+ }
1741
+ function editText(state, start, end, inserted = "") {
1742
+ return {
1743
+ text: state.text.slice(0, start) + inserted + state.text.slice(end),
1744
+ cursor: start + inserted.length
1745
+ };
1746
+ }
1747
+ function moveCursor(state, direction) {
1748
+ const boundaries = [...segmenter.segment(state.text)].map((part) => part.index);
1749
+ boundaries.push(state.text.length);
1750
+ const cursor = direction < 0 ? boundaries.findLast((index) => index < state.cursor) ?? 0 : boundaries.find((index) => index > state.cursor) ?? state.text.length;
1751
+ return { text: state.text, cursor };
1752
+ }
1753
+ function moveVertical(state, width, direction) {
1754
+ const lines = editorLines(state.text, width);
1755
+ const row = cursorRow(lines, state.cursor);
1756
+ const column = state.column ?? stringWidth2(state.text.slice(lines[row].start, state.cursor));
1757
+ const target = lines[Math.max(0, Math.min(lines.length - 1, row + direction))];
1758
+ let cursor = target.start;
1759
+ let cells = 0;
1760
+ for (const { segment, index } of segmenter.segment(target.text)) {
1761
+ cells += stringWidth2(segment);
1762
+ if (cells > column) break;
1763
+ cursor = target.start + index + segment.length;
1764
+ }
1765
+ return { text: state.text, cursor, column };
1766
+ }
1767
+ function wordCursor(state, direction) {
1768
+ if (direction < 0) {
1769
+ return state.cursor - (state.text.slice(0, state.cursor).match(/\S+\s*$|\s+$/u)?.[0].length ?? 0);
1770
+ }
1771
+ return state.cursor + (state.text.slice(state.cursor).match(/^\s*\S*/u)?.[0].length ?? 0);
1772
+ }
1773
+ function updateEditor(state, input, key, width) {
1774
+ const { text, cursor } = state;
1775
+ if (key.escape || key.tab || key.pageUp || key.pageDown) return;
1776
+ if (key.upArrow || key.downArrow) return moveVertical(state, width, key.upArrow ? -1 : 1);
1777
+ if (key.leftArrow || key.rightArrow) {
1778
+ const direction = key.leftArrow ? -1 : 1;
1779
+ return key.ctrl || key.meta ? { text, cursor: wordCursor(state, direction) } : moveCursor(state, direction);
1780
+ }
1781
+ const start = cursor === 0 ? 0 : text.lastIndexOf("\n", cursor - 1) + 1;
1782
+ const nextBreak = text.indexOf("\n", cursor);
1783
+ const end = nextBreak < 0 ? text.length : nextBreak;
1784
+ if (key.home || key.ctrl && input === "a") return { text, cursor: start };
1785
+ if (key.end || key.ctrl && input === "e") return { text, cursor: end };
1786
+ if (key.return || key.ctrl && input === "j") return editText(state, cursor, cursor, "\n");
1787
+ if (key.ctrl) {
1788
+ if (input === "u") return editText(state, start, cursor);
1789
+ if (input === "k") return editText(state, cursor, end);
1790
+ if (input === "w") return editText(state, wordCursor(state, -1), cursor);
1791
+ return;
1792
+ }
1793
+ if (key.backspace) return editText(state, moveCursor(state, -1).cursor, cursor);
1794
+ if (key.delete) return editText(state, cursor, moveCursor(state, 1).cursor);
1795
+ if (key.meta) return;
1796
+ const inserted = input.replace(/\r\n?/g, "\n").replace(/\t/g, " ").replace(new RegExp("\\p{Cc}", "gu"), (character) => character === "\n" ? character : "");
1797
+ if (inserted) return editText(state, cursor, cursor, inserted);
1798
+ }
1799
+
1800
+ // packages/cli/src/components/composer-input.tsx
1801
+ import { Fragment as Fragment2, jsx as jsx18, jsxs as jsxs11 } from "react/jsx-runtime";
1802
+ function ComposerInput({
1803
+ value,
1804
+ focused,
1805
+ onChange,
1806
+ onSubmit,
1807
+ ignoreArrows,
1808
+ placeholder = composerPlaceholder
1809
+ }) {
1810
+ const { colors, width: availableWidth } = useTerminalStyle();
1811
+ const { rows } = useWindowSize();
1812
+ const container = useRef6(null);
1813
+ const { clientWidth } = useBoxMetrics4(container);
1814
+ const width = Math.max(1, clientWidth || availableWidth - 5);
1815
+ const maximum = Math.max(1, Math.min(10, rows - 8));
1816
+ const [editor, setEditor] = useState6({ text: value, cursor: value.length });
1817
+ const [offset, setOffset] = useState6(0);
1818
+ const state = editor.text === value ? editor : { text: value, cursor: value.length };
1819
+ if (state !== editor) setEditor(state);
1820
+ const lines = editorLines(value, width);
1821
+ const row = cursorRow(lines, state.cursor);
1822
+ const height = Math.min(maximum, lines.length);
1823
+ const start = Math.max(0, Math.min(offset, lines.length - height, row));
1824
+ const visibleStart = row >= start + height ? row - height + 1 : start;
1825
+ if (visibleStart !== offset) setOffset(visibleStart);
1826
+ useInput4(
1827
+ (input, key) => {
1828
+ if (key.return && !key.shift && !key.meta) {
1829
+ onSubmit(value);
1830
+ return;
1831
+ }
1832
+ if (ignoreArrows() && (key.upArrow || key.downArrow)) return;
1833
+ const next = updateEditor(state, input, key, width);
1834
+ if (!next) return;
1835
+ setEditor(next);
1836
+ if (next.text !== value) onChange(next.text);
1837
+ },
1838
+ { isActive: focused }
1839
+ );
1840
+ return /* @__PURE__ */ jsx18(Box13, { ref: container, flexGrow: 1, minWidth: 0, height, flexDirection: "column", children: lines.slice(visibleStart, visibleStart + height).map((line, index) => {
1841
+ const current = visibleStart + index === row;
1842
+ const cursor = current ? state.cursor - line.start : -1;
1843
+ const after = current ? moveCursor(state, 1).cursor - line.start : -1;
1844
+ return /* @__PURE__ */ jsx18(Text13, { color: colors.fg, wrap: "truncate-end", children: !value ? /* @__PURE__ */ jsxs11(Text13, { color: colors.muted, children: [
1845
+ /* @__PURE__ */ jsx18(Text13, { inverse: focused, children: placeholder[0] }),
1846
+ placeholder.slice(1)
1847
+ ] }) : current && focused ? /* @__PURE__ */ jsxs11(Fragment2, { children: [
1848
+ line.text.slice(0, cursor),
1849
+ /* @__PURE__ */ jsx18(Text13, { inverse: true, children: line.text.slice(cursor, after) || " " }),
1850
+ line.text.slice(after)
1851
+ ] }) : line.text || " " }, line.start);
1852
+ }) });
1853
+ }
1854
+
1855
+ // packages/cli/src/components/panels.tsx
1856
+ import { Box as Box16, Text as Text16, useBoxMetrics as useBoxMetrics6, useInput as useInput7 } from "ink";
1857
+ import { useContext as useContext6, useImperativeHandle as useImperativeHandle3, useRef as useRef8, useState as useState9 } from "react";
1858
+
1859
+ // packages/cli/src/terminal/scroll-area.tsx
1860
+ import { Box as Box14, Text as Text14, measureElement as measureElement2, useBoxMetrics as useBoxMetrics5, useFocus as useFocus3, useInput as useInput5 } from "ink";
1861
+ import { useImperativeHandle as useImperativeHandle2, useRef as useRef7, useState as useState7 } from "react";
1862
+ import { jsx as jsx19, jsxs as jsxs12 } from "react/jsx-runtime";
1863
+ function revealOffset(element, content, viewport, offset, height, maximum) {
1864
+ let parent = element;
1865
+ while (parent && parent !== content) parent = parent.parentNode;
1866
+ if (!parent || !viewport) return offset;
1867
+ const item = measureElement2(element);
1868
+ const top = item.y - measureElement2(viewport).y;
1869
+ if (top < offset || item.height > height) return Math.max(0, top);
1870
+ if (top + item.height > offset + height) return Math.min(maximum, top + item.height - height);
1871
+ return offset;
1872
+ }
1873
+ function ScrollArea({
1874
+ children,
1875
+ active = true,
1876
+ ref
1877
+ }) {
1878
+ const { colors } = useTerminalStyle();
1879
+ const { isFocused, focus } = useFocus3({ id: "content", isActive: active });
1880
+ const viewport = useRef7(null);
1881
+ const content = useRef7(null);
1882
+ const { clientHeight: height } = useBoxMetrics5(viewport);
1883
+ const { height: contentHeight } = useBoxMetrics5(content);
1884
+ const [anchor, setAnchor] = useState7(0);
1885
+ const maximum = Math.max(0, contentHeight - height);
1886
+ const offset = anchor === "end" ? maximum : Math.min(anchor, maximum);
1887
+ useImperativeHandle2(
1888
+ ref,
1889
+ () => ({
1890
+ end: () => setAnchor("end"),
1891
+ reveal: (element) => setAnchor(
1892
+ revealOffset(element, content.current, viewport.current, offset, height, maximum)
1893
+ )
1894
+ }),
1895
+ [offset, height, maximum]
1896
+ );
1897
+ useInput5(
1898
+ (_input, key) => {
1899
+ let next = offset;
1900
+ if (key.pageUp) next -= Math.max(1, height - 1);
1901
+ else if (key.pageDown) next += Math.max(1, height - 1);
1902
+ else if (isFocused && key.upArrow) next--;
1903
+ else if (isFocused && key.downArrow) next++;
1904
+ else if (isFocused && key.home) next = 0;
1905
+ else if (isFocused && key.end) next = maximum;
1906
+ else return;
1907
+ setAnchor(next >= maximum ? "end" : Math.max(0, next));
1908
+ },
1909
+ { isActive: active }
1910
+ );
1911
+ const track = Math.max(0, height);
1912
+ const thumbSize = Math.min(
1913
+ track,
1914
+ Math.max(1, Math.round(height * height / Math.max(1, contentHeight)))
1915
+ );
1916
+ const thumbTop = maximum > 0 ? Math.round(offset / maximum * (height - thumbSize)) : 0;
1917
+ const before = Array(thumbTop).fill("\u2502").join("\n");
1918
+ const thumb = Array(thumbSize).fill("\u2503").join("\n");
1919
+ const after = Array(Math.max(0, track - thumbTop - thumbSize)).fill("\u2502").join("\n");
1920
+ return /* @__PURE__ */ jsxs12(
1921
+ MouseArea,
1922
+ {
1923
+ mouseEnabled: active,
1924
+ onClick: () => focus("content"),
1925
+ onWheel: (direction) => setAnchor(
1926
+ (current) => Math.max(
1927
+ 0,
1928
+ Math.min(
1929
+ maximum,
1930
+ (current === "end" ? maximum : Math.min(current, maximum)) + direction * 3
1931
+ )
1932
+ )
1933
+ ),
1934
+ flexGrow: 1,
1935
+ flexShrink: 1,
1936
+ minHeight: 1,
1937
+ marginX: 2,
1938
+ children: [
1939
+ /* @__PURE__ */ jsx19(
1940
+ Box14,
1941
+ {
1942
+ ref: viewport,
1943
+ flexDirection: "column",
1944
+ flexGrow: 1,
1945
+ flexShrink: 1,
1946
+ overflow: "hidden",
1947
+ contentOffsetY: offset,
1948
+ children: /* @__PURE__ */ jsx19(Box14, { ref: content, flexDirection: "column", flexShrink: 0, width: "100%", children })
1949
+ }
1950
+ ),
1951
+ /* @__PURE__ */ jsx19(Box14, { width: 1, flexShrink: 0, "aria-hidden": true, children: /* @__PURE__ */ jsxs12(Text14, { color: colors.line, children: [
1952
+ before,
1953
+ before && "\n",
1954
+ /* @__PURE__ */ jsx19(Text14, { color: isFocused ? colors.accent : colors.muted, children: thumb }),
1955
+ after && "\n",
1956
+ after
1957
+ ] }) })
1958
+ ]
1959
+ }
1960
+ );
1961
+ }
1962
+
1963
+ // packages/cli/src/components/theme-picker.tsx
1964
+ import { Box as Box15, Text as Text15, useFocus as useFocus4, useInput as useInput6 } from "ink";
1965
+ import { useEffect as useEffect4, useState as useState8 } from "react";
1966
+ import { jsx as jsx20, jsxs as jsxs13 } from "react/jsx-runtime";
1967
+ function ThemePicker({
1968
+ initial,
1969
+ visibleCount,
1970
+ onPreview,
1971
+ onApply
1972
+ }) {
1973
+ const { colors } = useTerminalStyle();
1974
+ const { isFocused, focus } = useFocus4({ id: "theme-picker" });
1975
+ const [selected, setSelected] = useState8(
1976
+ () => Math.max(
1977
+ 0,
1978
+ themeOptions.findIndex((option) => option.id === initial)
1979
+ )
1980
+ );
1981
+ useEffect4(() => {
1982
+ focus("theme-picker");
1983
+ return () => onPreview(void 0);
1984
+ }, [focus, onPreview]);
1985
+ function moveSelection(direction) {
1986
+ const next = (selected + direction + themeOptions.length) % themeOptions.length;
1987
+ setSelected(next);
1988
+ onPreview(themeOptions[next].id);
1989
+ }
1990
+ useInput6(
1991
+ (input, key) => {
1992
+ const direction = Number(key.downArrow || input === "j") - Number(key.upArrow || input === "k");
1993
+ if (direction) {
1994
+ moveSelection(direction);
1995
+ } else if (key.return) {
1996
+ onApply(themeOptions[selected].id);
1997
+ }
1998
+ },
1999
+ { isActive: isFocused }
2000
+ );
2001
+ const start = Math.max(0, selected - visibleCount + 1);
2002
+ return /* @__PURE__ */ jsx20(
2003
+ MouseArea,
2004
+ {
2005
+ flexDirection: "column",
2006
+ onWheel: moveSelection,
2007
+ children: themeOptions.slice(start, start + visibleCount).map((option, index) => /* @__PURE__ */ jsxs13(
2008
+ MouseArea,
2009
+ {
2010
+ onClick: () => {
2011
+ focus("theme-picker");
2012
+ setSelected(start + index);
2013
+ onApply(option.id);
2014
+ },
2015
+ backgroundColor: start + index === selected ? colors.band : void 0,
2016
+ children: [
2017
+ /* @__PURE__ */ jsx20(Box15, { width: 20, flexShrink: 0, children: /* @__PURE__ */ jsxs13(Text15, { color: colors.fg, bold: true, children: [
2018
+ start + index === selected ? "\u25B8" : " ",
2019
+ " ",
2020
+ option.name
2021
+ ] }) }),
2022
+ /* @__PURE__ */ jsx20(Box15, { flexGrow: 1, flexShrink: 1, children: /* @__PURE__ */ jsxs13(Text15, { color: colors.muted, wrap: "truncate-end", children: [
2023
+ option.description,
2024
+ option.id === initial ? "\uFF08\u5F53\u524D\uFF09" : ""
2025
+ ] }) })
2026
+ ]
2027
+ },
2028
+ option.id
2029
+ ))
2030
+ }
2031
+ );
2032
+ }
2033
+
2034
+ // packages/cli/src/components/panels.tsx
2035
+ import { jsx as jsx21, jsxs as jsxs14 } from "react/jsx-runtime";
2036
+ function PanelBox({
2037
+ title,
2038
+ availableHeight,
2039
+ children,
2040
+ hints = "Esc:\u5173\u95ED"
2041
+ }) {
2042
+ const { colors } = useTerminalStyle();
2043
+ const viewport = useRef8(null);
2044
+ const content = useRef8(null);
2045
+ const { clientHeight: height } = useBoxMetrics6(viewport);
2046
+ const { height: contentHeight } = useBoxMetrics6(content);
2047
+ const [offset, setOffset] = useState9(0);
2048
+ const maximum = Math.max(0, contentHeight - height);
2049
+ const navigation = useContext6(LinkNavigation);
2050
+ const scroll = useRef8(null);
2051
+ useImperativeHandle3(
2052
+ scroll,
2053
+ () => ({
2054
+ end: () => setOffset(maximum),
2055
+ reveal: (element) => setOffset(
2056
+ (current) => revealOffset(element, content.current, viewport.current, current, height, maximum)
2057
+ )
2058
+ }),
2059
+ [height, maximum]
2060
+ );
2061
+ useInput7((_input, key) => {
2062
+ const direction = Number(key.pageDown) - Number(key.pageUp);
2063
+ if (direction)
2064
+ setOffset(
2065
+ (current) => Math.max(0, Math.min(maximum, current + direction * Math.max(1, height - 1)))
2066
+ );
2067
+ });
2068
+ return /* @__PURE__ */ jsx21(LinkNavigation, { value: navigation ? { ...navigation, scroll } : null, children: /* @__PURE__ */ jsxs14(
2069
+ MouseArea,
2070
+ {
2071
+ onClick: () => {
2072
+ },
2073
+ onWheel: (direction) => setOffset((current) => Math.max(0, Math.min(maximum, current + direction * 3))),
2074
+ flexDirection: "column",
2075
+ borderStyle: "round",
2076
+ borderColor: colors.line,
2077
+ backgroundColor: colors.bg,
2078
+ paddingX: 2,
2079
+ marginBottom: 1,
2080
+ children: [
2081
+ /* @__PURE__ */ jsx21(Text16, { color: colors.fg, bold: true, children: title }),
2082
+ /* @__PURE__ */ jsx21(
2083
+ Box16,
2084
+ {
2085
+ ref: viewport,
2086
+ flexDirection: "column",
2087
+ maxHeight: Math.max(1, availableHeight - 5),
2088
+ overflow: "hidden",
2089
+ contentOffsetY: Math.min(offset, maximum),
2090
+ children: /* @__PURE__ */ jsx21(
2091
+ Box16,
2092
+ {
2093
+ ref: content,
2094
+ flexDirection: "column",
2095
+ flexShrink: 0,
2096
+ marginY: availableHeight >= 8 ? 1 : 0,
2097
+ children
2098
+ }
2099
+ )
2100
+ }
2101
+ ),
2102
+ /* @__PURE__ */ jsxs14(Text16, { color: colors.muted, wrap: "truncate-end", children: [
2103
+ hints,
2104
+ maximum > 0 ? " \u2502 PgUp/PgDn:\u6EDA\u52A8" : ""
2105
+ ] })
2106
+ ]
2107
+ }
2108
+ ) });
2109
+ }
2110
+ function TerminalPanels({
2111
+ panel,
2112
+ availableHeight,
2113
+ savedTheme,
2114
+ onPreviewTheme,
2115
+ onApplyTheme,
2116
+ site
2117
+ }) {
2118
+ const { colors } = useTerminalStyle();
2119
+ if (!panel) return null;
2120
+ if (panel.type === "theme") {
2121
+ return /* @__PURE__ */ jsx21(
2122
+ PanelBox,
2123
+ {
2124
+ title: "\u9009\u62E9\u4E3B\u9898",
2125
+ availableHeight,
2126
+ hints: "\u2191\u2193:\u9884\u89C8 \u2502 Enter:\u5E94\u7528 \u2502 Esc:\u53D6\u6D88",
2127
+ children: /* @__PURE__ */ jsx21(
2128
+ ThemePicker,
2129
+ {
2130
+ initial: savedTheme,
2131
+ visibleCount: Math.max(1, availableHeight - (availableHeight >= 8 ? 7 : 5)),
2132
+ onPreview: onPreviewTheme,
2133
+ onApply: onApplyTheme
2134
+ }
2135
+ )
2136
+ }
2137
+ );
2138
+ }
2139
+ if (panel.type === "help") {
2140
+ return /* @__PURE__ */ jsxs14(PanelBox, { title: "\u547D\u4EE4", availableHeight, children: [
2141
+ terminalCommands.map((command) => /* @__PURE__ */ jsxs14(Box16, { children: [
2142
+ /* @__PURE__ */ jsx21(Box16, { width: 20, flexShrink: 0, children: /* @__PURE__ */ jsxs14(Text16, { color: colors.skill, children: [
2143
+ "/",
2144
+ command.id,
2145
+ /* @__PURE__ */ jsx21(Text16, { color: colors.muted, children: command.args ? ` ${command.args}` : "" })
2146
+ ] }) }),
2147
+ /* @__PURE__ */ jsx21(Box16, { flexGrow: 1, flexShrink: 1, children: /* @__PURE__ */ jsx21(Text16, { color: colors.muted, wrap: "truncate-end", children: command.description }) })
2148
+ ] }, command.id)),
2149
+ /* @__PURE__ */ jsx21(Text16, { color: colors["fg-soft"], children: "\u76F4\u63A5\u8F93\u5165\u95EE\u9898\uFF0CAI \u4F1A\u68C0\u7D22\u5168\u7AD9\u6587\u7AE0\u540E\u56DE\u7B54\uFF0C\u4E5F\u80FD\u5E2E\u4F60\u5207\u6362\u4E3B\u9898\u3001\u6253\u5F00\u9875\u9762\u3002" })
2150
+ ] });
2151
+ }
2152
+ if (panel.type === "rss") {
2153
+ return /* @__PURE__ */ jsx21(PanelBox, { title: "\u8BA2\u9605", availableHeight, children: /* @__PURE__ */ jsx21(
2154
+ Markdown,
2155
+ {
2156
+ text: `RSS [${site.feedUrl}](${site.feedUrl})
2157
+
2158
+ \u4E5F\u53EF\u4EE5\u901A\u8FC7 [Feedly](https://feedly.com/i/subscription/feed%2F${encodeURIComponent(site.feedUrl)}) \u6216 [Inoreader](https://www.inoreader.com/feed/${encodeURIComponent(site.feedUrl)}) \u8BA2\u9605\u3002`
2159
+ }
2160
+ ) });
2161
+ }
2162
+ return /* @__PURE__ */ jsxs14(
2163
+ Text16,
2164
+ {
2165
+ color: panel.tone === "error" ? colors.err : colors["fg-soft"],
2166
+ backgroundColor: colors.bg,
2167
+ children: [
2168
+ "\u25C6 ",
2169
+ cleanText(panel.text)
2170
+ ]
2171
+ }
2172
+ );
2173
+ }
2174
+
2175
+ // packages/cli/src/components/ui/spinner.tsx
2176
+ import cliSpinners from "cli-spinners";
2177
+ import { Box as Box17, Text as Text17 } from "ink";
2178
+
2179
+ // packages/cli/src/hooks/use-animation.ts
2180
+ import { useEffect as useEffect5, useState as useState10 } from "react";
2181
+ function useAnimation({
2182
+ intervalMs,
2183
+ isActive = true
2184
+ }) {
2185
+ const [frame, setFrame] = useState10(0);
2186
+ useEffect5(() => {
2187
+ if (!isActive || process.env.CI || process.env.NO_MOTION) return;
2188
+ const timer = setInterval(() => setFrame((value) => value + 1), intervalMs);
2189
+ return () => clearInterval(timer);
2190
+ }, [intervalMs, isActive]);
2191
+ return frame;
2192
+ }
2193
+
2194
+ // packages/cli/src/components/ui/spinner.tsx
2195
+ import { jsx as jsx22, jsxs as jsxs15 } from "react/jsx-runtime";
2196
+ function Spinner({
2197
+ type = "dots",
2198
+ label,
2199
+ color,
2200
+ isActive = true
2201
+ }) {
2202
+ const { colors } = useTerminalStyle();
2203
+ const spinner = cliSpinners[type];
2204
+ const frame = useAnimation({ intervalMs: spinner.interval, isActive });
2205
+ return /* @__PURE__ */ jsxs15(Box17, { "aria-role": "progressbar", "aria-label": label ?? "\u52A0\u8F7D\u4E2D", "aria-state": { busy: isActive }, children: [
2206
+ /* @__PURE__ */ jsx22(Text17, { "aria-hidden": true, color: color ?? colors.muted, children: spinner.frames[frame % spinner.frames.length] }),
2207
+ label && /* @__PURE__ */ jsxs15(Text17, { color: colors.muted, children: [
2208
+ " ",
2209
+ label
2210
+ ] })
2211
+ ] });
2212
+ }
2213
+
2214
+ // packages/cli/src/components/run-status.tsx
2215
+ import { Box as Box18, Text as Text18 } from "ink";
2216
+ import { jsx as jsx23, jsxs as jsxs16 } from "react/jsx-runtime";
2217
+ function RunStatus({ running, loading }) {
2218
+ const { colors } = useTerminalStyle();
2219
+ const seconds = useElapsedTime(running, true);
2220
+ if (!running && !loading) return null;
2221
+ return /* @__PURE__ */ jsxs16(Box18, { paddingLeft: 3, height: 1, children: [
2222
+ /* @__PURE__ */ jsx23(Spinner, {}),
2223
+ /* @__PURE__ */ jsxs16(Text18, { color: colors.fg, children: [
2224
+ " ",
2225
+ loading ? "\u601D\u8003\u4E2D\u2026" : "\u56DE\u590D\u4E2D\u2026"
2226
+ ] }),
2227
+ running && /* @__PURE__ */ jsxs16(Text18, { color: colors.muted, children: [
2228
+ " ",
2229
+ Math.floor(seconds),
2230
+ "s"
2231
+ ] }),
2232
+ /* @__PURE__ */ jsx23(Box18, { flexGrow: 1 }),
2233
+ /* @__PURE__ */ jsx23(Text18, { color: colors.muted, children: "[Esc \u505C\u6B62]" })
2234
+ ] });
2235
+ }
2236
+
2237
+ // packages/cli/src/components/composer.tsx
2238
+ import { jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
2239
+ function Composer({
2240
+ runtime,
2241
+ loading,
2242
+ panel,
2243
+ onCommand,
2244
+ onCancel,
2245
+ focusKey,
2246
+ searchPage,
2247
+ onSubmit,
2248
+ ...panels
2249
+ }) {
2250
+ const { colors } = useTerminalStyle();
2251
+ const aui = useAui();
2252
+ const { activeId, focus } = useFocusManager2();
2253
+ const running = useAuiState2((state) => state.thread.isRunning);
2254
+ const text = useAuiState2((state) => state.composer.text);
2255
+ const menu = useRef9(null);
2256
+ const dock = useRef9(null);
2257
+ const { height, top } = useBoxMetrics7(dock);
2258
+ const availableHeight = Math.max(6, top);
2259
+ const { isFocused: focused } = useFocus5({ id: "composer", autoFocus: true });
2260
+ const restoreComposerFocus = useEffectEvent(() => {
2261
+ if (panel?.type !== "theme") focus(searchPage && !panel ? "search" : "composer");
2262
+ });
2263
+ useEffect6(() => {
2264
+ restoreComposerFocus();
2265
+ }, [focusKey, searchPage]);
2266
+ useEffect6(() => {
2267
+ if (panel?.type) restoreComposerFocus();
2268
+ }, [panel?.type]);
2269
+ useInput8((input, key) => {
2270
+ if (key.escape) {
2271
+ if (!menu.current?.dismiss()) onCancel();
2272
+ focus("composer");
2273
+ }
2274
+ if (input === "/" && activeId !== "composer" && activeId !== "search") {
2275
+ if (panel) onCancel();
2276
+ focus("composer");
2277
+ if (!text) aui.composer.setText("/");
2278
+ }
2279
+ });
2280
+ return /* @__PURE__ */ jsxs17(Box19, { ref: dock, flexDirection: "column", marginX: 2, paddingTop: 1, flexShrink: 0, children: [
2281
+ /* @__PURE__ */ jsx24(RunStatus, { loading, running }),
2282
+ /* @__PURE__ */ jsxs17(
2283
+ MouseArea,
2284
+ {
2285
+ onClick: () => focus("composer"),
2286
+ borderStyle: "round",
2287
+ borderColor: focused ? colors["line-strong"] : colors.line,
2288
+ paddingX: 2,
2289
+ alignItems: "flex-start",
2290
+ children: [
2291
+ /* @__PURE__ */ jsx24(Text19, { color: colors.prompt, children: "\u276F " }),
2292
+ /* @__PURE__ */ jsx24(
2293
+ ComposerInput,
2294
+ {
2295
+ value: text,
2296
+ focused,
2297
+ onChange: (value) => {
2298
+ if (value.startsWith("/") && panel) onCancel();
2299
+ aui.composer.setText(value);
2300
+ },
2301
+ ignoreArrows: () => menu.current?.isOpen() ?? false,
2302
+ onSubmit: (text2) => {
2303
+ const input = text2.trim();
2304
+ if (!input) return;
2305
+ if (menu.current?.submit(text2)) return;
2306
+ const command = parseCommand(input);
2307
+ if (command) {
2308
+ aui.composer.setText("");
2309
+ void onCommand(command.name, command.args);
2310
+ } else if (!running) {
2311
+ onSubmit();
2312
+ runtime.thread.composer.send();
2313
+ }
2314
+ }
2315
+ }
2316
+ ),
2317
+ /* @__PURE__ */ jsx24(Box19, { position: "absolute", bottom: -1, right: 1, children: /* @__PURE__ */ jsxs17(Text19, { color: colors.muted, backgroundColor: colors.bg, children: [
2318
+ " ",
2319
+ composerBrand.mark,
2320
+ " \xB7 ",
2321
+ composerBrand.model,
2322
+ " "
2323
+ ] }) })
2324
+ ]
2325
+ }
2326
+ ),
2327
+ /* @__PURE__ */ jsxs17(Box19, { justifyContent: "space-between", flexWrap: "wrap", columnGap: 2, children: [
2328
+ /* @__PURE__ */ jsx24(Text19, { color: colors.muted, children: "Tab/Shift+Tab:\u5207\u6362 \u2502 Enter:\u6253\u5F00/\u53D1\u9001 \u2502 /:\u547D\u4EE4 \u2502 Esc:\u5173\u95ED/\u505C\u6B62" }),
2329
+ /* @__PURE__ */ jsxs17(Box19, { columnGap: 2, children: [
2330
+ /* @__PURE__ */ jsx24(TerminalLink, { href: panels.site.feedUrl, color: colors.muted, underline: false, children: "RSS" }),
2331
+ panels.site.sponsorUrl && /* @__PURE__ */ jsx24(TerminalLink, { href: panels.site.sponsorUrl, color: colors.muted, underline: false, children: "\u8D5E\u8D4F" }),
2332
+ /* @__PURE__ */ jsxs17(Text19, { color: colors.muted, children: [
2333
+ "\xA9 ",
2334
+ (/* @__PURE__ */ new Date()).getFullYear(),
2335
+ " ",
2336
+ panels.site.name
2337
+ ] })
2338
+ ] })
2339
+ ] }),
2340
+ /* @__PURE__ */ jsxs17(Box19, { position: "absolute", bottom: height, width: "100%", flexDirection: "column", children: [
2341
+ /* @__PURE__ */ jsx24(LinkScope, { value: true, children: /* @__PURE__ */ jsx24(TerminalPanels, { panel, availableHeight, ...panels }) }),
2342
+ /* @__PURE__ */ jsx24(
2343
+ CommandMenu,
2344
+ {
2345
+ ref: menu,
2346
+ text,
2347
+ focused,
2348
+ availableHeight,
2349
+ onSelect: (command) => {
2350
+ if (command.id === "read") aui.composer.setText("/read ");
2351
+ else {
2352
+ aui.composer.setText("");
2353
+ void onCommand(command.id, "");
2354
+ }
2355
+ }
2356
+ },
2357
+ text
2358
+ )
2359
+ ] })
2360
+ ] });
2361
+ }
2362
+
2363
+ // packages/cli/src/components/page-content.tsx
2364
+ import { Box as Box21, Text as Text24 } from "ink";
2365
+ import { memo } from "react";
2366
+
2367
+ // packages/cli/src/components/article-page.tsx
2368
+ import { Box as Box20, Text as Text21 } from "ink";
2369
+
2370
+ // packages/cli/src/components/ui/heading.tsx
2371
+ import { Text as Text20 } from "ink";
2372
+ import { jsx as jsx25 } from "react/jsx-runtime";
2373
+ function Heading({
2374
+ level = 1,
2375
+ children,
2376
+ color
2377
+ }) {
2378
+ const { colors } = useTerminalStyle();
2379
+ const token = level <= 2 ? "h1" : level === 3 ? "h2" : level === 4 ? "h3" : "muted";
2380
+ return /* @__PURE__ */ jsx25(Text20, { color: color ?? colors[token], bold: true, children });
2381
+ }
2382
+
2383
+ // packages/cli/src/components/article-page.tsx
2384
+ import { jsx as jsx26, jsxs as jsxs18 } from "react/jsx-runtime";
2385
+ function ArticlePage({
2386
+ post,
2387
+ title,
2388
+ description,
2389
+ tool: tool2,
2390
+ markdown
2391
+ }) {
2392
+ const { colors } = useTerminalStyle();
2393
+ return /* @__PURE__ */ jsxs18(Box20, { flexDirection: "column", paddingX: 3, paddingTop: 1, marginBottom: 1, gap: 1, children: [
2394
+ tool2 && /* @__PURE__ */ jsx26(Tool, { ...tool2, aside: formatShortDate(post.date) }),
2395
+ /* @__PURE__ */ jsx26(Heading, { children: cleanText(title) }),
2396
+ description && /* @__PURE__ */ jsx26(Text21, { color: colors.muted, children: cleanText(description) }),
2397
+ /* @__PURE__ */ jsx26(Text21, { color: colors.muted, children: "\u2500\u2500\u2500" }),
2398
+ /* @__PURE__ */ jsx26(Box20, { flexDirection: "column", maxWidth: 90, children: markdown && /* @__PURE__ */ jsx26(Markdown, { text: markdown }) }),
2399
+ /* @__PURE__ */ jsxs18(LinkRow, { columnGap: 2, flexWrap: "wrap", marginTop: 1, children: [
2400
+ post.tags.map((tag) => /* @__PURE__ */ jsxs18(TerminalLink, { href: getTagPath(tag), color: colors.muted, underline: false, children: [
2401
+ "#",
2402
+ cleanText(tag)
2403
+ ] }, tag)),
2404
+ /* @__PURE__ */ jsx26(Box20, { flexGrow: 1, justifyContent: "flex-end", children: /* @__PURE__ */ jsx26(TerminalLink, { href: "/blog/", underline: false, children: "\u2039 \u5168\u90E8\u6587\u7AE0" }) })
2405
+ ] })
2406
+ ] });
2407
+ }
2408
+
2409
+ // packages/cli/src/components/search-input.tsx
2410
+ import { Text as Text22, useFocus as useFocus6, useInput as useInput9 } from "ink";
2411
+ import { useContext as useContext7, useEffect as useEffect7, useRef as useRef10, useState as useState11 } from "react";
2412
+ import { jsx as jsx27, jsxs as jsxs19 } from "react/jsx-runtime";
2413
+ function SearchInput({
2414
+ path,
2415
+ onSearch
2416
+ }) {
2417
+ const { colors } = useTerminalStyle();
2418
+ const active = useContext7(LinkScope);
2419
+ const { isFocused, focus } = useFocus6({ id: "search", isActive: active });
2420
+ const [query, setQuery] = useState11(
2421
+ () => new URL(path, "https://miantiao.me").searchParams.get("q") ?? ""
2422
+ );
2423
+ const timer = useRef10(void 0);
2424
+ useEffect7(() => () => clearTimeout(timer.current), []);
2425
+ useEffect7(() => {
2426
+ if (!active) clearTimeout(timer.current);
2427
+ }, [active]);
2428
+ useInput9((_input, key) => {
2429
+ if (key.escape) clearTimeout(timer.current);
2430
+ });
2431
+ return /* @__PURE__ */ jsxs19(
2432
+ LinkRow,
2433
+ {
2434
+ reveal: isFocused,
2435
+ onClick: () => focus("search"),
2436
+ borderStyle: "round",
2437
+ borderColor: isFocused ? colors["line-strong"] : colors.line,
2438
+ paddingX: 1,
2439
+ marginY: 1,
2440
+ children: [
2441
+ /* @__PURE__ */ jsx27(Text22, { color: colors.prompt, children: "\u276F " }),
2442
+ /* @__PURE__ */ jsx27(
2443
+ ComposerInput,
2444
+ {
2445
+ value: query,
2446
+ focused: active && isFocused,
2447
+ placeholder: "\u641C\u7D22\u6807\u9898\u3001\u63CF\u8FF0\u3001\u6B63\u6587\u4E0E\u6807\u7B7E",
2448
+ ignoreArrows: () => false,
2449
+ onChange: (value) => {
2450
+ setQuery(value);
2451
+ clearTimeout(timer.current);
2452
+ timer.current = setTimeout(() => onSearch(value.trim()), 300);
2453
+ },
2454
+ onSubmit: (value) => {
2455
+ clearTimeout(timer.current);
2456
+ onSearch(value.trim());
2457
+ }
2458
+ }
2459
+ )
2460
+ ]
2461
+ }
2462
+ );
2463
+ }
2464
+
2465
+ // packages/cli/src/components/ui/pagination.tsx
2466
+ import { Text as Text23 } from "ink";
2467
+ import { jsx as jsx28, jsxs as jsxs20 } from "react/jsx-runtime";
2468
+ function Pagination({
2469
+ current,
2470
+ total,
2471
+ previous,
2472
+ next
2473
+ }) {
2474
+ const { colors } = useTerminalStyle();
2475
+ if (total <= 1) return null;
2476
+ return /* @__PURE__ */ jsxs20(LinkRow, { columnGap: 1, flexWrap: "wrap", children: [
2477
+ previous ? /* @__PURE__ */ jsx28(TerminalLink, { href: previous, underline: false, children: "\u2039 \u4E0A\u4E00\u9875" }) : /* @__PURE__ */ jsx28(Text23, { color: colors.muted, children: "\u2039 \u4E0A\u4E00\u9875" }),
2478
+ /* @__PURE__ */ jsx28(Text23, { color: colors.dim, children: "\u2502" }),
2479
+ /* @__PURE__ */ jsxs20(Text23, { color: colors.muted, children: [
2480
+ "\u7B2C ",
2481
+ current,
2482
+ " \u9875\uFF0C\u5171 ",
2483
+ total,
2484
+ " \u9875"
2485
+ ] }),
2486
+ /* @__PURE__ */ jsx28(Text23, { color: colors.dim, children: "\u2502" }),
2487
+ next ? /* @__PURE__ */ jsx28(TerminalLink, { href: next, underline: false, children: "\u4E0B\u4E00\u9875 \u203A" }) : /* @__PURE__ */ jsx28(Text23, { color: colors.muted, children: "\u4E0B\u4E00\u9875 \u203A" })
2488
+ ] });
2489
+ }
2490
+
2491
+ // packages/cli/src/components/page-content.tsx
2492
+ import { jsx as jsx29, jsxs as jsxs21 } from "react/jsx-runtime";
2493
+ function PageEntries({ page }) {
2494
+ const { colors } = useTerminalStyle();
2495
+ if (page.projects)
2496
+ return /* @__PURE__ */ jsx29(HomeList, { list: { kind: "projects", items: page.projects, before: "", after: "" } });
2497
+ if (!page.tags) return page.items.length ? /* @__PURE__ */ jsx29(PageList, { items: page.items }) : null;
2498
+ return /* @__PURE__ */ jsx29(Box21, { flexWrap: "wrap", columnGap: 3, children: page.tags.map((tag) => /* @__PURE__ */ jsxs21(LinkRow, { children: [
2499
+ /* @__PURE__ */ jsxs21(TerminalLink, { href: tag.path, color: colors.fg, underline: false, children: [
2500
+ "#",
2501
+ cleanText(tag.name)
2502
+ ] }),
2503
+ /* @__PURE__ */ jsxs21(Text24, { color: colors.muted, children: [
2504
+ " ",
2505
+ tag.count
2506
+ ] })
2507
+ ] }, tag.path)) });
2508
+ }
2509
+ var PageContent = memo(function PageContent2({
2510
+ page,
2511
+ prompt,
2512
+ onSearch
2513
+ }) {
2514
+ const { colors } = useTerminalStyle();
2515
+ if (page.sections) return /* @__PURE__ */ jsx29(HomeSections, { page });
2516
+ if (page.post) return /* @__PURE__ */ jsx29(ArticlePage, { ...page, post: page.post });
2517
+ return /* @__PURE__ */ jsxs21(Box21, { flexDirection: "column", marginBottom: 1, children: [
2518
+ page.path !== "/" && /* @__PURE__ */ jsx29(Prompt, { text: page.command ?? prompt, command: true, aside: page.aside }),
2519
+ /* @__PURE__ */ jsxs21(Box21, { flexDirection: "column", paddingX: 3, paddingTop: 1, gap: 1, children: [
2520
+ page.tool && /* @__PURE__ */ jsx29(Tool, { ...page.tool }),
2521
+ page.verb !== "Read" && /* @__PURE__ */ jsx29(Heading, { children: cleanText(page.title) }),
2522
+ page.description && /* @__PURE__ */ jsx29(Text24, { color: colors["fg-soft"], children: cleanText(page.description) }),
2523
+ page.path.startsWith("/search/") && /* @__PURE__ */ jsx29(SearchInput, { path: page.path, onSearch }),
2524
+ page.markdown && /* @__PURE__ */ jsx29(Box21, { flexDirection: "column", maxWidth: page.verb === "Read" ? 90 : void 0, children: /* @__PURE__ */ jsx29(Markdown, { text: page.markdown }) }),
2525
+ /* @__PURE__ */ jsx29(PageEntries, { page }),
2526
+ page.more && /* @__PURE__ */ jsxs21(LinkRow, { children: [
2527
+ /* @__PURE__ */ jsx29(Text24, { color: colors.muted, children: "\u203A " }),
2528
+ /* @__PURE__ */ jsx29(TerminalLink, { href: page.more.path, children: page.more.title })
2529
+ ] }),
2530
+ page.pagination && /* @__PURE__ */ jsx29(Pagination, { ...page.pagination })
2531
+ ] })
2532
+ ] });
2533
+ });
2534
+
2535
+ // packages/cli/src/terminal/terminal-layout.tsx
2536
+ import { Box as Box22, Text as Text25, useWindowSize as useWindowSize2 } from "ink";
2537
+ import { jsx as jsx30, jsxs as jsxs22 } from "react/jsx-runtime";
2538
+ function TerminalLayout({
2539
+ path,
2540
+ nav = [],
2541
+ children
2542
+ }) {
2543
+ const { columns, rows } = useWindowSize2();
2544
+ const { colors } = useTerminalStyle();
2545
+ const segments = new URL(path, "https://miantiao.me").pathname.split("/").filter(Boolean);
2546
+ return /* @__PURE__ */ jsxs22(
2547
+ Box22,
2548
+ {
2549
+ width: columns,
2550
+ height: rows,
2551
+ flexDirection: "column",
2552
+ backgroundColor: colors.bg,
2553
+ overflow: "hidden",
2554
+ children: [
2555
+ /* @__PURE__ */ jsxs22(Box22, { paddingX: 2, paddingY: 1, flexShrink: 0, flexDirection: columns < 70 ? "column" : "row", children: [
2556
+ /* @__PURE__ */ jsxs22(Box22, { flexGrow: 1, flexShrink: 1, children: [
2557
+ /* @__PURE__ */ jsx30(TerminalLink, { href: "/", color: colors.muted, underline: false, children: "~/miantiao.me" }),
2558
+ segments.map((segment, index) => {
2559
+ const prefix = `/${segments.slice(0, index + 1).join("/")}/`;
2560
+ const href = segments[0] === "posts" && index < segments.length - 1 ? "/blog/" : prefix;
2561
+ return /* @__PURE__ */ jsxs22(Box22, { flexShrink: 1, minWidth: 0, children: [
2562
+ /* @__PURE__ */ jsx30(Text25, { color: colors.muted, children: "/" }),
2563
+ /* @__PURE__ */ jsx30(TerminalLink, { href, color: colors.muted, underline: false, children: cleanText(decodeURIComponent(segment)) })
2564
+ ] }, prefix);
2565
+ })
2566
+ ] }),
2567
+ /* @__PURE__ */ jsx30(Box22, { columnGap: 2, flexWrap: "wrap", flexShrink: 0, children: nav.map((link) => /* @__PURE__ */ jsx30(Box22, { flexShrink: 0, children: /* @__PURE__ */ jsx30(
2568
+ TerminalLink,
2569
+ {
2570
+ href: link.href,
2571
+ color: path.startsWith(link.href) ? colors.fg : colors.muted,
2572
+ underline: false,
2573
+ children: cleanText(link.title)
2574
+ }
2575
+ ) }, link.href)) })
2576
+ ] }),
2577
+ children
2578
+ ]
2579
+ }
2580
+ );
2581
+ }
2582
+
2583
+ // packages/cli/src/terminal/console.tsx
2584
+ import { jsx as jsx31, jsxs as jsxs23 } from "react/jsx-runtime";
2585
+ function Console({
2586
+ runtime,
2587
+ page,
2588
+ prompt,
2589
+ locationKey,
2590
+ site,
2591
+ origin,
2592
+ loading,
2593
+ panel,
2594
+ onCommand,
2595
+ onCancel,
2596
+ onSelect,
2597
+ onSearch,
2598
+ onClosePanel,
2599
+ ...themePicker
2600
+ }) {
2601
+ const scroll = useRef11(null);
2602
+ const focusKey = String(locationKey);
2603
+ return /* @__PURE__ */ jsx31(LinkNavigation, { value: { site: new URL(page.path, site.url).href, origin, onSelect, scroll }, children: /* @__PURE__ */ jsx31(LinkScope, { value: !panel || panel.type === "notice", children: /* @__PURE__ */ jsx31(TerminalLayout, { path: page.path, nav: site.nav, children: /* @__PURE__ */ jsxs23(ThreadPrimitive2.Root, { flexGrow: 1, flexShrink: 1, minHeight: 0, children: [
2604
+ /* @__PURE__ */ jsxs23(ScrollArea, { ref: scroll, active: !panel || panel.type === "notice", children: [
2605
+ /* @__PURE__ */ jsx31(PageContent, { page, prompt, onSearch }),
2606
+ /* @__PURE__ */ jsx31(ChatThread, {})
2607
+ ] }),
2608
+ /* @__PURE__ */ jsx31(
2609
+ Composer,
2610
+ {
2611
+ runtime,
2612
+ focusKey,
2613
+ searchPage: page.path.startsWith("/search/"),
2614
+ loading,
2615
+ panel,
2616
+ site,
2617
+ onCommand,
2618
+ onCancel,
2619
+ ...themePicker,
2620
+ onSubmit: () => {
2621
+ onClosePanel();
2622
+ scroll.current?.end();
2623
+ }
2624
+ }
2625
+ )
2626
+ ] }) }, focusKey) }) });
2627
+ }
2628
+
2629
+ // src/shared/pagination.ts
2630
+ function getPageCount(total, pageSize) {
2631
+ return Math.max(1, Math.ceil(total / pageSize));
2632
+ }
2633
+ function paginate(items, page, pageSize) {
2634
+ const lastPage = getPageCount(items.length, pageSize);
2635
+ const currentPage = Number.isSafeInteger(page) ? Math.min(Math.max(page, 1), lastPage) : 1;
2636
+ const start = (currentPage - 1) * pageSize;
2637
+ const data = items.slice(start, start + pageSize);
2638
+ const previousPath = currentPage === 2 ? "/blog/" : `/blog/${currentPage - 1}/`;
2639
+ return {
2640
+ data,
2641
+ first: items.length ? start + 1 : 0,
2642
+ last: start + data.length,
2643
+ total: items.length,
2644
+ currentPage,
2645
+ lastPage,
2646
+ url: {
2647
+ prev: currentPage > 1 ? previousPath : null,
2648
+ next: currentPage < lastPage ? `/blog/${currentPage + 1}/` : null
2649
+ }
2650
+ };
2651
+ }
2652
+
2653
+ // packages/cli/src/content/pages.ts
2654
+ async function loadPage(client, path, signal, cachedIndex) {
2655
+ signal?.throwIfAborted();
2656
+ const index = cachedIndex ?? await client.getIndex(signal);
2657
+ const url = new URL(path, client.site);
2658
+ const pathname = url.pathname.endsWith("/") ? url.pathname : `${url.pathname}/`;
2659
+ const page = { path: pathname + url.search, title: "", verb: "List", items: [] };
2660
+ const document = [...Object.values(index.pages), ...index.posts].find(
2661
+ (item) => new URL(item.path, client.site).pathname === pathname
2662
+ );
2663
+ if (document) {
2664
+ const result = await client.getDocument(document.path, index, signal);
2665
+ return {
2666
+ index: result.index,
2667
+ page: documentPage(result.index, result.document, client.site, url.search)
2668
+ };
2669
+ } else if (pathname === "/blog/" || /^\/blog\/\d+\/$/.test(pathname)) {
2670
+ const size = index.site.pageSize;
2671
+ const requested = Number(pathname.match(/\d+/)?.[0] ?? 1);
2672
+ const result = paginate(index.posts, requested, size);
2673
+ const canonicalPath = requested === 1 ? "/blog/" : `/blog/${requested}/`;
2674
+ if (!Number.isSafeInteger(requested) || requested !== result.currentPage || pathname !== canonicalPath)
2675
+ throw new Error("\u627E\u4E0D\u5230\u8FD9\u4E00\u9875\uFF0C\u8BF7\u4F7F\u7528 /blog \u6D4F\u89C8\u6587\u7AE0\u3002");
2676
+ const current = result.currentPage;
2677
+ const total = result.lastPage;
2678
+ page.title = "\u535A\u5BA2";
2679
+ page.command = current === 1 ? "/blog" : `/blog ${current}`;
2680
+ page.aside = `${current}/${total}`;
2681
+ page.tool = {
2682
+ verb: "List",
2683
+ target: "posts/",
2684
+ detail: `${result.first}\u2013${result.last} of ${result.total}`
2685
+ };
2686
+ page.items = result.data.map((post) => ({
2687
+ title: post.title,
2688
+ date: post.date,
2689
+ description: post.description,
2690
+ path: post.path
2691
+ }));
2692
+ page.pagination = {
2693
+ current,
2694
+ total,
2695
+ previous: result.url.prev ?? void 0,
2696
+ next: result.url.next ?? void 0
2697
+ };
2698
+ } else if (pathname === "/projects/") {
2699
+ page.title = "\u9879\u76EE";
2700
+ page.command = "/projects";
2701
+ page.tool = {
2702
+ verb: "List",
2703
+ target: "projects/",
2704
+ detail: `\u6309 Star \u6392\u5E8F\uFF0C${index.projects.length} \u4E2A`
2705
+ };
2706
+ page.description = "\u8FD9\u91CC\u8BB0\u5F55\u6211\u5728\u5DE5\u4F5C\u4E4B\u5916\u6301\u7EED\u6298\u817E\u7684\u5C0F\u9879\u76EE\uFF1A\u6709\u4E9B\u662F\u5DE5\u5177\uFF0C\u6709\u4E9B\u662F\u5B9E\u9A8C\uFF0C\u4E5F\u6709\u4E9B\u53EA\u662F\u597D\u5947\u5FC3\u7684\u526F\u4EA7\u7269\u3002";
2707
+ page.projects = index.projects;
2708
+ page.items = index.projects.map((project) => ({
2709
+ title: `${project.title} \xB7 \u2605 ${project.stars}`,
2710
+ description: project.description,
2711
+ path: project.url,
2712
+ external: true
2713
+ }));
2714
+ } else if (pathname === "/tags/") {
2715
+ page.title = "\u6807\u7B7E";
2716
+ page.command = "/tags";
2717
+ page.tool = { verb: "List", target: "tags/", detail: `${index.tags.length} \u4E2A` };
2718
+ page.tags = index.tags;
2719
+ page.items = index.tags.map((tag) => ({
2720
+ title: `#${tag.name}`,
2721
+ description: String(tag.count),
2722
+ path: tag.path
2723
+ }));
2724
+ } else if (pathname.startsWith("/tags/")) {
2725
+ const tag = index.tags.find((tag2) => tag2.path === pathname);
2726
+ if (!tag) throw new Error("\u627E\u4E0D\u5230\u8FD9\u4E2A\u6807\u7B7E\uFF0C\u8BF7\u4F7F\u7528 /tags \u67E5\u627E\u3002");
2727
+ page.title = `\u6807\u7B7E\uFF1A${tag.name}`;
2728
+ page.command = `/tags ${tag.name}`;
2729
+ page.items = index.posts.filter((post) => post.tags.includes(tag.name)).map((post) => ({
2730
+ title: post.title,
2731
+ date: post.date,
2732
+ description: post.description,
2733
+ path: post.path
2734
+ }));
2735
+ page.tool = { verb: "Grep", target: `#${tag.name}`, detail: `${page.items.length} \u7BC7` };
2736
+ page.more = { title: "\u5168\u90E8\u6807\u7B7E", path: "/tags/" };
2737
+ } else if (pathname === "/search/") {
2738
+ const query = url.searchParams.get("q") ?? "";
2739
+ page.title = "\u641C\u7D22";
2740
+ page.command = query ? `/search ${query}` : "/search";
2741
+ page.verb = "Search";
2742
+ if (query) {
2743
+ const results = await client.search(query, index, signal);
2744
+ page.items = results.map((post) => ({
2745
+ title: post.title,
2746
+ description: post.description,
2747
+ date: post.date,
2748
+ path: post.url
2749
+ }));
2750
+ }
2751
+ if (query) page.aside = `${page.items.length} \u6761`;
2752
+ if (query && !page.items.length) page.markdown = `\u6CA1\u6709\u627E\u5230\u4E0E\u300C${query}\u300D\u76F8\u5173\u7684\u6587\u7AE0`;
2753
+ } else if (pathname === "/rss/") {
2754
+ page.title = "\u8BA2\u9605 RSS";
2755
+ page.items = [{ title: index.site.feedUrl, path: index.site.feedUrl, external: true }];
2756
+ } else {
2757
+ throw new Error("\u627E\u4E0D\u5230\u8FD9\u4E2A\u9875\u9762\uFF0C\u8BF7\u4F7F\u7528 /help \u67E5\u770B\u547D\u4EE4\u3002");
2758
+ }
2759
+ return { index, page };
2760
+ }
2761
+
2762
+ // packages/cli/src/app/session.ts
2763
+ import { useApp, useInput as useInput10 } from "ink";
2764
+ import { useCallback as useCallback2, useEffect as useEffect10, useState as useState13 } from "react";
2765
+
2766
+ // packages/cli/src/terminal/preferences.ts
2767
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2768
+ import { homedir } from "node:os";
2769
+ import { dirname, join } from "node:path";
2770
+ function themeFile() {
2771
+ return join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "miantiao", "theme.json");
2772
+ }
2773
+ function readTheme() {
2774
+ try {
2775
+ return parseThemePreference(JSON.parse(readFileSync(themeFile(), "utf8"))) ?? "auto";
2776
+ } catch {
2777
+ return "auto";
2778
+ }
2779
+ }
2780
+ function saveTheme(theme) {
2781
+ try {
2782
+ const file = themeFile();
2783
+ mkdirSync(dirname(file), { recursive: true });
2784
+ writeFileSync(file, JSON.stringify(theme) + "\n");
2785
+ } catch {
2786
+ }
2787
+ }
2788
+
2789
+ // src/shared/agent/command-actions.ts
2790
+ async function runSharedCommand(name, args, actions) {
2791
+ const path = getCommandPath(name, args);
2792
+ if (path) {
2793
+ actions.setPanel(null);
2794
+ await actions.navigate(path, `/${name} ${args}`.trim());
2795
+ return true;
2796
+ }
2797
+ switch (name) {
2798
+ case "help":
2799
+ case "rss":
2800
+ actions.setPanel({ type: name });
2801
+ return true;
2802
+ case "theme": {
2803
+ if (!args) {
2804
+ actions.setPanel({ type: "theme" });
2805
+ return true;
2806
+ }
2807
+ const preference = parseThemePreference(args);
2808
+ if (preference) {
2809
+ actions.setTheme(preference);
2810
+ actions.setPanel({ type: "notice", text: `\u5DF2\u5207\u6362\u5230 ${getThemeName(preference)}` });
2811
+ } else {
2812
+ actions.setPanel({ type: "notice", tone: "error", text: `\u672A\u77E5\u4E3B\u9898\uFF1A${args}\u3002\u53EF\u9009\uFF1A${themeOptions.map((option) => option.id).join("\u3001")}` });
2813
+ }
2814
+ return true;
2815
+ }
2816
+ case "clear":
2817
+ actions.clear();
2818
+ actions.setPanel(null);
2819
+ return true;
2820
+ default:
2821
+ return false;
2822
+ }
2823
+ }
2824
+
2825
+ // packages/cli/src/app/agent.ts
2826
+ import { useChatRuntime } from "@assistant-ui/ai-sdk";
2827
+ import { useEffect as useEffect8, useMemo as useMemo3 } from "react";
2828
+
2829
+ // src/shared/agent/transport.ts
2830
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
2831
+ import { DirectChatTransport, isStepCount, tool, ToolLoopAgent } from "ai";
2832
+ import { z as z3 } from "zod";
2833
+
2834
+ // src/shared/agent/context.ts
2835
+ import { escapeRegExp } from "es-toolkit/string";
2836
+ function formatTime(now, timeZone) {
2837
+ const parts = Object.fromEntries(
2838
+ new Intl.DateTimeFormat("en-CA", {
2839
+ timeZone,
2840
+ year: "numeric",
2841
+ month: "2-digit",
2842
+ day: "2-digit",
2843
+ hour: "2-digit",
2844
+ minute: "2-digit",
2845
+ second: "2-digit",
2846
+ hourCycle: "h23",
2847
+ timeZoneName: "longOffset"
2848
+ }).formatToParts(now).map((part) => [part.type, part.value])
2849
+ );
2850
+ const offset = parts.timeZoneName === "GMT" ? "UTC+00:00" : parts.timeZoneName.replace("GMT", "UTC");
2851
+ return `${parts.year}-${parts.month}-${parts.day} ${parts.hour}:${parts.minute}:${parts.second} (${timeZone}, ${offset})`;
2852
+ }
2853
+ function describeDevice(userAgent, viewportWidth) {
2854
+ const os = [
2855
+ ["iOS", /iPhone|iPad|iPod/],
2856
+ ["Android", /Android/],
2857
+ ["Windows", /Windows/],
2858
+ ["macOS", /Mac OS X|Macintosh/],
2859
+ ["Linux", /Linux/]
2860
+ ].find(([, pattern]) => pattern.test(userAgent))?.[0] ?? "Unknown OS";
2861
+ const browser = [
2862
+ ["Edge", /Edg\//],
2863
+ ["Firefox", /Firefox\//],
2864
+ ["Chrome", /Chrome\//],
2865
+ ["Safari", /Safari\//]
2866
+ ].find(([, pattern]) => pattern.test(userAgent))?.[0] ?? "Unknown browser";
2867
+ return `${os} \xB7 ${browser} \xB7 ${viewportWidth < 768 ? "mobile" : "desktop"}`;
2868
+ }
2869
+ function buildContextMessage(context, siteName) {
2870
+ const title = context.title.replace(new RegExp(`\\s*[|-]\\s*${escapeRegExp(siteName)}.*$`), "") || siteName;
2871
+ const lines = [
2872
+ `page: ${title} (${context.path})`,
2873
+ `theme: ${context.theme}`,
2874
+ `language: ${context.languages.slice(0, 3).join(", ")}`,
2875
+ `time: ${formatTime(context.now, context.timeZone)}`,
2876
+ `device: ${context.terminal ? `terminal \xB7 ${context.terminal.platform} \xB7 ${context.terminal.columns}x${context.terminal.rows}` : describeDevice(context.userAgent, context.viewportWidth)}`
2877
+ ];
2878
+ if (context.referrer && !context.referrer.startsWith(context.origin))
2879
+ lines.push(`referrer: ${context.referrer}`);
2880
+ return `<context>
2881
+ ${lines.join("\n")}
2882
+ </context>`;
2883
+ }
2884
+
2885
+ // src/shared/agent/transport.ts
2886
+ var themeIds = themeOptions.map((option) => option.id);
2887
+ function createAgentTransport(handlers, options) {
2888
+ const provider = createOpenAICompatible({
2889
+ name: "miantiao",
2890
+ baseURL: options.baseUrl
2891
+ });
2892
+ const agent = new ToolLoopAgent({
2893
+ model: provider(options.model),
2894
+ maxRetries: 0,
2895
+ stopWhen: isStepCount(2),
2896
+ // 工具执行后只生成答复,避免模型反复执行同一导航。
2897
+ prepareStep: ({ stepNumber }) => stepNumber > 0 ? { toolChoice: "none" } : void 0,
2898
+ tools: {
2899
+ set_theme: tool({
2900
+ description: "\u5207\u6362\u5F53\u524D\u754C\u9762\u4E3B\u9898",
2901
+ inputSchema: z3.object({ theme: z3.enum(themeIds) }),
2902
+ execute: async ({ theme }) => {
2903
+ handlers.setTheme(theme);
2904
+ return `\u5DF2\u5207\u6362\u5230 ${getThemeName(theme)}`;
2905
+ }
2906
+ }),
2907
+ open_page: tool({
2908
+ description: "\u6253\u5F00\u7AD9\u5185\u9875\u9762\uFF0C\u53C2\u6570\u4E3A\u4EE5 / \u5F00\u5934\u7684\u7AD9\u5185\u8DEF\u5F84",
2909
+ inputSchema: z3.object({ path: z3.string().regex(/^\/(?!\/)[^\s\\]*$/) }),
2910
+ execute: async ({ path }) => {
2911
+ await handlers.openPage(path);
2912
+ return `\u5DF2\u6253\u5F00 ${path}`;
2913
+ }
2914
+ })
2915
+ },
2916
+ // DirectChatTransport 以 `prompt` 传入完整的消息历史
2917
+ prepareCall: ({ prompt, messages, ...rest }) => ({
2918
+ ...rest,
2919
+ messages: [
2920
+ { role: "user", content: buildContextMessage(handlers.getContext(), options.siteName) },
2921
+ { role: "assistant", content: "\u5DF2\u8BFB\u53D6\u4E0A\u4E0B\u6587\u3002" },
2922
+ ...messages ?? (Array.isArray(prompt) ? prompt : [])
2923
+ ]
2924
+ })
2925
+ });
2926
+ return new DirectChatTransport({ agent, onError: () => "AI \u6682\u65F6\u65E0\u6CD5\u56DE\u590D\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002" });
2927
+ }
2928
+
2929
+ // packages/cli/src/app/agent.ts
2930
+ function useTerminalAgent({ site, index, page, theme, setTheme, navigate }) {
2931
+ const transport = useMemo3(
2932
+ () => createAgentTransport(
2933
+ {
2934
+ getContext: () => {
2935
+ const locale = Intl.DateTimeFormat().resolvedOptions();
2936
+ return {
2937
+ title: page.title,
2938
+ path: page.path,
2939
+ theme,
2940
+ languages: [locale.locale],
2941
+ timeZone: locale.timeZone,
2942
+ now: /* @__PURE__ */ new Date(),
2943
+ userAgent: "",
2944
+ viewportWidth: process.stdout.columns || 80,
2945
+ referrer: "",
2946
+ origin: site,
2947
+ terminal: {
2948
+ platform: process.platform,
2949
+ columns: process.stdout.columns || 80,
2950
+ rows: process.stdout.rows || 24
2951
+ }
2952
+ };
2953
+ },
2954
+ setTheme,
2955
+ openPage: async (path) => {
2956
+ if (!await navigate(path)) throw new Error("\u65E0\u6CD5\u6253\u5F00\u8FD9\u4E2A\u7AD9\u5185\u9875\u9762\u3002");
2957
+ }
2958
+ },
2959
+ { baseUrl: index.ai.baseUrl, model: index.ai.model, siteName: index.site.name }
2960
+ ),
2961
+ [
2962
+ site,
2963
+ page.title,
2964
+ page.path,
2965
+ theme,
2966
+ setTheme,
2967
+ navigate,
2968
+ index.ai.baseUrl,
2969
+ index.ai.model,
2970
+ index.site.name
2971
+ ]
2972
+ );
2973
+ const runtime = useChatRuntime({ transport });
2974
+ useEffect8(() => () => runtime.thread.cancelRun(), [runtime]);
2975
+ return runtime;
2976
+ }
2977
+
2978
+ // packages/cli/src/terminal/browser.ts
2979
+ import { spawn } from "node:child_process";
2980
+ async function openBrowser(input, site) {
2981
+ const url = new URL(input, site);
2982
+ if (!["http:", "https:", "mailto:"].includes(url.protocol))
2983
+ throw new Error("\u53EA\u652F\u6301\u7F51\u9875\u6216\u90AE\u4EF6\u94FE\u63A5\u3002");
2984
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
2985
+ await new Promise((resolve, reject) => {
2986
+ const child = spawn(command, [url.href], { stdio: "ignore" });
2987
+ child.once("error", () => reject(new Error(`\u65E0\u6CD5\u6253\u5F00\u6D4F\u89C8\u5668\uFF0C\u8BF7\u624B\u52A8\u8BBF\u95EE ${url.href}`)));
2988
+ child.once(
2989
+ "exit",
2990
+ (code) => code === 0 ? resolve() : reject(new Error(`\u65E0\u6CD5\u6253\u5F00\u6D4F\u89C8\u5668\uFF0C\u8BF7\u624B\u52A8\u8BBF\u95EE ${url.href}`))
2991
+ );
2992
+ });
2993
+ }
2994
+
2995
+ // packages/cli/src/app/navigation.ts
2996
+ import { useCallback, useEffect as useEffect9, useLayoutEffect as useLayoutEffect4, useMemo as useMemo4, useRef as useRef12, useState as useState12 } from "react";
2997
+ import { useLocation, useNavigate, useNavigationType } from "react-router";
2998
+ function useNavigation(client, initial) {
2999
+ const location = useLocation();
3000
+ const routerNavigate = useNavigate();
3001
+ const navigationType = useNavigationType();
3002
+ const [home, setHome] = useState12(initial);
3003
+ const entry = useMemo4(() => location.state ?? {
3004
+ ...home,
3005
+ prompt: "/home",
3006
+ locationKey: 0
3007
+ }, [location.state, home]);
3008
+ const current = useRef12({ entry, routerNavigate });
3009
+ useLayoutEffect4(() => {
3010
+ current.current = { entry, routerNavigate };
3011
+ }, [entry, routerNavigate]);
3012
+ const [status, setStatus] = useState12("idle");
3013
+ const [panel, setPanel] = useState12(
3014
+ client.warning ? { type: "notice", text: client.warning } : null
3015
+ );
3016
+ useEffect9(() => {
3017
+ if (initial.page.postsStatus !== "loading") return;
3018
+ const controller = new AbortController();
3019
+ const updateHome = (content) => {
3020
+ if (controller.signal.aborted) return;
3021
+ setHome(content);
3022
+ if (current.current.entry.page !== initial.page) return;
3023
+ const state = { ...current.current.entry, ...content };
3024
+ current.current.routerNavigate(state.page.path, { replace: true, state });
3025
+ current.current.entry = state;
3026
+ };
3027
+ void client.getIndex(controller.signal).then((index2) => {
3028
+ updateHome({ index: index2, page: withHomePosts(initial.page, index2) });
3029
+ }).catch(() => {
3030
+ updateHome({ ...initial, page: { ...initial.page, postsStatus: "error" } });
3031
+ });
3032
+ return () => controller.abort();
3033
+ }, [client, initial]);
3034
+ const { index } = entry;
3035
+ useEffect9(() => {
3036
+ if (index === initial.index && initial.page.postsStatus === "loading") return;
3037
+ void client.preloadSearch(index).catch(() => void 0);
3038
+ }, [client, index, initial]);
3039
+ const active = useRef12(void 0);
3040
+ useEffect9(
3041
+ () => () => {
3042
+ active.current?.controller.abort();
3043
+ active.current = void 0;
3044
+ client.cancelSearch();
3045
+ },
3046
+ [client]
3047
+ );
3048
+ const cancel = useCallback(() => {
3049
+ active.current?.controller.abort();
3050
+ active.current = void 0;
3051
+ setStatus("idle");
3052
+ }, []);
3053
+ useLayoutEffect4(() => {
3054
+ if (navigationType === "POP") cancel();
3055
+ }, [location.key, navigationType, cancel]);
3056
+ const notify = useCallback((text, tone) => {
3057
+ setPanel({ type: "notice", text, tone });
3058
+ }, []);
3059
+ const navigate = useCallback(
3060
+ async (path, prompt = path, replace = false) => {
3061
+ if (replace && active.current && !active.current.replace) return false;
3062
+ active.current?.controller.abort();
3063
+ const request = { controller: new AbortController(), replace };
3064
+ active.current = request;
3065
+ setStatus("loading");
3066
+ setPanel(null);
3067
+ let next;
3068
+ try {
3069
+ next = await loadPage(
3070
+ client,
3071
+ path,
3072
+ request.controller.signal,
3073
+ replace ? current.current.entry.index : void 0
3074
+ );
3075
+ } catch (error) {
3076
+ if (active.current === request) {
3077
+ active.current = void 0;
3078
+ setStatus("idle");
3079
+ setPanel({
3080
+ type: "notice",
3081
+ tone: "error",
3082
+ text: error instanceof Error ? error.message : "\u65E0\u6CD5\u8BFB\u53D6\u5B98\u7F51\u5185\u5BB9\u3002"
3083
+ });
3084
+ }
3085
+ return false;
3086
+ }
3087
+ if (active.current !== request) return false;
3088
+ active.current = void 0;
3089
+ const state = {
3090
+ ...next,
3091
+ prompt,
3092
+ locationKey: current.current.entry.locationKey + Number(!replace)
3093
+ };
3094
+ current.current.routerNavigate(next.page.path, { replace, state });
3095
+ current.current.entry = state;
3096
+ setStatus("idle");
3097
+ setPanel(client.warning ? { type: "notice", text: client.warning } : null);
3098
+ return true;
3099
+ },
3100
+ [client]
3101
+ );
3102
+ return { ...entry, status, panel, navigate, setPanel, cancel, notify };
3103
+ }
3104
+
3105
+ // packages/cli/src/app/session.ts
3106
+ function useTerminalSession(client, initial) {
3107
+ const { exit } = useApp();
3108
+ const [theme, setTheme] = useState13(readTheme);
3109
+ useEffect10(() => saveTheme(theme), [theme]);
3110
+ const [previewTheme, setPreviewTheme] = useState13();
3111
+ const { index, page, prompt, locationKey, status, panel, navigate, setPanel, cancel, notify } = useNavigation(client, initial);
3112
+ const runtime = useTerminalAgent({ site: client.site, index, page, theme, setTheme, navigate });
3113
+ const stop = () => {
3114
+ cancel();
3115
+ runtime.thread.cancelRun();
3116
+ };
3117
+ useInput10((input, key) => {
3118
+ if (key.ctrl && input === "c") {
3119
+ stop();
3120
+ exit();
3121
+ }
3122
+ });
3123
+ const onClosePanel = () => {
3124
+ setPreviewTheme(void 0);
3125
+ setPanel(null);
3126
+ };
3127
+ const onCancel = () => {
3128
+ if (panel) onClosePanel();
3129
+ else stop();
3130
+ };
3131
+ const onApplyTheme = (preference) => {
3132
+ setPreviewTheme(void 0);
3133
+ setTheme(preference);
3134
+ notify(`\u5DF2\u5207\u6362\u5230 ${getThemeName(preference)}`);
3135
+ };
3136
+ const onCommand = async (name, args) => {
3137
+ setPreviewTheme(void 0);
3138
+ if (name === "home" && page.path === "/") {
3139
+ cancel();
3140
+ setPanel(null);
3141
+ return;
3142
+ }
3143
+ try {
3144
+ if (await runSharedCommand(name, args, {
3145
+ navigate: (path, prompt2) => navigate(name === "tags" && args ? resolveTagPath(args, index) : path, prompt2),
3146
+ setPanel,
3147
+ setTheme,
3148
+ clear: () => {
3149
+ stop();
3150
+ runtime.thread.reset();
3151
+ }
3152
+ }))
3153
+ return;
3154
+ switch (name) {
3155
+ case "read":
3156
+ await navigate(resolveReadPath(args, page), `/read ${args}`);
3157
+ break;
3158
+ case "refresh":
3159
+ await navigate(page.path, "/refresh");
3160
+ break;
3161
+ case "open":
3162
+ await openBrowser(resolveOpenTarget(args, page), client.site);
3163
+ break;
3164
+ case "quit":
3165
+ stop();
3166
+ exit();
3167
+ break;
3168
+ default:
3169
+ notify(`\u672A\u77E5\u547D\u4EE4\uFF1A/${name}\uFF0C\u8F93\u5165 /help \u67E5\u770B\u53EF\u7528\u547D\u4EE4`, "error");
3170
+ }
3171
+ } catch (error) {
3172
+ notify(error instanceof Error ? error.message : "\u65E0\u6CD5\u6267\u884C\u547D\u4EE4\u3002", "error");
3173
+ }
3174
+ };
3175
+ const onSelect = (path, external = false) => {
3176
+ if (external) void onCommand("open", path);
3177
+ else void navigate(path);
3178
+ };
3179
+ const onSearch = useCallback2(
3180
+ (query) => {
3181
+ void navigate(getSearchPath(query), `/search ${query}`.trim(), true);
3182
+ },
3183
+ [navigate]
3184
+ );
3185
+ return {
3186
+ runtime,
3187
+ page,
3188
+ revision: index.revision,
3189
+ locationKey,
3190
+ prompt,
3191
+ site: index.site,
3192
+ origin: client.site,
3193
+ loading: status === "loading",
3194
+ panel,
3195
+ onCommand,
3196
+ onCancel,
3197
+ onClosePanel,
3198
+ onSelect,
3199
+ onSearch,
3200
+ theme: previewTheme ?? theme,
3201
+ savedTheme: theme,
3202
+ onPreviewTheme: setPreviewTheme,
3203
+ onApplyTheme
3204
+ };
3205
+ }
3206
+
3207
+ // packages/cli/src/app.tsx
3208
+ import { jsx as jsx32, jsxs as jsxs24 } from "react/jsx-runtime";
3209
+ function Session({
3210
+ client,
3211
+ initial,
3212
+ width
3213
+ }) {
3214
+ const { theme, ...session } = useTerminalSession(client, initial);
3215
+ return /* @__PURE__ */ jsx32(TerminalTheme, { preference: theme, width, children: /* @__PURE__ */ jsx32(AssistantRuntimeProvider, { runtime: session.runtime, children: /* @__PURE__ */ jsx32(Console, { ...session }) }) });
3216
+ }
3217
+ function App({ site }) {
3218
+ const { exit } = useApp2();
3219
+ const { columns } = useWindowSize3();
3220
+ const [client] = useState14(() => new TerminalClient(site));
3221
+ const [state, setState] = useState14(() => {
3222
+ const content = bundledHome(site);
3223
+ return content ? { status: "ready", content } : { status: "loading" };
3224
+ });
3225
+ const [attempt, setAttempt] = useState14(0);
3226
+ const width = Math.max(10, columns - 5);
3227
+ useEffect11(() => {
3228
+ if (bundledHome(site)) return;
3229
+ const request = new AbortController();
3230
+ loadPage(client, "/", request.signal).then((content) => {
3231
+ if (!request.signal.aborted) setState({ status: "ready", content });
3232
+ }).catch((error) => {
3233
+ if (!request.signal.aborted)
3234
+ setState({
3235
+ status: "error",
3236
+ message: error instanceof Error ? error.message : "\u65E0\u6CD5\u8BFB\u53D6\u5B98\u7F51\u5185\u5BB9\u3002"
3237
+ });
3238
+ });
3239
+ return () => request.abort();
3240
+ }, [client, site, attempt]);
3241
+ useInput11(
3242
+ (input, key) => {
3243
+ if (key.ctrl && input === "c" || input === "q") exit();
3244
+ else if (state.status === "error" && (key.return || input === "r" || input === "/refresh")) {
3245
+ setState({ status: "loading" });
3246
+ setAttempt((value) => value + 1);
3247
+ }
3248
+ },
3249
+ { isActive: state.status !== "ready" }
3250
+ );
3251
+ if (state.status === "ready")
3252
+ return /* @__PURE__ */ jsx32(MemoryRouter, { initialEntries: [state.content.page.path], useTransitions: false, children: /* @__PURE__ */ jsx32(Session, { client, initial: state.content, width }) });
3253
+ return /* @__PURE__ */ jsx32(TerminalTheme, { preference: "auto", width, children: /* @__PURE__ */ jsx32(TerminalLayout, { path: "/", children: /* @__PURE__ */ jsxs24(Box23, { flexDirection: "column", flexGrow: 1, padding: 2, children: [
3254
+ /* @__PURE__ */ jsxs24(Text26, { children: [
3255
+ "\u25C6 ",
3256
+ state.status === "error" ? state.message : "\u601D\u8003\u4E2D\u2026\u2026"
3257
+ ] }),
3258
+ /* @__PURE__ */ jsx32(Text26, { children: state.status === "error" ? "Enter \u91CD\u8BD5 \xB7 q \u9000\u51FA" : "Ctrl+C \u9000\u51FA" })
3259
+ ] }) }) });
3260
+ }
3261
+ async function run(site) {
3262
+ const app = render(
3263
+ /* @__PURE__ */ jsx32(MouseProvider, { children: /* @__PURE__ */ jsx32(App, { site }) }),
3264
+ {
3265
+ exitOnCtrlC: false,
3266
+ alternateScreen: true,
3267
+ incrementalRendering: true
3268
+ }
3269
+ );
3270
+ await app.waitUntilExit();
3271
+ }
3272
+ export {
3273
+ run
3274
+ };