miantiao.me 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2057 +0,0 @@
1
- import {
2
- define_TERMINAL_PALETTES_default
3
- } from "./chunk-ZOMIOTOE.js";
4
-
5
- // packages/cli/src/app.tsx
6
- import { AssistantRuntimeProvider } from "@assistant-ui/react-ink";
7
- import { Box as Box14, render, Text as Text14, useApp as useApp2, useInput as useInput9, useWindowSize as useWindowSize4 } from "ink";
8
- import { useEffect as useEffect9, useState as useState11 } from "react";
9
-
10
- // src/terminal/schema.ts
11
- import { z } from "zod";
12
- var terminalApiPath = "/api/terminal/v1";
13
- var linkSchema = z.object({ title: z.string(), href: z.string() });
14
- var pageSchema = z.object({
15
- path: z.string(),
16
- title: z.string(),
17
- documentUrl: z.string()
18
- });
19
- var terminalIndexSchema = z.object({
20
- schemaVersion: z.literal(1),
21
- revision: z.string().min(1),
22
- site: z.object({
23
- name: z.string(),
24
- description: z.string(),
25
- url: z.string(),
26
- feedUrl: z.string(),
27
- author: z.object({ name: z.string(), roles: z.array(z.string()) }),
28
- nav: z.array(linkSchema),
29
- socials: z.array(linkSchema),
30
- friends: z.array(linkSchema)
31
- }),
32
- ai: z.object({ baseUrl: z.string(), model: z.string() }),
33
- pages: z.object({ home: pageSchema, about: pageSchema }),
34
- posts: z.array(z.object({
35
- slug: z.string(),
36
- title: z.string(),
37
- description: z.string(),
38
- date: z.string(),
39
- tags: z.array(z.string()),
40
- path: z.string(),
41
- url: z.string(),
42
- documentUrl: z.string()
43
- })),
44
- projects: z.array(z.object({
45
- id: z.string(),
46
- title: z.string(),
47
- description: z.string(),
48
- url: z.string(),
49
- repo: z.string(),
50
- stars: z.number()
51
- })),
52
- tags: z.array(z.object({ name: z.string(), path: z.string(), count: z.number().int().nonnegative() })),
53
- searchIndexUrl: z.string()
54
- });
55
- var sectionSchema = z.discriminatedUnion("kind", [
56
- z.object({
57
- kind: z.literal("welcome"),
58
- markdown: z.string(),
59
- logo: z.string().optional(),
60
- menu: z.array(linkSchema.extend({ command: z.string() })).optional()
61
- }),
62
- z.object({ kind: z.literal("turn"), prompt: z.string(), markdown: z.string() })
63
- ]);
64
- var terminalDocumentSchema = z.object({
65
- schemaVersion: z.literal(1),
66
- revision: z.string().min(1),
67
- path: z.string(),
68
- title: z.string(),
69
- markdown: z.string().min(1),
70
- sections: z.array(sectionSchema).optional()
71
- });
72
-
73
- // src/utils/search-core.ts
74
- import { Document } from "flexsearch";
75
- var SearchManager = class _SearchManager {
76
- index;
77
- dataById;
78
- constructor(searchData) {
79
- this.index = new Document({
80
- document: {
81
- id: "id",
82
- index: [
83
- { field: "searchTitle", tokenize: "forward", resolution: 9 },
84
- { field: "searchDescription", tokenize: "strict", resolution: 7 },
85
- {
86
- field: "searchContent",
87
- tokenize: "forward",
88
- resolution: 5,
89
- context: {
90
- depth: 1,
91
- resolution: 3,
92
- bidirectional: true
93
- }
94
- },
95
- { field: "searchTags", tokenize: "strict", resolution: 9 }
96
- ]
97
- },
98
- cache: 100,
99
- fastupdate: false
100
- });
101
- this.dataById = new Map(searchData.map((doc) => [doc.id, doc]));
102
- searchData.forEach((doc) => this.index.add(doc));
103
- }
104
- static fromData(data) {
105
- return new _SearchManager(data);
106
- }
107
- search(query, limit = 20) {
108
- const trimmedQuery = query.trim();
109
- if (!trimmedQuery || limit <= 0) return [];
110
- const phraseTerms = /[\u4E00-\u9FFF]{4,}/.test(trimmedQuery) ? trimmedQuery.toLowerCase().match(/[\u4E00-\u9FFF]+|[a-z0-9]+/g) : null;
111
- const matchesPhrase = (document) => {
112
- if (!phraseTerms) return true;
113
- const fields = [document.title, document.description, document.content, ...document.tags];
114
- const words = new Set(
115
- fields.flatMap((field) => field.toLowerCase().match(/[a-z0-9]+/g) ?? [])
116
- );
117
- return phraseTerms.every(
118
- (term) => /^[a-z0-9]+$/.test(term) ? words.has(term) : fields.some((field) => field.includes(term))
119
- );
120
- };
121
- const results = this.index.search(trimmedQuery, {
122
- limit,
123
- suggest: true
124
- });
125
- const seenIds = /* @__PURE__ */ new Set();
126
- const documents = [];
127
- for (const fieldResult of results) {
128
- for (const docId of fieldResult.result) {
129
- const id = String(docId);
130
- if (seenIds.has(id)) continue;
131
- const document = this.dataById.get(id);
132
- if (!document || !matchesPhrase(document)) continue;
133
- seenIds.add(id);
134
- documents.push(document);
135
- if (documents.length >= limit) return documents;
136
- }
137
- }
138
- if (phraseTerms) {
139
- for (const document of this.dataById.values()) {
140
- if (seenIds.has(document.id) || !matchesPhrase(document)) continue;
141
- seenIds.add(document.id);
142
- documents.push(document);
143
- if (documents.length >= limit) break;
144
- }
145
- }
146
- return documents;
147
- }
148
- };
149
-
150
- // src/utils/search-schema.ts
151
- import { z as z2 } from "zod";
152
- var searchIndexSchema = z2.array(
153
- z2.object({
154
- id: z2.string(),
155
- title: z2.string(),
156
- description: z2.string(),
157
- content: z2.string(),
158
- url: z2.string(),
159
- date: z2.string(),
160
- tags: z2.array(z2.string()),
161
- searchTitle: z2.string(),
162
- searchDescription: z2.string(),
163
- searchContent: z2.string(),
164
- searchTags: z2.string()
165
- })
166
- );
167
-
168
- // packages/cli/src/client.ts
169
- var ContentError = class extends Error {
170
- constructor(message, status) {
171
- super(message);
172
- this.status = status;
173
- }
174
- status;
175
- };
176
- var TerminalClient = class {
177
- constructor(site, fetcher = fetch) {
178
- this.site = site;
179
- this.fetcher = fetcher;
180
- }
181
- site;
182
- fetcher;
183
- cache = /* @__PURE__ */ new Map();
184
- pending = /* @__PURE__ */ new Map();
185
- revision;
186
- searchManager;
187
- warning = "";
188
- async request(path, schema, signal) {
189
- signal?.throwIfAborted();
190
- const url = new URL(path, this.site).href;
191
- const running = this.pending.get(url);
192
- if (running && running.signal === signal) return running.promise;
193
- const promise = this.fetchDocument(url, schema, signal);
194
- this.pending.set(url, { promise, signal });
195
- try {
196
- return await promise;
197
- } finally {
198
- if (this.pending.get(url)?.promise === promise) this.pending.delete(url);
199
- }
200
- }
201
- async fetchDocument(url, schema, signal) {
202
- const cached = this.cache.get(url);
203
- const headers = new Headers({ Accept: "application/json" });
204
- if (cached?.etag) headers.set("If-None-Match", cached.etag);
205
- else if (cached?.modified) headers.set("If-Modified-Since", cached.modified);
206
- try {
207
- const response = await this.fetcher(url, {
208
- headers,
209
- signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(15e3)]) : AbortSignal.timeout(15e3)
210
- });
211
- if (response.status >= 500) {
212
- await response.body?.cancel();
213
- throw new Error(`HTTP ${response.status}`);
214
- }
215
- if (response.status === 304 && cached) return cached.value;
216
- if (!response.ok) {
217
- await response.body?.cancel();
218
- throw new ContentError(
219
- 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`,
220
- response.status
221
- );
222
- }
223
- signal?.throwIfAborted();
224
- const body = await response.json();
225
- signal?.throwIfAborted();
226
- const value = schema.safeParse(body);
227
- if (!value.success)
228
- 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");
229
- this.cache.set(url, {
230
- value: value.data,
231
- etag: response.headers.get("etag"),
232
- modified: response.headers.get("last-modified")
233
- });
234
- return value.data;
235
- } catch (error) {
236
- if (signal?.aborted || error instanceof ContentError) throw error;
237
- if (error instanceof SyntaxError)
238
- throw new ContentError("\u5B98\u7F51\u8FD4\u56DE\u4E86\u65E0\u6CD5\u8BFB\u53D6\u7684\u5185\u5BB9\uFF0C\u8BF7\u7A0D\u540E /refresh \u91CD\u8BD5\u3002");
239
- if (cached) {
240
- 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";
241
- return cached.value;
242
- }
243
- throw new ContentError("\u65E0\u6CD5\u8FDE\u63A5\u5B98\u7F51\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u540E\u4F7F\u7528 /refresh \u91CD\u8BD5\u3002");
244
- }
245
- }
246
- async getIndex(signal) {
247
- this.warning = "";
248
- const index = await this.request(`${terminalApiPath}/index.json`, terminalIndexSchema, signal);
249
- if (this.revision && this.revision !== index.revision) {
250
- const indexUrl = new URL(`${terminalApiPath}/index.json`, this.site).href;
251
- for (const key of this.cache.keys()) {
252
- if (key !== indexUrl) this.cache.delete(key);
253
- }
254
- this.searchManager = void 0;
255
- }
256
- this.revision = index.revision;
257
- return index;
258
- }
259
- async getDocument(path, index, signal) {
260
- for (let attempt = 0; attempt < 2; attempt++) {
261
- const page = [...Object.values(index.pages), ...index.posts].find(
262
- (item) => item.path === path
263
- );
264
- if (!page) throw new ContentError("\u627E\u4E0D\u5230\u8FD9\u4E2A\u9875\u9762\uFF0C\u8BF7\u4F7F\u7528 /blog \u6216 /search \u67E5\u627E\u3002");
265
- try {
266
- const document = await this.request(page.documentUrl, terminalDocumentSchema, signal);
267
- if (document.revision === index.revision && document.path === path)
268
- return { index, document };
269
- } catch (error) {
270
- if (!(error instanceof ContentError) || error.status !== 404) throw error;
271
- }
272
- if (attempt === 0) {
273
- index = await this.getIndex(signal);
274
- this.cache.delete(new URL(page.documentUrl, this.site).href);
275
- }
276
- }
277
- 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");
278
- }
279
- async search(query, index, signal) {
280
- if (!this.searchManager) {
281
- const data = await this.request(index.searchIndexUrl, searchIndexSchema, signal);
282
- this.searchManager = SearchManager.fromData(data);
283
- }
284
- return this.searchManager.search(query);
285
- }
286
- };
287
-
288
- // packages/cli/src/console.tsx
289
- import { ThreadPrimitive as ThreadPrimitive2 } from "@assistant-ui/react-ink";
290
- import { useRef as useRef8 } from "react";
291
-
292
- // packages/cli/src/chat.tsx
293
- import { ThreadPrimitive, useAuiState } from "@assistant-ui/react-ink";
294
- import { Box as Box4, Text as Text4 } from "ink";
295
-
296
- // src/theme-data.ts
297
- var themes = [
298
- { id: "groknight", name: "GrokNight", description: "\u4E2D\u6027\u6697\u8272\uFF0C\u9ED8\u8BA4\u4E3B\u9898", dark: true, color: "#141414" },
299
- { id: "grokday", name: "GrokDay", description: "\u660E\u4EAE\u7684\u6D45\u8272\u4E3B\u9898", dark: false, color: "#eeeeee" },
300
- { id: "tokyonight", name: "TokyoNight", description: "\u84DD\u8C03\u6697\u8272", dark: true, color: "#24283b" },
301
- { id: "rosepine", name: "RosePine Moon", description: "\u67D4\u548C\u7684\u7D2B\u7070\u6697\u8272", dark: true, color: "#232136" },
302
- { id: "oscura", name: "Oscura Midnight", description: "\u6DF1\u9ED1\u5E95\u4E0E\u7D2B\u8272\u70B9\u7F00", dark: true, color: "#030304" }
303
- ];
304
- var themeOptions = [
305
- ...themes,
306
- { id: "auto", name: "Auto", description: "\u8DDF\u968F\u7CFB\u7EDF\u4EAE\u6697" }
307
- ];
308
- var aliases = {
309
- "dark": "groknight",
310
- "night": "groknight",
311
- "grok-night": "groknight",
312
- "light": "grokday",
313
- "day": "grokday",
314
- "grok-day": "grokday",
315
- "tokyo": "tokyonight",
316
- "tokyo-night": "tokyonight",
317
- "rose-pine": "rosepine",
318
- "rosepine-moon": "rosepine",
319
- "rose-pine-moon": "rosepine",
320
- "oscura-midnight": "oscura",
321
- "system": "auto"
322
- };
323
- function parseThemePreference(input) {
324
- const name = input?.trim().toLowerCase();
325
- if (!name)
326
- return void 0;
327
- return themeOptions.find((option) => option.id === name)?.id ?? (Object.hasOwn(aliases, name) ? aliases[name] : void 0);
328
- }
329
- function resolveTheme(preference, prefersDark2) {
330
- if (preference !== "auto")
331
- return preference;
332
- return prefersDark2 ? "groknight" : "grokday";
333
- }
334
- function getThemeName(id) {
335
- return themeOptions.find((option) => option.id === id)?.name ?? id;
336
- }
337
-
338
- // src/agent/tool-header.ts
339
- function getToolHeader({ toolName, args }) {
340
- switch (toolName) {
341
- case "set_theme": {
342
- const theme = typeof args?.theme === "string" ? args.theme : "";
343
- const preference = parseThemePreference(theme);
344
- return { verb: "Theme", target: preference ? getThemeName(preference) : theme };
345
- }
346
- case "open_page":
347
- return { verb: "Open", target: typeof args?.path === "string" ? args.path : "" };
348
- default:
349
- return { verb: toolName, target: "" };
350
- }
351
- }
352
-
353
- // packages/cli/src/theme.tsx
354
- import { createContext, useContext, useMemo } from "react";
355
- import {
356
- MarkdownText,
357
- useShikiHighlighter
358
- } from "@assistant-ui/react-ink-markdown";
359
- import { stripVTControlCharacters } from "node:util";
360
- import { jsx } from "react/jsx-runtime";
361
- function cleanText(text) {
362
- return stripVTControlCharacters(text).replace(
363
- /[\p{Cc}\p{Cf}]/gu,
364
- (character) => /[\n\t]/.test(character) ? character : ""
365
- );
366
- }
367
- function prefersDark() {
368
- const background = process.env.COLORFGBG?.split(";").at(-1);
369
- return background === void 0 || Number(background) < 8;
370
- }
371
- var syntaxThemes = {
372
- groknight: "github-dark",
373
- grokday: "github-light",
374
- rosepine: "rose-pine-moon",
375
- tokyonight: "tokyo-night",
376
- oscura: "github-dark"
377
- };
378
- var StyleContext = createContext(null);
379
- function TerminalTheme({
380
- preference,
381
- width,
382
- children
383
- }) {
384
- const id = resolveTheme(preference, prefersDark());
385
- const highlighter = useShikiHighlighter({ theme: syntaxThemes[id] });
386
- const style = useMemo(() => {
387
- const colors = define_TERMINAL_PALETTES_default[id];
388
- return {
389
- colors,
390
- width,
391
- highlighter,
392
- markdownTheme: {
393
- heading: { color: colors.h1, bold: true },
394
- strong: { color: colors.fg, bold: true },
395
- inlineCode: { color: colors.code },
396
- code: { color: colors.code },
397
- link: { color: colors.link, underline: true },
398
- quote: { color: colors.muted },
399
- hr: { color: colors.line },
400
- listMarker: { color: colors.accent },
401
- tableHeader: { color: colors.fg, bold: true }
402
- }
403
- };
404
- }, [id, width, highlighter]);
405
- return /* @__PURE__ */ jsx(StyleContext, { value: style, children });
406
- }
407
- function useTerminalStyle() {
408
- const style = useContext(StyleContext);
409
- if (!style) throw new Error("TerminalTheme is required");
410
- return style;
411
- }
412
- function Markdown({ text, width: availableWidth }) {
413
- const { width, markdownTheme, highlighter } = useTerminalStyle();
414
- return /* @__PURE__ */ jsx(
415
- MarkdownText,
416
- {
417
- text,
418
- width: Math.max(10, availableWidth ?? width - 3),
419
- theme: markdownTheme,
420
- highlighter,
421
- codeWrap: true,
422
- hyperlinks: true,
423
- tableTruncate: false
424
- }
425
- );
426
- }
427
-
428
- // packages/cli/src/blocks.tsx
429
- import { Box as Box3, Text as Text3, useBoxMetrics as useBoxMetrics3 } from "ink";
430
- import { useRef as useRef3 } from "react";
431
-
432
- // packages/cli/src/page-list.tsx
433
- import { Box as Box2, Text as Text2, useBoxMetrics as useBoxMetrics2, useInput as useInput2 } from "ink";
434
- import { useEffect, useRef as useRef2, useState as useState2 } from "react";
435
-
436
- // packages/cli/src/scroll-area.tsx
437
- import { Box, Text, measureElement, useBoxMetrics, useFocus, useInput } from "ink";
438
- import {
439
- createContext as createContext2,
440
- useContext as useContext2,
441
- useImperativeHandle,
442
- useMemo as useMemo2,
443
- useRef,
444
- useState
445
- } from "react";
446
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
447
- var ScrollContext = createContext2(null);
448
- function useScrollArea() {
449
- const value = useContext2(ScrollContext);
450
- if (!value) throw new Error("ScrollArea is required");
451
- return value;
452
- }
453
- function ScrollArea({
454
- children,
455
- selection,
456
- active = true,
457
- ref
458
- }) {
459
- const { colors } = useTerminalStyle();
460
- const { isFocused } = useFocus({ id: "content", isActive: active });
461
- const viewport = useRef(null);
462
- const content = useRef(null);
463
- const { clientHeight: height } = useBoxMetrics(viewport);
464
- const { height: contentHeight } = useBoxMetrics(content);
465
- const [anchor, setAnchor] = useState(0);
466
- const maximum = Math.max(0, contentHeight - height);
467
- const offset = anchor === "end" ? maximum : Math.min(anchor, maximum);
468
- const context = useMemo2(() => ({ focused: isFocused, height }), [isFocused, height]);
469
- useImperativeHandle(
470
- ref,
471
- () => ({
472
- end: () => setAnchor("end"),
473
- reveal: (element) => {
474
- if (!viewport.current) return false;
475
- const item = measureElement(element);
476
- const top = item.y - measureElement(viewport.current).y;
477
- const visible = top >= offset && top < offset + height;
478
- if (top < offset || item.height > height) setAnchor(Math.max(0, top));
479
- else if (top + item.height > offset + height)
480
- setAnchor(Math.min(maximum, top + item.height - height));
481
- return visible;
482
- }
483
- }),
484
- [offset, height, maximum]
485
- );
486
- useInput(
487
- (_input, key) => {
488
- let next = offset;
489
- if (key.pageUp) next -= Math.max(1, height - 1);
490
- else if (key.pageDown) next += Math.max(1, height - 1);
491
- else if (isFocused && !selection && key.upArrow) next--;
492
- else if (isFocused && !selection && key.downArrow) next++;
493
- else if (isFocused && !selection && key.home) next = 0;
494
- else if (isFocused && !selection && key.end) next = maximum;
495
- else return;
496
- setAnchor(next >= maximum ? "end" : Math.max(0, next));
497
- },
498
- { isActive: active }
499
- );
500
- const track = Math.max(0, height);
501
- const thumbSize = Math.min(
502
- track,
503
- Math.max(1, Math.round(height * height / Math.max(1, contentHeight)))
504
- );
505
- const thumbTop = maximum > 0 ? Math.round(offset / maximum * (height - thumbSize)) : 0;
506
- const before = Array(thumbTop).fill("\u2502").join("\n");
507
- const thumb = Array(thumbSize).fill("\u2503").join("\n");
508
- const after = Array(Math.max(0, track - thumbTop - thumbSize)).fill("\u2502").join("\n");
509
- return /* @__PURE__ */ jsx2(ScrollContext, { value: context, children: /* @__PURE__ */ jsxs(Box, { flexGrow: 1, flexShrink: 1, minHeight: 1, marginX: 2, children: [
510
- /* @__PURE__ */ jsx2(
511
- Box,
512
- {
513
- ref: viewport,
514
- flexDirection: "column",
515
- flexGrow: 1,
516
- flexShrink: 1,
517
- overflow: "hidden",
518
- contentOffsetY: offset,
519
- children: /* @__PURE__ */ jsx2(Box, { ref: content, flexDirection: "column", flexShrink: 0, width: "100%", children })
520
- }
521
- ),
522
- /* @__PURE__ */ jsx2(Box, { width: 1, flexShrink: 0, "aria-hidden": true, children: /* @__PURE__ */ jsxs(Text, { color: colors.line, children: [
523
- before,
524
- before && "\n",
525
- /* @__PURE__ */ jsx2(Text, { color: isFocused ? colors.accent : colors.muted, children: thumb }),
526
- after && "\n",
527
- after
528
- ] }) })
529
- ] }) });
530
- }
531
-
532
- // packages/cli/src/page-list.tsx
533
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
534
- function PageList({ items, onSelect, scroll, compact = false }) {
535
- const { focused } = useScrollArea();
536
- const { colors } = useTerminalStyle();
537
- const [position, setSelected] = useState2(0);
538
- const selected = Math.max(0, Math.min(position, items.length - 1));
539
- const selectedItem = items[selected];
540
- const elements = useRef2(/* @__PURE__ */ new Map());
541
- const container = useRef2(null);
542
- const { hasMeasured } = useBoxMetrics2(container);
543
- useEffect(() => {
544
- const element = selectedItem && elements.current.get(selectedItem.path);
545
- if (focused && hasMeasured && element) scroll.current?.reveal(element);
546
- }, [focused, hasMeasured, selectedItem, scroll]);
547
- useInput2(
548
- (_input, key) => {
549
- let next = selected;
550
- if (key.upArrow) next--;
551
- else if (key.downArrow) next++;
552
- else if (key.home) next = 0;
553
- else if (key.end) next = items.length - 1;
554
- else if (key.return) {
555
- if (!selectedItem) return;
556
- const element = elements.current.get(selectedItem.path);
557
- if (element && scroll.current?.reveal(element) === false) return;
558
- onSelect(selectedItem.path, selectedItem.external);
559
- return;
560
- } else return;
561
- next = Math.max(0, Math.min(items.length - 1, next));
562
- setSelected(next);
563
- },
564
- { isActive: focused }
565
- );
566
- return /* @__PURE__ */ jsx3(Box2, { ref: container, flexDirection: "column", marginTop: 1, children: items.map((item, index) => {
567
- const active = focused && index === selected;
568
- return /* @__PURE__ */ jsxs2(
569
- Box2,
570
- {
571
- flexDirection: compact ? "row" : "column",
572
- justifyContent: compact ? "space-between" : void 0,
573
- marginBottom: compact ? 0 : 1,
574
- backgroundColor: active ? colors.band : void 0,
575
- ref: (element) => {
576
- if (element) elements.current.set(item.path, element);
577
- else elements.current.delete(item.path);
578
- },
579
- children: [
580
- /* @__PURE__ */ jsxs2(Text2, { color: active ? colors.accent : colors.fg, bold: active, children: [
581
- active ? "\u25B8" : compact ? " " : "\u2022",
582
- " ",
583
- compact ? "" : `${index + 1}. `,
584
- cleanText(item.title)
585
- ] }),
586
- item.description && /* @__PURE__ */ jsx3(Text2, { color: colors.muted, children: cleanText(item.description) })
587
- ]
588
- },
589
- item.path
590
- );
591
- }) });
592
- }
593
-
594
- // packages/cli/src/blocks.tsx
595
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
596
- function Prompt({ text, command = false }) {
597
- const { colors } = useTerminalStyle();
598
- return /* @__PURE__ */ jsxs3(Box3, { backgroundColor: colors.band, paddingX: 2, children: [
599
- /* @__PURE__ */ jsx4(Text3, { color: colors.prompt, children: "\u276F " }),
600
- /* @__PURE__ */ jsx4(Text3, { color: command ? colors.skill : colors.fg, children: cleanText(text) })
601
- ] });
602
- }
603
- function Welcome({ section, ...selection }) {
604
- const { colors, width } = useTerminalStyle();
605
- const content = useRef3(null);
606
- const { clientWidth } = useBoxMetrics3(content);
607
- return /* @__PURE__ */ jsxs3(Box3, { borderStyle: "round", borderColor: colors.line, paddingX: 2, paddingY: 1, gap: 4, children: [
608
- section.logo && width >= 90 && /* @__PURE__ */ jsx4(Text3, { color: colors.skill, children: cleanText(section.logo) }),
609
- /* @__PURE__ */ jsxs3(Box3, { ref: content, flexDirection: "column", flexGrow: 1, flexShrink: 1, minWidth: 0, children: [
610
- /* @__PURE__ */ jsx4(Markdown, { text: section.markdown, width: clientWidth || width - 6 }),
611
- section.menu && selection.items.length > 0 && /* @__PURE__ */ jsx4(PageList, { ...selection, compact: true })
612
- ] })
613
- ] });
614
- }
615
- function HomeSections({ page, ...selection }) {
616
- return /* @__PURE__ */ jsx4(Box3, { flexDirection: "column", gap: 1, children: page.sections?.map((section) => section.kind === "welcome" ? /* @__PURE__ */ jsx4(Welcome, { section, items: page.items, ...selection }, "welcome") : /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", gap: 1, children: [
617
- /* @__PURE__ */ jsx4(Prompt, { text: section.prompt }),
618
- /* @__PURE__ */ jsx4(Box3, { flexDirection: "column", paddingLeft: 2, paddingRight: 1, children: /* @__PURE__ */ jsx4(Markdown, { text: section.markdown }) })
619
- ] }, section.prompt)) });
620
- }
621
-
622
- // packages/cli/src/chat.tsx
623
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
624
- function MessageBody({ message }) {
625
- const { colors } = useTerminalStyle();
626
- if (message.role === "user") {
627
- return /* @__PURE__ */ jsx5(Box4, { marginY: 1, children: /* @__PURE__ */ jsx5(Prompt, { text: message.content.filter((part) => part.type === "text").map((part) => part.text).join("\n") }) });
628
- }
629
- if (message.role !== "assistant") return null;
630
- return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", paddingLeft: 2, marginBottom: 1, children: [
631
- message.content.map((part, index) => {
632
- if (part.type === "text")
633
- return /* @__PURE__ */ jsx5(Markdown, { text: part.text }, index);
634
- if (part.type === "reasoning") {
635
- const label = message.status.type === "running" ? "\u601D\u8003\u4E2D\u2026" : "\u601D\u8003";
636
- return (
637
- // eslint-disable-next-line react-doctor/no-array-index-as-key
638
- /* @__PURE__ */ jsxs4(Text4, { color: colors.muted, children: [
639
- "\u25C6 ",
640
- label
641
- ] }, index)
642
- );
643
- }
644
- if (part.type === "tool-call") {
645
- const { verb, target } = getToolHeader(part);
646
- return /* @__PURE__ */ jsxs4(Text4, { color: colors.skill, children: [
647
- "\u25C6 ",
648
- cleanText(verb),
649
- " ",
650
- cleanText(target),
651
- part.result !== void 0 && ` (${cleanText(String(part.result))})`
652
- ] }, part.toolCallId);
653
- }
654
- return null;
655
- }),
656
- message.status.type === "incomplete" && message.status.reason === "error" && /* @__PURE__ */ jsx5(Text4, { color: colors.err, children: "\u25C6 AI \u6682\u65F6\u65E0\u6CD5\u56DE\u590D\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002" })
657
- ] });
658
- }
659
- function Message() {
660
- const message = useAuiState((state) => state.message);
661
- return /* @__PURE__ */ jsx5(MessageBody, { message });
662
- }
663
- function ChatThread() {
664
- return /* @__PURE__ */ jsx5(ThreadPrimitive.Messages, { components: { Message } });
665
- }
666
-
667
- // packages/cli/src/composer.tsx
668
- import { useAui, useAuiState as useAuiState2 } from "@assistant-ui/react-ink";
669
- import {
670
- Box as Box10,
671
- Text as Text10,
672
- useBoxMetrics as useBoxMetrics6,
673
- useFocus as useFocus3,
674
- useFocusManager as useFocusManager2,
675
- useInput as useInput7,
676
- useWindowSize as useWindowSize2
677
- } from "ink";
678
- import { useEffect as useEffect5, useEffectEvent, useRef as useRef6 } from "react";
679
-
680
- // src/agent/appearance.ts
681
- var composerPlaceholder = "\u95EE\u70B9\u4EC0\u4E48\uFF0C\u6216\u8F93\u5165 / \u67E5\u770B\u547D\u4EE4";
682
- var composerBrand = { mark: "<MT/>", model: "V3.14 Pro" };
683
-
684
- // src/utils/tags.ts
685
- function getTagSlug(tag) {
686
- return encodeURIComponent(tag);
687
- }
688
- function getTagPath(tag) {
689
- return `/tags/${getTagSlug(tag)}/`;
690
- }
691
-
692
- // src/agent/commands.ts
693
- var commands = [
694
- { id: "help", description: "\u67E5\u770B\u53EF\u7528\u547D\u4EE4" },
695
- { id: "theme", args: "[\u4E3B\u9898]", description: "\u5207\u6362\u4E3B\u9898\uFF0C\u4E0D\u5E26\u53C2\u6570\u65F6\u6253\u5F00\u9009\u62E9\u5668" },
696
- { id: "clear", description: "\u6E05\u7A7A\u5F53\u524D\u5BF9\u8BDD" },
697
- { id: "home", description: "\u6253\u5F00\u9996\u9875" },
698
- { id: "blog", description: "\u6D4F\u89C8\u5168\u90E8\u6587\u7AE0" },
699
- { id: "projects", description: "\u67E5\u770B\u6298\u817E\u7684\u9879\u76EE" },
700
- { id: "tags", args: "[\u6807\u7B7E]", description: "\u6309\u6807\u7B7E\u6D4F\u89C8\u6587\u7AE0" },
701
- { id: "search", args: "[\u5173\u952E\u8BCD]", description: "\u641C\u7D22\u6587\u7AE0" },
702
- { id: "about", description: "\u5173\u4E8E\u672C\u7AD9" },
703
- { id: "rss", description: "\u8BA2\u9605 RSS" }
704
- ];
705
- var pageCommands = {
706
- home: "/",
707
- blog: "/blog/",
708
- projects: "/projects/",
709
- tags: "/tags/",
710
- about: "/about/"
711
- };
712
- function getCommandPath(name, args = "") {
713
- if (name === "search")
714
- return getSearchPath(args);
715
- if (name === "tags" && args)
716
- return getTagPath(args);
717
- return Object.hasOwn(pageCommands, name) ? pageCommands[name] : void 0;
718
- }
719
- function parseCommand(input) {
720
- const match = input.trim().match(/^\/(\S+)(?:\s+([\s\S]*))?$/);
721
- if (!match)
722
- return null;
723
- return { name: match[1].toLowerCase(), args: (match[2] ?? "").trim() };
724
- }
725
- function getSearchPath(query) {
726
- return query ? `/search/?q=${encodeURIComponent(query)}` : "/search/";
727
- }
728
-
729
- // packages/cli/src/command-menu.tsx
730
- import { Box as Box5, Text as Text5, useFocusManager, useInput as useInput3 } from "ink";
731
- import { useEffect as useEffect2, useImperativeHandle as useImperativeHandle2, useState as useState3 } from "react";
732
-
733
- // packages/cli/src/commands.ts
734
- var terminalCommands = [
735
- ...commands,
736
- { id: "read", args: "<\u7F16\u53F7\u3001slug \u6216\u8DEF\u5F84>", description: "\u9605\u8BFB\u6587\u7AE0\u5168\u6587" },
737
- { id: "open", args: "[\u7F16\u53F7\u6216 URL]", description: "\u5728\u6D4F\u89C8\u5668\u6253\u5F00\u5F53\u524D\u9875\u9762\u6216\u94FE\u63A5" },
738
- { id: "refresh", description: "\u4ECE\u5B98\u7F51\u5237\u65B0\u5F53\u524D\u9875\u9762" },
739
- { id: "quit", description: "\u9000\u51FA" }
740
- ];
741
- function getCommandSuggestions(text) {
742
- const match = text.match(/^\/([^\s]*)$/);
743
- if (!match) return null;
744
- const query = match[1].toLowerCase();
745
- return terminalCommands.filter(
746
- (command) => command.id.includes(query) || command.description.toLowerCase().includes(query)
747
- );
748
- }
749
- function numberedItem(input, page) {
750
- if (!/^\d+$/.test(input)) return void 0;
751
- const item = page.items[Number(input) - 1];
752
- if (!item) throw new Error("\u5F53\u524D\u5217\u8868\u6CA1\u6709\u8FD9\u4E2A\u7F16\u53F7\uFF0C\u8BF7\u4F7F\u7528\u5217\u8868\u4E2D\u663E\u793A\u7684\u7F16\u53F7\u3002");
753
- return item;
754
- }
755
- function resolveReadPath(input, page) {
756
- if (!input) throw new Error("\u8F93\u5165 /read <\u7F16\u53F7\u3001slug \u6216\u7AD9\u5185\u8DEF\u5F84> \u9605\u8BFB\u6587\u7AE0\u3002");
757
- const item = numberedItem(input, page);
758
- if (item) {
759
- if (item.external || !item.path.startsWith("/posts/"))
760
- throw new Error("\u8FD9\u4E2A\u7F16\u53F7\u4E0D\u662F\u6587\u7AE0\uFF0C\u8BF7\u4F7F\u7528 /blog \u6216 /search \u67E5\u627E\u6587\u7AE0\u3002");
761
- return item.path;
762
- }
763
- return input.startsWith("/") ? input : `/posts/${input.replace(/\/$/, "")}/`;
764
- }
765
- function resolveOpenTarget(input, page) {
766
- return numberedItem(input, page)?.path ?? (input || page.path);
767
- }
768
- function resolveTagPath(input, index) {
769
- const tag = index.tags.find((tag2) => tag2.name === input);
770
- if (!tag) throw new Error("\u627E\u4E0D\u5230\u8FD9\u4E2A\u6807\u7B7E\uFF0C\u8BF7\u4F7F\u7528 /tags \u67E5\u770B\u53EF\u7528\u6807\u7B7E\u3002");
771
- return tag.path;
772
- }
773
-
774
- // packages/cli/src/command-menu.tsx
775
- import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
776
- function CommandMenu({
777
- text,
778
- focused,
779
- availableHeight,
780
- onSelect,
781
- ref
782
- }) {
783
- const { colors } = useTerminalStyle();
784
- const { activeId, disableFocus, enableFocus, focus, focusPrevious } = useFocusManager();
785
- const [selected, setSelected] = useState3(0);
786
- const [dismissed, setDismissed] = useState3(false);
787
- const suggestions = getCommandSuggestions(text);
788
- const matches = suggestions ?? [];
789
- const open = focused && suggestions !== null && !dismissed;
790
- const visibleCount = Math.max(1, Math.min(8, availableHeight - 4));
791
- const start = Math.max(0, selected - visibleCount + 1);
792
- useEffect2(() => {
793
- if (!open || !activeId) return;
794
- disableFocus();
795
- focus(activeId);
796
- return enableFocus;
797
- }, [open, activeId, disableFocus, enableFocus, focus]);
798
- useImperativeHandle2(ref, () => ({
799
- isOpen: () => open,
800
- submit: (input) => {
801
- if (!open || input !== text) return false;
802
- if (matches.length) onSelect(matches[selected]);
803
- return true;
804
- },
805
- dismiss: () => {
806
- if (!open) return false;
807
- setDismissed(true);
808
- return true;
809
- }
810
- }));
811
- useInput3(
812
- (_input, key) => {
813
- if (key.tab) {
814
- if (key.shift) queueMicrotask(focusPrevious);
815
- else if (matches.length) onSelect(matches[selected]);
816
- return;
817
- }
818
- if (!matches.length) return;
819
- const direction = Number(key.downArrow) - Number(key.upArrow);
820
- if (direction) setSelected((index) => (index + direction + matches.length) % matches.length);
821
- },
822
- { isActive: open }
823
- );
824
- if (!open) return null;
825
- return /* @__PURE__ */ jsxs5(
826
- Box5,
827
- {
828
- flexDirection: "column",
829
- borderStyle: "round",
830
- borderColor: colors.line,
831
- backgroundColor: colors.bg,
832
- paddingX: 1,
833
- marginBottom: 1,
834
- children: [
835
- matches.length === 0 && /* @__PURE__ */ jsx6(Text5, { color: colors.muted, children: "\u6CA1\u6709\u5339\u914D\u7684\u547D\u4EE4" }),
836
- matches.slice(start, start + visibleCount).map((command, index) => {
837
- const active = start + index === selected;
838
- return /* @__PURE__ */ jsxs5(Box5, { height: 1, backgroundColor: active ? colors.band : void 0, children: [
839
- /* @__PURE__ */ jsx6(Box5, { width: 14, flexShrink: 0, children: /* @__PURE__ */ jsxs5(Text5, { color: colors.skill, children: [
840
- active ? "\u25B8" : " ",
841
- " /",
842
- command.id
843
- ] }) }),
844
- /* @__PURE__ */ jsx6(Box5, { flexGrow: 1, flexShrink: 1, children: /* @__PURE__ */ jsx6(Text5, { color: colors.muted, wrap: "truncate-end", children: command.description }) })
845
- ] }, command.id);
846
- }),
847
- /* @__PURE__ */ jsx6(Text5, { color: colors.muted, wrap: "truncate-end", children: "\u2191\u2193 \u9009\u62E9 \xB7 Enter/Tab \u786E\u8BA4 \xB7 Esc \u5173\u95ED" })
848
- ]
849
- }
850
- );
851
- }
852
-
853
- // packages/cli/src/composer-input.tsx
854
- import { Box as Box6, Text as Text6, useBoxMetrics as useBoxMetrics4, useInput as useInput4, useWindowSize } from "ink";
855
- import { useRef as useRef4, useState as useState4 } from "react";
856
-
857
- // packages/cli/src/input-editor.ts
858
- import stringWidth from "string-width";
859
- var segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
860
- function editorLines(text, width) {
861
- const lines = [];
862
- let start = 0;
863
- let columns = 0;
864
- for (const { segment, index } of segmenter.segment(text)) {
865
- if (segment === "\n") {
866
- lines.push({ start, end: index, text: text.slice(start, index) });
867
- if (columns >= width) lines.push({ start: index, end: index, text: "" });
868
- start = index + 1;
869
- columns = 0;
870
- continue;
871
- }
872
- const size = stringWidth(segment);
873
- if (columns && columns + size > width) {
874
- lines.push({ start, end: index, text: text.slice(start, index) });
875
- start = index;
876
- columns = 0;
877
- }
878
- columns += size;
879
- }
880
- lines.push({ start, end: text.length, text: text.slice(start) });
881
- if (columns >= width) lines.push({ start: text.length, end: text.length, text: "" });
882
- return lines;
883
- }
884
- function cursorRow(lines, cursor) {
885
- return lines.findLastIndex((line) => line.start <= cursor);
886
- }
887
- function editText(state, start, end, inserted = "") {
888
- return {
889
- text: state.text.slice(0, start) + inserted + state.text.slice(end),
890
- cursor: start + inserted.length
891
- };
892
- }
893
- function moveCursor(state, direction) {
894
- const boundaries = [...segmenter.segment(state.text)].map((part) => part.index);
895
- boundaries.push(state.text.length);
896
- const cursor = direction < 0 ? boundaries.findLast((index) => index < state.cursor) ?? 0 : boundaries.find((index) => index > state.cursor) ?? state.text.length;
897
- return { text: state.text, cursor };
898
- }
899
- function moveVertical(state, width, direction) {
900
- const lines = editorLines(state.text, width);
901
- const row = cursorRow(lines, state.cursor);
902
- const column = state.column ?? stringWidth(state.text.slice(lines[row].start, state.cursor));
903
- const target = lines[Math.max(0, Math.min(lines.length - 1, row + direction))];
904
- let cursor = target.start;
905
- let cells = 0;
906
- for (const { segment, index } of segmenter.segment(target.text)) {
907
- cells += stringWidth(segment);
908
- if (cells > column) break;
909
- cursor = target.start + index + segment.length;
910
- }
911
- return { text: state.text, cursor, column };
912
- }
913
- function wordCursor(state, direction) {
914
- if (direction < 0) {
915
- return state.cursor - (state.text.slice(0, state.cursor).match(/\S+\s*$|\s+$/u)?.[0].length ?? 0);
916
- }
917
- return state.cursor + (state.text.slice(state.cursor).match(/^\s*\S*/u)?.[0].length ?? 0);
918
- }
919
- function updateEditor(state, input, key, width) {
920
- const { text, cursor } = state;
921
- if (key.escape || key.tab || key.pageUp || key.pageDown) return;
922
- if (key.upArrow || key.downArrow) return moveVertical(state, width, key.upArrow ? -1 : 1);
923
- if (key.leftArrow || key.rightArrow) {
924
- const direction = key.leftArrow ? -1 : 1;
925
- return key.ctrl || key.meta ? { text, cursor: wordCursor(state, direction) } : moveCursor(state, direction);
926
- }
927
- const start = cursor === 0 ? 0 : text.lastIndexOf("\n", cursor - 1) + 1;
928
- const nextBreak = text.indexOf("\n", cursor);
929
- const end = nextBreak < 0 ? text.length : nextBreak;
930
- if (key.home || key.ctrl && input === "a") return { text, cursor: start };
931
- if (key.end || key.ctrl && input === "e") return { text, cursor: end };
932
- if (key.return || key.ctrl && input === "j") return editText(state, cursor, cursor, "\n");
933
- if (key.ctrl) {
934
- if (input === "u") return editText(state, start, cursor);
935
- if (input === "k") return editText(state, cursor, end);
936
- if (input === "w") return editText(state, wordCursor(state, -1), cursor);
937
- return;
938
- }
939
- if (key.backspace) return editText(state, moveCursor(state, -1).cursor, cursor);
940
- if (key.delete) return editText(state, cursor, moveCursor(state, 1).cursor);
941
- if (key.meta) return;
942
- const inserted = input.replace(/\r\n?/g, "\n").replace(/\t/g, " ").replace(new RegExp("\\p{Cc}", "gu"), (character) => character === "\n" ? character : "");
943
- if (inserted) return editText(state, cursor, cursor, inserted);
944
- }
945
-
946
- // packages/cli/src/composer-input.tsx
947
- import { Fragment, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
948
- function ComposerInput({
949
- value,
950
- focused,
951
- onChange,
952
- onSubmit,
953
- ignoreArrows
954
- }) {
955
- const { colors, width: availableWidth } = useTerminalStyle();
956
- const { rows } = useWindowSize();
957
- const container = useRef4(null);
958
- const { clientWidth } = useBoxMetrics4(container);
959
- const width = Math.max(1, clientWidth || availableWidth - 5);
960
- const maximum = Math.max(1, Math.min(10, rows - 8));
961
- const [editor, setEditor] = useState4({ text: value, cursor: value.length });
962
- const [offset, setOffset] = useState4(0);
963
- const state = editor.text === value ? editor : { text: value, cursor: value.length };
964
- if (state !== editor) setEditor(state);
965
- const lines = editorLines(value, width);
966
- const row = cursorRow(lines, state.cursor);
967
- const height = Math.min(maximum, lines.length);
968
- const start = Math.max(0, Math.min(offset, lines.length - height, row));
969
- const visibleStart = row >= start + height ? row - height + 1 : start;
970
- if (visibleStart !== offset) setOffset(visibleStart);
971
- useInput4(
972
- (input, key) => {
973
- if (key.return && !key.shift && !key.meta) {
974
- onSubmit(value);
975
- return;
976
- }
977
- if (ignoreArrows() && (key.upArrow || key.downArrow)) return;
978
- const next = updateEditor(state, input, key, width);
979
- if (!next) return;
980
- setEditor(next);
981
- if (next.text !== value) onChange(next.text);
982
- },
983
- { isActive: focused }
984
- );
985
- return /* @__PURE__ */ jsx7(Box6, { ref: container, flexGrow: 1, minWidth: 0, height, flexDirection: "column", children: lines.slice(visibleStart, visibleStart + height).map((line, index) => {
986
- const current = visibleStart + index === row;
987
- const cursor = current ? state.cursor - line.start : -1;
988
- const after = current ? moveCursor(state, 1).cursor - line.start : -1;
989
- return /* @__PURE__ */ jsx7(Text6, { color: colors.fg, wrap: "truncate-end", children: !value ? /* @__PURE__ */ jsxs6(Text6, { color: colors.muted, children: [
990
- /* @__PURE__ */ jsx7(Text6, { inverse: focused, children: composerPlaceholder[0] }),
991
- composerPlaceholder.slice(1)
992
- ] }) : current && focused ? /* @__PURE__ */ jsxs6(Fragment, { children: [
993
- line.text.slice(0, cursor),
994
- /* @__PURE__ */ jsx7(Text6, { inverse: true, children: line.text.slice(cursor, after) || " " }),
995
- line.text.slice(after)
996
- ] }) : line.text || " " }, line.start);
997
- }) });
998
- }
999
-
1000
- // packages/cli/src/panels.tsx
1001
- import { Box as Box8, Text as Text8, useBoxMetrics as useBoxMetrics5, useInput as useInput6 } from "ink";
1002
- import { useRef as useRef5, useState as useState6 } from "react";
1003
-
1004
- // packages/cli/src/theme-picker.tsx
1005
- import { Box as Box7, Text as Text7, useFocus as useFocus2, useInput as useInput5 } from "ink";
1006
- import { useEffect as useEffect3, useState as useState5 } from "react";
1007
- import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1008
- function ThemePicker({
1009
- initial,
1010
- visibleCount,
1011
- onPreview,
1012
- onApply
1013
- }) {
1014
- const { colors } = useTerminalStyle();
1015
- const { isFocused, focus } = useFocus2({ id: "theme-picker" });
1016
- const [selected, setSelected] = useState5(
1017
- () => Math.max(
1018
- 0,
1019
- themeOptions.findIndex((option) => option.id === initial)
1020
- )
1021
- );
1022
- useEffect3(() => {
1023
- focus("theme-picker");
1024
- return () => onPreview(void 0);
1025
- }, [focus, onPreview]);
1026
- useInput5(
1027
- (input, key) => {
1028
- const direction = Number(key.downArrow || input === "j") - Number(key.upArrow || input === "k");
1029
- if (direction) {
1030
- const next = (selected + direction + themeOptions.length) % themeOptions.length;
1031
- setSelected(next);
1032
- onPreview(themeOptions[next].id);
1033
- } else if (key.return) {
1034
- onApply(themeOptions[selected].id);
1035
- }
1036
- },
1037
- { isActive: isFocused }
1038
- );
1039
- const start = Math.max(0, selected - visibleCount + 1);
1040
- return /* @__PURE__ */ jsx8(Box7, { flexDirection: "column", children: themeOptions.slice(start, start + visibleCount).map((option, index) => /* @__PURE__ */ jsxs7(Box7, { backgroundColor: start + index === selected ? colors.band : void 0, children: [
1041
- /* @__PURE__ */ jsx8(Box7, { width: 20, flexShrink: 0, children: /* @__PURE__ */ jsxs7(Text7, { color: colors.fg, bold: true, children: [
1042
- start + index === selected ? "\u25B8" : " ",
1043
- " ",
1044
- option.name
1045
- ] }) }),
1046
- /* @__PURE__ */ jsx8(Box7, { flexGrow: 1, flexShrink: 1, children: /* @__PURE__ */ jsxs7(Text7, { color: colors.muted, wrap: "truncate-end", children: [
1047
- option.description,
1048
- option.id === initial ? "\uFF08\u5F53\u524D\uFF09" : ""
1049
- ] }) })
1050
- ] }, option.id)) });
1051
- }
1052
-
1053
- // packages/cli/src/panels.tsx
1054
- import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1055
- function PanelBox({
1056
- title,
1057
- availableHeight,
1058
- children,
1059
- hints = "Esc:\u5173\u95ED"
1060
- }) {
1061
- const { colors } = useTerminalStyle();
1062
- const viewport = useRef5(null);
1063
- const content = useRef5(null);
1064
- const { clientHeight: height } = useBoxMetrics5(viewport);
1065
- const { height: contentHeight } = useBoxMetrics5(content);
1066
- const [offset, setOffset] = useState6(0);
1067
- const maximum = Math.max(0, contentHeight - height);
1068
- useInput6((_input, key) => {
1069
- const direction = Number(key.pageDown) - Number(key.pageUp);
1070
- if (direction)
1071
- setOffset(
1072
- (current) => Math.max(0, Math.min(maximum, current + direction * Math.max(1, height - 1)))
1073
- );
1074
- });
1075
- return /* @__PURE__ */ jsxs8(
1076
- Box8,
1077
- {
1078
- flexDirection: "column",
1079
- borderStyle: "round",
1080
- borderColor: colors.line,
1081
- backgroundColor: colors.bg,
1082
- paddingX: 2,
1083
- marginBottom: 1,
1084
- children: [
1085
- /* @__PURE__ */ jsx9(Text8, { color: colors.fg, bold: true, children: title }),
1086
- /* @__PURE__ */ jsx9(
1087
- Box8,
1088
- {
1089
- ref: viewport,
1090
- flexDirection: "column",
1091
- maxHeight: Math.max(1, availableHeight - 5),
1092
- overflow: "hidden",
1093
- contentOffsetY: Math.min(offset, maximum),
1094
- children: /* @__PURE__ */ jsx9(
1095
- Box8,
1096
- {
1097
- ref: content,
1098
- flexDirection: "column",
1099
- flexShrink: 0,
1100
- marginY: availableHeight >= 8 ? 1 : 0,
1101
- children
1102
- }
1103
- )
1104
- }
1105
- ),
1106
- /* @__PURE__ */ jsxs8(Text8, { color: colors.muted, wrap: "truncate-end", children: [
1107
- hints,
1108
- maximum > 0 ? " \u2502 PgUp/PgDn:\u6EDA\u52A8" : ""
1109
- ] })
1110
- ]
1111
- }
1112
- );
1113
- }
1114
- function TerminalPanels({
1115
- panel,
1116
- availableHeight,
1117
- savedTheme,
1118
- onPreviewTheme,
1119
- onApplyTheme,
1120
- site
1121
- }) {
1122
- const { colors } = useTerminalStyle();
1123
- if (!panel) return null;
1124
- if (panel.type === "theme") {
1125
- return /* @__PURE__ */ jsx9(
1126
- PanelBox,
1127
- {
1128
- title: "\u9009\u62E9\u4E3B\u9898",
1129
- availableHeight,
1130
- hints: "\u2191\u2193:\u9884\u89C8 \u2502 Enter:\u5E94\u7528 \u2502 Esc:\u53D6\u6D88",
1131
- children: /* @__PURE__ */ jsx9(
1132
- ThemePicker,
1133
- {
1134
- initial: savedTheme,
1135
- visibleCount: Math.max(1, availableHeight - (availableHeight >= 8 ? 7 : 5)),
1136
- onPreview: onPreviewTheme,
1137
- onApply: onApplyTheme
1138
- }
1139
- )
1140
- }
1141
- );
1142
- }
1143
- if (panel.type === "help") {
1144
- return /* @__PURE__ */ jsxs8(PanelBox, { title: "\u547D\u4EE4", availableHeight, children: [
1145
- terminalCommands.map((command) => /* @__PURE__ */ jsxs8(Box8, { children: [
1146
- /* @__PURE__ */ jsx9(Box8, { width: 20, flexShrink: 0, children: /* @__PURE__ */ jsxs8(Text8, { color: colors.skill, children: [
1147
- "/",
1148
- command.id,
1149
- /* @__PURE__ */ jsx9(Text8, { color: colors.muted, children: command.args ? ` ${command.args}` : "" })
1150
- ] }) }),
1151
- /* @__PURE__ */ jsx9(Box8, { flexGrow: 1, flexShrink: 1, children: /* @__PURE__ */ jsx9(Text8, { color: colors.muted, wrap: "truncate-end", children: command.description }) })
1152
- ] }, command.id)),
1153
- /* @__PURE__ */ jsx9(Text8, { 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" })
1154
- ] });
1155
- }
1156
- if (panel.type === "rss") {
1157
- return /* @__PURE__ */ jsx9(PanelBox, { title: "\u8BA2\u9605", availableHeight, children: /* @__PURE__ */ jsx9(Markdown, { text: `RSS [${site.feedUrl}](${site.feedUrl})` }) });
1158
- }
1159
- return /* @__PURE__ */ jsxs8(
1160
- Text8,
1161
- {
1162
- color: panel.tone === "error" ? colors.err : colors["fg-soft"],
1163
- backgroundColor: colors.bg,
1164
- children: [
1165
- "\u25C6 ",
1166
- cleanText(panel.text)
1167
- ]
1168
- }
1169
- );
1170
- }
1171
-
1172
- // packages/cli/src/run-status.tsx
1173
- import { LoadingPrimitive } from "@assistant-ui/react-ink";
1174
- import { Box as Box9, Text as Text9 } from "ink";
1175
-
1176
- // src/agent/useElapsedTime.ts
1177
- import { useEffect as useEffect4, useState as useState7 } from "react";
1178
- function useElapsedTime(active, live = false) {
1179
- const [seconds, setSeconds] = useState7(0);
1180
- useEffect4(() => {
1181
- if (!active)
1182
- return;
1183
- const startedAt = Date.now();
1184
- const elapsed = () => (Date.now() - startedAt) / 1e3;
1185
- const finish = () => setSeconds(elapsed());
1186
- setSeconds(0);
1187
- if (!live)
1188
- return finish;
1189
- const timer = setInterval(finish, 1e3);
1190
- return () => {
1191
- clearInterval(timer);
1192
- finish();
1193
- };
1194
- }, [active, live]);
1195
- return seconds;
1196
- }
1197
-
1198
- // packages/cli/src/run-status.tsx
1199
- import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1200
- function RunStatus({ running, loading }) {
1201
- const { colors } = useTerminalStyle();
1202
- const seconds = useElapsedTime(running, true);
1203
- if (!running && !loading) return null;
1204
- return /* @__PURE__ */ jsxs9(Box9, { paddingLeft: 2, height: 1, children: [
1205
- /* @__PURE__ */ jsx10(LoadingPrimitive.Spinner, {}),
1206
- /* @__PURE__ */ jsxs9(Text9, { color: colors.fg, children: [
1207
- " ",
1208
- loading ? "\u601D\u8003\u4E2D\u2026" : "\u56DE\u590D\u4E2D\u2026"
1209
- ] }),
1210
- running && /* @__PURE__ */ jsxs9(Text9, { color: colors.muted, children: [
1211
- " ",
1212
- Math.floor(seconds),
1213
- "s"
1214
- ] }),
1215
- /* @__PURE__ */ jsx10(Box9, { flexGrow: 1 }),
1216
- /* @__PURE__ */ jsx10(Text9, { color: colors.muted, children: "[Esc \u505C\u6B62]" })
1217
- ] });
1218
- }
1219
-
1220
- // packages/cli/src/composer.tsx
1221
- import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
1222
- function Composer({
1223
- runtime,
1224
- loading,
1225
- panel,
1226
- onCommand,
1227
- onCancel,
1228
- focusKey,
1229
- onSubmit,
1230
- ...panels
1231
- }) {
1232
- const { colors } = useTerminalStyle();
1233
- const { rows } = useWindowSize2();
1234
- const aui = useAui();
1235
- const { activeId, focus } = useFocusManager2();
1236
- const running = useAuiState2((state) => state.thread.isRunning);
1237
- const text = useAuiState2((state) => state.composer.text);
1238
- const menu = useRef6(null);
1239
- const dock = useRef6(null);
1240
- const { height } = useBoxMetrics6(dock);
1241
- const { isFocused: focused } = useFocus3({ id: "composer", autoFocus: true });
1242
- const restoreComposerFocus = useEffectEvent(() => {
1243
- if (panel?.type !== "theme") focus("composer");
1244
- });
1245
- useEffect5(() => {
1246
- restoreComposerFocus();
1247
- }, [focusKey, panel?.type]);
1248
- useInput7((input, key) => {
1249
- if (key.escape) {
1250
- if (!menu.current?.dismiss()) onCancel();
1251
- focus("composer");
1252
- }
1253
- if (input === "/" && activeId === "content") {
1254
- focus("composer");
1255
- aui.composer.setText("/");
1256
- }
1257
- });
1258
- return /* @__PURE__ */ jsxs10(Box10, { ref: dock, flexDirection: "column", marginX: 2, flexShrink: 0, children: [
1259
- /* @__PURE__ */ jsx11(RunStatus, { loading, running }),
1260
- /* @__PURE__ */ jsxs10(
1261
- Box10,
1262
- {
1263
- borderStyle: "round",
1264
- borderColor: focused ? colors["line-strong"] : colors.line,
1265
- paddingX: 1,
1266
- alignItems: "flex-start",
1267
- children: [
1268
- /* @__PURE__ */ jsx11(Text10, { color: colors.prompt, children: "\u276F " }),
1269
- /* @__PURE__ */ jsx11(
1270
- ComposerInput,
1271
- {
1272
- value: text,
1273
- focused,
1274
- onChange: (value) => aui.composer.setText(value),
1275
- ignoreArrows: () => menu.current?.isOpen() ?? false,
1276
- onSubmit: (text2) => {
1277
- const input = text2.trim();
1278
- if (!input) return;
1279
- if (menu.current?.submit(text2)) return;
1280
- if (input.startsWith("/")) {
1281
- const command = parseCommand(input);
1282
- aui.composer.setText("");
1283
- void onCommand(command?.name ?? "help", command?.args ?? "");
1284
- } else if (!running) {
1285
- onSubmit();
1286
- runtime.thread.composer.send();
1287
- }
1288
- }
1289
- }
1290
- ),
1291
- /* @__PURE__ */ jsx11(Box10, { position: "absolute", bottom: -1, right: 1, children: /* @__PURE__ */ jsxs10(Text10, { color: colors.muted, backgroundColor: colors.bg, children: [
1292
- " ",
1293
- composerBrand.mark,
1294
- " \xB7 ",
1295
- composerBrand.model,
1296
- " "
1297
- ] }) })
1298
- ]
1299
- }
1300
- ),
1301
- /* @__PURE__ */ jsxs10(Text10, { color: colors.muted, wrap: "truncate-end", children: [
1302
- /* @__PURE__ */ jsx11(Text10, { bold: true, children: "/" }),
1303
- ":\u547D\u4EE4 \u2502 ",
1304
- /* @__PURE__ */ jsx11(Text10, { bold: true, children: "Enter" }),
1305
- ":\u53D1\u9001 \u2502 ",
1306
- /* @__PURE__ */ jsx11(Text10, { bold: true, children: "Shift+Enter" }),
1307
- ":\u6362\u884C \u2502 ",
1308
- /* @__PURE__ */ jsx11(Text10, { bold: true, children: "Esc" }),
1309
- ":\u505C\u6B62"
1310
- ] }),
1311
- /* @__PURE__ */ jsx11(Box10, { position: "absolute", bottom: height, width: "100%", flexDirection: "column", children: /* @__PURE__ */ jsx11(
1312
- TerminalPanels,
1313
- {
1314
- panel,
1315
- availableHeight: Math.max(6, rows - height - 1),
1316
- ...panels
1317
- }
1318
- ) }),
1319
- /* @__PURE__ */ jsx11(Box10, { position: "absolute", bottom: height, width: "100%", flexDirection: "column", children: /* @__PURE__ */ jsx11(
1320
- CommandMenu,
1321
- {
1322
- ref: menu,
1323
- text,
1324
- focused,
1325
- availableHeight: Math.max(6, rows - height - 1),
1326
- onSelect: (command) => {
1327
- if (command.id === "read") aui.composer.setText("/read ");
1328
- else {
1329
- aui.composer.setText("");
1330
- void onCommand(command.id, "");
1331
- }
1332
- }
1333
- },
1334
- text
1335
- ) })
1336
- ] });
1337
- }
1338
-
1339
- // packages/cli/src/page-content.tsx
1340
- import { Box as Box12, Text as Text12 } from "ink";
1341
- import { memo } from "react";
1342
-
1343
- // packages/cli/src/search-input.tsx
1344
- import { TextInput } from "@assistant-ui/react-ink";
1345
- import { Box as Box11, Text as Text11 } from "ink";
1346
- import { useEffect as useEffect6, useRef as useRef7, useState as useState8 } from "react";
1347
- import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
1348
- function SearchInput({
1349
- path,
1350
- onSearch
1351
- }) {
1352
- const { colors } = useTerminalStyle();
1353
- const [query, setQuery] = useState8(
1354
- () => new URL(path, "https://miantiao.me").searchParams.get("q") ?? ""
1355
- );
1356
- const timer = useRef7(void 0);
1357
- useEffect6(() => () => clearTimeout(timer.current), []);
1358
- return /* @__PURE__ */ jsxs11(Box11, { borderStyle: "round", borderColor: colors.line, paddingX: 1, marginY: 1, children: [
1359
- /* @__PURE__ */ jsx12(Text11, { color: colors.prompt, children: "\u276F " }),
1360
- /* @__PURE__ */ jsx12(
1361
- TextInput,
1362
- {
1363
- value: query,
1364
- autoFocus: false,
1365
- flexGrow: 1,
1366
- placeholder: "\u641C\u7D22\u6807\u9898\u3001\u63CF\u8FF0\u3001\u6B63\u6587\u4E0E\u6807\u7B7E",
1367
- onChange: (value) => {
1368
- setQuery(value);
1369
- clearTimeout(timer.current);
1370
- timer.current = setTimeout(() => onSearch(value.trim()), 300);
1371
- }
1372
- }
1373
- )
1374
- ] });
1375
- }
1376
-
1377
- // packages/cli/src/page-content.tsx
1378
- import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
1379
- var PageContent = memo(function PageContent2({
1380
- page,
1381
- prompt,
1382
- onSelect,
1383
- scroll,
1384
- onSearch
1385
- }) {
1386
- const { colors } = useTerminalStyle();
1387
- if (page.sections) return /* @__PURE__ */ jsx13(HomeSections, { page, onSelect, scroll });
1388
- return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", marginBottom: 1, children: [
1389
- page.path !== "/" && /* @__PURE__ */ jsx13(Prompt, { text: prompt, command: true }),
1390
- /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", paddingLeft: 2, paddingRight: 1, paddingTop: 1, children: [
1391
- page.path !== "/" && /* @__PURE__ */ jsxs12(Text12, { children: [
1392
- /* @__PURE__ */ jsx13(Text12, { color: colors.skill, children: "\u25C6 " }),
1393
- /* @__PURE__ */ jsx13(Text12, { color: colors.fg, bold: true, children: page.verb }),
1394
- /* @__PURE__ */ jsxs12(Text12, { color: colors.path, children: [
1395
- " ",
1396
- cleanText(page.title)
1397
- ] })
1398
- ] }),
1399
- page.path.startsWith("/search/") && /* @__PURE__ */ jsx13(SearchInput, { path: page.path, onSearch }),
1400
- page.markdown && /* @__PURE__ */ jsx13(Markdown, { text: page.markdown }),
1401
- page.items.length > 0 && /* @__PURE__ */ jsx13(PageList, { items: page.items, onSelect, scroll })
1402
- ] })
1403
- ] });
1404
- });
1405
-
1406
- // packages/cli/src/terminal-layout.tsx
1407
- import { Box as Box13, Text as Text13, useWindowSize as useWindowSize3 } from "ink";
1408
- import { jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
1409
- function TerminalLayout({
1410
- path,
1411
- nav = [],
1412
- children
1413
- }) {
1414
- const { columns, rows } = useWindowSize3();
1415
- const { colors } = useTerminalStyle();
1416
- const directory = `~/miantiao.me${path === "/" ? "" : path.replace(/\/$/, "")}`;
1417
- return /* @__PURE__ */ jsxs13(
1418
- Box13,
1419
- {
1420
- width: columns,
1421
- height: rows,
1422
- flexDirection: "column",
1423
- backgroundColor: colors.bg,
1424
- overflow: "hidden",
1425
- children: [
1426
- /* @__PURE__ */ jsxs13(Box13, { paddingX: 2, flexShrink: 0, children: [
1427
- /* @__PURE__ */ jsx14(Box13, { flexGrow: 1, flexShrink: 1, children: /* @__PURE__ */ jsx14(Text13, { color: colors.muted, wrap: "truncate-middle", children: cleanText(directory) }) }),
1428
- columns >= 70 && nav.map((link) => /* @__PURE__ */ jsx14(Box13, { marginLeft: 2, flexShrink: 0, children: /* @__PURE__ */ jsx14(Text13, { color: path.startsWith(link.href) ? colors.fg : colors.muted, children: cleanText(link.title) }) }, link.href))
1429
- ] }),
1430
- children
1431
- ]
1432
- }
1433
- );
1434
- }
1435
-
1436
- // packages/cli/src/console.tsx
1437
- import { jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
1438
- function Console({
1439
- runtime,
1440
- page,
1441
- prompt,
1442
- revision,
1443
- locationKey,
1444
- site,
1445
- loading,
1446
- panel,
1447
- onCommand,
1448
- onCancel,
1449
- onSelect,
1450
- onSearch,
1451
- onClosePanel,
1452
- ...themePicker
1453
- }) {
1454
- const scroll = useRef8(null);
1455
- const focusKey = `${locationKey}:${revision}`;
1456
- return /* @__PURE__ */ jsx15(TerminalLayout, { path: page.path, nav: site.nav, children: /* @__PURE__ */ jsxs14(ThreadPrimitive2.Root, { flexGrow: 1, flexShrink: 1, minHeight: 0, children: [
1457
- /* @__PURE__ */ jsxs14(
1458
- ScrollArea,
1459
- {
1460
- ref: scroll,
1461
- selection: page.items.length > 0,
1462
- active: !panel || panel.type === "notice",
1463
- children: [
1464
- /* @__PURE__ */ jsx15(
1465
- PageContent,
1466
- {
1467
- page,
1468
- prompt,
1469
- onSelect,
1470
- scroll,
1471
- onSearch
1472
- }
1473
- ),
1474
- /* @__PURE__ */ jsx15(ChatThread, {})
1475
- ]
1476
- },
1477
- focusKey
1478
- ),
1479
- /* @__PURE__ */ jsx15(
1480
- Composer,
1481
- {
1482
- runtime,
1483
- focusKey,
1484
- loading,
1485
- panel,
1486
- site,
1487
- onCommand,
1488
- onCancel,
1489
- ...themePicker,
1490
- onSubmit: () => {
1491
- onClosePanel();
1492
- scroll.current?.end();
1493
- }
1494
- }
1495
- )
1496
- ] }) });
1497
- }
1498
-
1499
- // packages/cli/src/pages.ts
1500
- async function loadPage(client, path, signal) {
1501
- let index = await client.getIndex(signal);
1502
- const url = new URL(path, client.site);
1503
- const pathname = url.pathname.endsWith("/") ? url.pathname : `${url.pathname}/`;
1504
- const page = { path: pathname + url.search, title: "", verb: "List", items: [] };
1505
- const documentPage = [...Object.values(index.pages), ...index.posts].find(
1506
- (item) => new URL(item.path, client.site).pathname === pathname
1507
- );
1508
- if (documentPage) {
1509
- const result = await client.getDocument(documentPage.path, index, signal);
1510
- index = result.index;
1511
- page.path = result.document.path + url.search;
1512
- page.title = result.document.title;
1513
- page.markdown = result.document.markdown;
1514
- page.sections = result.document.sections;
1515
- page.verb = "Read";
1516
- if (page.path === index.pages.home.path) {
1517
- const welcome = page.sections?.find((section) => section.kind === "welcome");
1518
- const menu = welcome?.menu ?? index.site.nav.map((link) => ({ ...link, command: void 0 }));
1519
- const siteOrigins = new Set(
1520
- [client.site, index.site.url].map((site) => new URL(site).origin)
1521
- );
1522
- page.items = menu.map((link) => {
1523
- const destination = new URL(link.href, client.site);
1524
- const external = !siteOrigins.has(destination.origin);
1525
- return {
1526
- title: link.title,
1527
- description: link.command,
1528
- path: external ? destination.href : destination.pathname + destination.search,
1529
- external
1530
- };
1531
- });
1532
- }
1533
- } else if (pathname === "/blog/") {
1534
- page.title = "\u5168\u90E8\u6587\u7AE0";
1535
- page.items = index.posts.map((post) => ({
1536
- title: post.title,
1537
- description: `${post.date.slice(0, 10)} \xB7 ${post.description}`,
1538
- path: post.path
1539
- }));
1540
- } else if (pathname === "/projects/") {
1541
- page.title = "\u6298\u817E\u7684\u9879\u76EE";
1542
- page.items = index.projects.map((project) => ({
1543
- title: `${project.title} \xB7 \u2605 ${project.stars}`,
1544
- description: project.description,
1545
- path: project.url,
1546
- external: true
1547
- }));
1548
- } else if (pathname === "/tags/") {
1549
- page.title = "\u6807\u7B7E";
1550
- page.items = index.tags.map((tag) => ({ title: `${tag.name} (${tag.count})`, path: tag.path }));
1551
- } else if (pathname.startsWith("/tags/")) {
1552
- const tag = index.tags.find((tag2) => tag2.path === pathname);
1553
- if (!tag) throw new Error("\u627E\u4E0D\u5230\u8FD9\u4E2A\u6807\u7B7E\uFF0C\u8BF7\u4F7F\u7528 /tags \u67E5\u627E\u3002");
1554
- page.title = tag.name;
1555
- page.items = index.posts.filter((post) => post.tags.includes(tag.name)).map((post) => ({ title: post.title, description: post.description, path: post.path }));
1556
- } else if (pathname === "/search/") {
1557
- const query = url.searchParams.get("q") ?? "";
1558
- page.title = query ? `\u641C\u7D22\uFF1A${query}` : "\u641C\u7D22\u6587\u7AE0";
1559
- page.verb = "Search";
1560
- if (query) {
1561
- const results = await client.search(query, index, signal);
1562
- page.items = results.map((post) => ({
1563
- title: post.title,
1564
- description: post.description,
1565
- path: post.url
1566
- }));
1567
- }
1568
- if (query && !page.items.length) page.markdown = `\u6CA1\u6709\u627E\u5230\u4E0E\u300C${query}\u300D\u76F8\u5173\u7684\u6587\u7AE0`;
1569
- } else if (pathname === "/rss/") {
1570
- page.title = "\u8BA2\u9605 RSS";
1571
- page.items = [{ title: index.site.feedUrl, path: index.site.feedUrl, external: true }];
1572
- } else {
1573
- throw new Error("\u627E\u4E0D\u5230\u8FD9\u4E2A\u9875\u9762\uFF0C\u8BF7\u4F7F\u7528 /help \u67E5\u770B\u547D\u4EE4\u3002");
1574
- }
1575
- return { index, page };
1576
- }
1577
-
1578
- // packages/cli/src/session.ts
1579
- import { useApp, useInput as useInput8 } from "ink";
1580
- import { useCallback as useCallback2, useState as useState10 } from "react";
1581
-
1582
- // src/agent/command-actions.ts
1583
- async function runSharedCommand(name, args, actions) {
1584
- const path = getCommandPath(name, args);
1585
- if (path) {
1586
- actions.setPanel(null);
1587
- await actions.navigate(path, `/${name} ${args}`.trim());
1588
- return true;
1589
- }
1590
- switch (name) {
1591
- case "help":
1592
- case "rss":
1593
- actions.setPanel({ type: name });
1594
- return true;
1595
- case "theme": {
1596
- if (!args) {
1597
- actions.setPanel({ type: "theme" });
1598
- return true;
1599
- }
1600
- const preference = parseThemePreference(args);
1601
- if (preference) {
1602
- actions.setTheme(preference);
1603
- actions.setPanel({ type: "notice", text: `\u5DF2\u5207\u6362\u5230 ${getThemeName(preference)}` });
1604
- } else {
1605
- actions.setPanel({ type: "notice", tone: "error", text: `\u672A\u77E5\u4E3B\u9898\uFF1A${args}\u3002\u53EF\u9009\uFF1A${themeOptions.map((option) => option.id).join("\u3001")}` });
1606
- }
1607
- return true;
1608
- }
1609
- case "clear":
1610
- actions.clear();
1611
- actions.setPanel(null);
1612
- return true;
1613
- default:
1614
- return false;
1615
- }
1616
- }
1617
-
1618
- // packages/cli/src/agent.ts
1619
- import { useChatRuntime } from "@assistant-ui/ai-sdk";
1620
- import { useEffect as useEffect7, useMemo as useMemo3 } from "react";
1621
-
1622
- // src/agent/transport.ts
1623
- import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
1624
- import { DirectChatTransport, isStepCount, tool, ToolLoopAgent } from "ai";
1625
- import { z as z3 } from "zod";
1626
-
1627
- // src/agent/context-core.ts
1628
- import { escapeRegExp } from "es-toolkit/string";
1629
- function formatTime(now, timeZone) {
1630
- const parts = Object.fromEntries(
1631
- new Intl.DateTimeFormat("en-CA", {
1632
- timeZone,
1633
- year: "numeric",
1634
- month: "2-digit",
1635
- day: "2-digit",
1636
- hour: "2-digit",
1637
- minute: "2-digit",
1638
- second: "2-digit",
1639
- hourCycle: "h23",
1640
- timeZoneName: "longOffset"
1641
- }).formatToParts(now).map((part) => [part.type, part.value])
1642
- );
1643
- const offset = parts.timeZoneName === "GMT" ? "UTC+00:00" : parts.timeZoneName.replace("GMT", "UTC");
1644
- return `${parts.year}-${parts.month}-${parts.day} ${parts.hour}:${parts.minute}:${parts.second} (${timeZone}, ${offset})`;
1645
- }
1646
- function describeDevice(userAgent, viewportWidth) {
1647
- const os = [
1648
- ["iOS", /iPhone|iPad|iPod/],
1649
- ["Android", /Android/],
1650
- ["Windows", /Windows/],
1651
- ["macOS", /Mac OS X|Macintosh/],
1652
- ["Linux", /Linux/]
1653
- ].find(([, pattern]) => pattern.test(userAgent))?.[0] ?? "Unknown OS";
1654
- const browser = [
1655
- ["Edge", /Edg\//],
1656
- ["Firefox", /Firefox\//],
1657
- ["Chrome", /Chrome\//],
1658
- ["Safari", /Safari\//]
1659
- ].find(([, pattern]) => pattern.test(userAgent))?.[0] ?? "Unknown browser";
1660
- return `${os} \xB7 ${browser} \xB7 ${viewportWidth < 768 ? "mobile" : "desktop"}`;
1661
- }
1662
- function buildContextMessage(context, siteName) {
1663
- const title = context.title.replace(new RegExp(`\\s*[|-]\\s*${escapeRegExp(siteName)}.*$`), "") || siteName;
1664
- const lines = [
1665
- `page: ${title} (${context.path})`,
1666
- `theme: ${context.theme}`,
1667
- `language: ${context.languages.slice(0, 3).join(", ")}`,
1668
- `time: ${formatTime(context.now, context.timeZone)}`,
1669
- `device: ${context.terminal ? `terminal \xB7 ${context.terminal.platform} \xB7 ${context.terminal.columns}x${context.terminal.rows}` : describeDevice(context.userAgent, context.viewportWidth)}`
1670
- ];
1671
- if (context.referrer && !context.referrer.startsWith(context.origin))
1672
- lines.push(`referrer: ${context.referrer}`);
1673
- return `<context>
1674
- ${lines.join("\n")}
1675
- </context>`;
1676
- }
1677
- function retryContextMessage(message) {
1678
- return message.replace("\n</context>", "\nretry: 1\n</context>");
1679
- }
1680
-
1681
- // src/agent/fetch.ts
1682
- function stripSseEvent(body, eventName) {
1683
- const pattern = new RegExp(`^event:\\s*${eventName}\\s*$`, "m");
1684
- let buffer = "";
1685
- return body.pipeThrough(new TextDecoderStream()).pipeThrough(new TransformStream({
1686
- transform(text, controller) {
1687
- buffer += text;
1688
- const events = buffer.split(/\r?\n\r?\n/);
1689
- buffer = events.pop() ?? "";
1690
- for (const event of events) {
1691
- if (!pattern.test(event))
1692
- controller.enqueue(`${event}
1693
-
1694
- `);
1695
- }
1696
- },
1697
- flush(controller) {
1698
- if (buffer && !pattern.test(buffer))
1699
- controller.enqueue(buffer);
1700
- }
1701
- })).pipeThrough(new TextEncoderStream());
1702
- }
1703
- var aiSearchFetch = async (input, init) => {
1704
- let response = await fetch(input, init);
1705
- if (response.status >= 500 && typeof init?.body === "string") {
1706
- const body = JSON.parse(init.body);
1707
- const context = body.messages[0];
1708
- if (context?.role === "user" && typeof context.content === "string") {
1709
- context.content = retryContextMessage(context.content);
1710
- await response.body?.cancel();
1711
- response = await fetch(input, { ...init, body: JSON.stringify(body) });
1712
- }
1713
- }
1714
- if (!response.ok || !response.body || !response.headers.get("content-type")?.includes("text/event-stream"))
1715
- return response;
1716
- return new Response(stripSseEvent(response.body, "chunks"), response);
1717
- };
1718
-
1719
- // src/agent/transport.ts
1720
- var themeIds = themeOptions.map((option) => option.id);
1721
- function createAgentTransport(handlers, options) {
1722
- const provider = createOpenAICompatible({
1723
- name: "miantiao",
1724
- baseURL: options.baseUrl,
1725
- fetch: aiSearchFetch
1726
- });
1727
- const agent = new ToolLoopAgent({
1728
- model: provider(options.model),
1729
- maxRetries: 0,
1730
- stopWhen: isStepCount(2),
1731
- // 工具执行后只生成答复,避免模型反复执行同一导航。
1732
- prepareStep: ({ stepNumber }) => stepNumber > 0 ? { toolChoice: "none" } : void 0,
1733
- tools: {
1734
- set_theme: tool({
1735
- description: "\u5207\u6362\u5F53\u524D\u754C\u9762\u4E3B\u9898",
1736
- inputSchema: z3.object({ theme: z3.enum(themeIds) }),
1737
- execute: async ({ theme }) => {
1738
- handlers.setTheme(theme);
1739
- return `\u5DF2\u5207\u6362\u5230 ${getThemeName(theme)}`;
1740
- }
1741
- }),
1742
- open_page: tool({
1743
- description: "\u6253\u5F00\u7AD9\u5185\u9875\u9762\uFF0C\u53C2\u6570\u4E3A\u4EE5 / \u5F00\u5934\u7684\u7AD9\u5185\u8DEF\u5F84",
1744
- inputSchema: z3.object({ path: z3.string().regex(/^\/(?!\/)[^\s\\]*$/) }),
1745
- execute: async ({ path }) => {
1746
- await handlers.openPage(path);
1747
- return `\u5DF2\u6253\u5F00 ${path}`;
1748
- }
1749
- })
1750
- },
1751
- // DirectChatTransport 以 `prompt` 传入完整的消息历史
1752
- prepareCall: ({ prompt, messages, ...rest }) => ({
1753
- ...rest,
1754
- messages: [
1755
- { role: "user", content: buildContextMessage(handlers.getContext(), options.siteName) },
1756
- { role: "assistant", content: "\u5DF2\u8BFB\u53D6\u4E0A\u4E0B\u6587\u3002" },
1757
- ...messages ?? (Array.isArray(prompt) ? prompt : [])
1758
- ]
1759
- })
1760
- });
1761
- return new DirectChatTransport({ agent, onError: () => "AI \u6682\u65F6\u65E0\u6CD5\u56DE\u590D\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002" });
1762
- }
1763
-
1764
- // packages/cli/src/agent.ts
1765
- function useTerminalAgent({ site, index, page, theme, setTheme, navigate }) {
1766
- const transport = useMemo3(
1767
- () => createAgentTransport(
1768
- {
1769
- getContext: () => {
1770
- const locale = Intl.DateTimeFormat().resolvedOptions();
1771
- return {
1772
- title: page.title,
1773
- path: page.path,
1774
- theme,
1775
- languages: [locale.locale],
1776
- timeZone: locale.timeZone,
1777
- now: /* @__PURE__ */ new Date(),
1778
- userAgent: "",
1779
- viewportWidth: process.stdout.columns || 80,
1780
- referrer: "",
1781
- origin: site,
1782
- terminal: {
1783
- platform: process.platform,
1784
- columns: process.stdout.columns || 80,
1785
- rows: process.stdout.rows || 24
1786
- }
1787
- };
1788
- },
1789
- setTheme,
1790
- openPage: async (path) => {
1791
- if (!await navigate(path)) throw new Error("\u65E0\u6CD5\u6253\u5F00\u8FD9\u4E2A\u7AD9\u5185\u9875\u9762\u3002");
1792
- }
1793
- },
1794
- { baseUrl: index.ai.baseUrl, model: index.ai.model, siteName: index.site.name }
1795
- ),
1796
- [
1797
- site,
1798
- page.title,
1799
- page.path,
1800
- theme,
1801
- setTheme,
1802
- navigate,
1803
- index.ai.baseUrl,
1804
- index.ai.model,
1805
- index.site.name
1806
- ]
1807
- );
1808
- const runtime = useChatRuntime({ transport });
1809
- useEffect7(() => () => runtime.thread.cancelRun(), [runtime]);
1810
- return runtime;
1811
- }
1812
-
1813
- // packages/cli/src/browser.ts
1814
- import { spawn } from "node:child_process";
1815
- async function openBrowser(input, site) {
1816
- const url = new URL(input, site);
1817
- if (!["http:", "https:"].includes(url.protocol))
1818
- throw new Error("\u53EA\u80FD\u5728\u6D4F\u89C8\u5668\u6253\u5F00 HTTP \u6216 HTTPS \u94FE\u63A5\u3002");
1819
- const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
1820
- await new Promise((resolve, reject) => {
1821
- const child = spawn(command, [url.href], { stdio: "ignore" });
1822
- child.once("error", () => reject(new Error(`\u65E0\u6CD5\u6253\u5F00\u6D4F\u89C8\u5668\uFF0C\u8BF7\u624B\u52A8\u8BBF\u95EE ${url.href}`)));
1823
- child.once(
1824
- "exit",
1825
- (code) => code === 0 ? resolve() : reject(new Error(`\u65E0\u6CD5\u6253\u5F00\u6D4F\u89C8\u5668\uFF0C\u8BF7\u624B\u52A8\u8BBF\u95EE ${url.href}`))
1826
- );
1827
- });
1828
- }
1829
-
1830
- // packages/cli/src/navigation.ts
1831
- import { useCallback, useEffect as useEffect8, useRef as useRef9, useState as useState9 } from "react";
1832
- function useNavigation(client, initial) {
1833
- const [state, setState] = useState9({
1834
- ...initial,
1835
- status: "idle",
1836
- panel: client.warning ? { type: "notice", text: client.warning } : null,
1837
- prompt: "/home",
1838
- locationKey: 0
1839
- });
1840
- const active = useRef9(void 0);
1841
- useEffect8(
1842
- () => () => {
1843
- active.current?.controller.abort();
1844
- active.current = void 0;
1845
- },
1846
- []
1847
- );
1848
- const cancel = useCallback(() => {
1849
- active.current?.controller.abort();
1850
- active.current = void 0;
1851
- setState((current) => current.status === "idle" ? current : { ...current, status: "idle" });
1852
- }, []);
1853
- const setPanel = useCallback((panel) => {
1854
- setState((current) => ({ ...current, panel }));
1855
- }, []);
1856
- const notify = useCallback(
1857
- (text, tone) => {
1858
- setPanel({ type: "notice", text, tone });
1859
- },
1860
- [setPanel]
1861
- );
1862
- const navigate = useCallback(
1863
- async (path, prompt = path, replace = false) => {
1864
- if (replace && active.current && !active.current.replace) return false;
1865
- active.current?.controller.abort();
1866
- const request = { controller: new AbortController(), replace };
1867
- active.current = request;
1868
- setState((current) => ({ ...current, status: "loading", panel: null }));
1869
- let next;
1870
- try {
1871
- next = await loadPage(client, path, request.controller.signal);
1872
- } catch (error) {
1873
- if (active.current === request) {
1874
- active.current = void 0;
1875
- setState((current) => ({
1876
- ...current,
1877
- status: "idle",
1878
- panel: {
1879
- type: "notice",
1880
- tone: "error",
1881
- text: error instanceof Error ? error.message : "\u65E0\u6CD5\u8BFB\u53D6\u5B98\u7F51\u5185\u5BB9\u3002"
1882
- }
1883
- }));
1884
- }
1885
- return false;
1886
- }
1887
- if (active.current !== request) return false;
1888
- active.current = void 0;
1889
- setState((current) => ({
1890
- ...next,
1891
- status: "idle",
1892
- panel: client.warning ? { type: "notice", text: client.warning } : null,
1893
- prompt,
1894
- locationKey: current.locationKey + Number(!replace)
1895
- }));
1896
- return true;
1897
- },
1898
- [client]
1899
- );
1900
- return { ...state, navigate, setPanel, cancel, notify };
1901
- }
1902
-
1903
- // packages/cli/src/session.ts
1904
- function useTerminalSession(client, initial) {
1905
- const { exit } = useApp();
1906
- const [theme, setTheme] = useState10("auto");
1907
- const [previewTheme, setPreviewTheme] = useState10();
1908
- const { index, page, prompt, locationKey, status, panel, navigate, setPanel, cancel, notify } = useNavigation(client, initial);
1909
- const runtime = useTerminalAgent({ site: client.site, index, page, theme, setTheme, navigate });
1910
- const stop = () => {
1911
- cancel();
1912
- runtime.thread.cancelRun();
1913
- };
1914
- useInput8((input, key) => {
1915
- if (key.ctrl && input === "c") {
1916
- stop();
1917
- exit();
1918
- }
1919
- });
1920
- const onClosePanel = () => {
1921
- setPreviewTheme(void 0);
1922
- setPanel(null);
1923
- };
1924
- const onCancel = () => {
1925
- if (panel) onClosePanel();
1926
- else stop();
1927
- };
1928
- const onApplyTheme = (preference) => {
1929
- setPreviewTheme(void 0);
1930
- setTheme(preference);
1931
- notify(`\u5DF2\u5207\u6362\u5230 ${getThemeName(preference)}`);
1932
- };
1933
- const onCommand = async (name, args) => {
1934
- setPreviewTheme(void 0);
1935
- try {
1936
- if (await runSharedCommand(name, args, {
1937
- navigate: (path, prompt2) => navigate(name === "tags" && args ? resolveTagPath(args, index) : path, prompt2),
1938
- setPanel,
1939
- setTheme,
1940
- clear: () => {
1941
- stop();
1942
- runtime.thread.reset();
1943
- }
1944
- })) return;
1945
- switch (name) {
1946
- case "read":
1947
- await navigate(resolveReadPath(args, page), `/read ${args}`);
1948
- break;
1949
- case "refresh":
1950
- await navigate(page.path, "/refresh");
1951
- break;
1952
- case "open":
1953
- await openBrowser(resolveOpenTarget(args, page), client.site);
1954
- break;
1955
- case "quit":
1956
- stop();
1957
- exit();
1958
- break;
1959
- default:
1960
- notify(`\u672A\u77E5\u547D\u4EE4\uFF1A/${name}\uFF0C\u8F93\u5165 /help \u67E5\u770B\u53EF\u7528\u547D\u4EE4`, "error");
1961
- }
1962
- } catch (error) {
1963
- notify(error instanceof Error ? error.message : "\u65E0\u6CD5\u6267\u884C\u547D\u4EE4\u3002", "error");
1964
- }
1965
- };
1966
- const onSelect = (path, external = false) => {
1967
- if (external) void onCommand("open", path);
1968
- else void navigate(path);
1969
- };
1970
- const onSearch = useCallback2(
1971
- (query) => {
1972
- void navigate(getSearchPath(query), `/search ${query}`.trim(), true);
1973
- },
1974
- [navigate]
1975
- );
1976
- return {
1977
- runtime,
1978
- page,
1979
- revision: index.revision,
1980
- locationKey,
1981
- prompt,
1982
- site: index.site,
1983
- loading: status === "loading",
1984
- panel,
1985
- onCommand,
1986
- onCancel,
1987
- onClosePanel,
1988
- onSelect,
1989
- onSearch,
1990
- theme: previewTheme ?? theme,
1991
- savedTheme: theme,
1992
- onPreviewTheme: setPreviewTheme,
1993
- onApplyTheme
1994
- };
1995
- }
1996
-
1997
- // packages/cli/src/app.tsx
1998
- import { jsx as jsx16, jsxs as jsxs15 } from "react/jsx-runtime";
1999
- function Session({
2000
- client,
2001
- initial,
2002
- width
2003
- }) {
2004
- const { theme, ...session } = useTerminalSession(client, initial);
2005
- return /* @__PURE__ */ jsx16(TerminalTheme, { preference: theme, width, children: /* @__PURE__ */ jsx16(AssistantRuntimeProvider, { runtime: session.runtime, children: /* @__PURE__ */ jsx16(Console, { ...session }) }) });
2006
- }
2007
- function App({ site }) {
2008
- const { exit } = useApp2();
2009
- const { columns } = useWindowSize4();
2010
- const [client] = useState11(() => new TerminalClient(site));
2011
- const [state, setState] = useState11({ status: "loading" });
2012
- const [attempt, setAttempt] = useState11(0);
2013
- const width = Math.max(10, columns - 5);
2014
- useEffect9(() => {
2015
- const request = new AbortController();
2016
- loadPage(client, "/", request.signal).then((content) => {
2017
- if (!request.signal.aborted) setState({ status: "ready", content });
2018
- }).catch((error) => {
2019
- if (!request.signal.aborted)
2020
- setState({
2021
- status: "error",
2022
- message: error instanceof Error ? error.message : "\u65E0\u6CD5\u8BFB\u53D6\u5B98\u7F51\u5185\u5BB9\u3002"
2023
- });
2024
- });
2025
- return () => request.abort();
2026
- }, [client, attempt]);
2027
- useInput9(
2028
- (input, key) => {
2029
- if (key.ctrl && input === "c" || input === "q") exit();
2030
- else if (state.status === "error" && (key.return || input === "r" || input === "/refresh")) {
2031
- setState({ status: "loading" });
2032
- setAttempt((value) => value + 1);
2033
- }
2034
- },
2035
- { isActive: state.status !== "ready" }
2036
- );
2037
- if (state.status === "ready")
2038
- return /* @__PURE__ */ jsx16(Session, { client, initial: state.content, width });
2039
- return /* @__PURE__ */ jsx16(TerminalTheme, { preference: "auto", width, children: /* @__PURE__ */ jsx16(TerminalLayout, { path: "/", children: /* @__PURE__ */ jsxs15(Box14, { flexDirection: "column", flexGrow: 1, padding: 2, children: [
2040
- /* @__PURE__ */ jsxs15(Text14, { children: [
2041
- "\u25C6 ",
2042
- state.status === "error" ? state.message : "\u601D\u8003\u4E2D\u2026\u2026"
2043
- ] }),
2044
- /* @__PURE__ */ jsx16(Text14, { children: state.status === "error" ? "Enter \u91CD\u8BD5 \xB7 q \u9000\u51FA" : "Ctrl+C \u9000\u51FA" })
2045
- ] }) }) });
2046
- }
2047
- async function run(site) {
2048
- const app = render(/* @__PURE__ */ jsx16(App, { site }), {
2049
- exitOnCtrlC: false,
2050
- alternateScreen: true,
2051
- incrementalRendering: true
2052
- });
2053
- await app.waitUntilExit();
2054
- }
2055
- export {
2056
- run
2057
- };