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