@leviyuan/lodestar 0.2.9 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,22 +0,0 @@
1
- /**
2
- * Element-id convention (must be unique within a card):
3
- * user_input — the collapsible "你说" panel
4
- * thinking — the de-emphasized thinking stream
5
- * tool_<i> — one collapsible per tool call, indexed from 0
6
- * assistant — the main streaming assistant answer
7
- * footer — runtime footer (timing / status)
8
- */
9
- export const ELEMENTS = {
10
- thinking: 'thinking',
11
- footer: 'footer',
12
- tool: (i: number) => `tool_${i}`,
13
- /** Assistant text is segmented: every tool call closes the running segment
14
- * and the next assistant chunk opens a new one, so element order in the
15
- * card matches Claude's emission order. */
16
- assistant: (i: number) => `assistant_${i}`,
17
- /** Console (hi) card — the subscription-usage row is rendered as its
18
- * own element so we can replace it after the initial card lands,
19
- * decoupling the slow ccusage fetch from the rest of the panel's
20
- * synchronous data. */
21
- consoleUsage: 'console_usage',
22
- } as const
package/src/cards/turn.ts DELETED
@@ -1,530 +0,0 @@
1
- /**
2
- * Schema 2.0 turn-card templates: the main streaming card, the per-tool
3
- * collapsible panels, and the AskUserQuestion interactive panel. All
4
- * rendering for "the in-flight conversation card" lives here. Console
5
- * UI lives in console.ts; the shared element-id convention is in
6
- * elements.ts.
7
- */
8
-
9
- import { ELEMENTS } from './elements'
10
-
11
- /** Minimal projection of an SDK task — used by Session's local mirror,
12
- * built incrementally from observed TaskCreate / TaskUpdate input+output
13
- * pairs. Not authoritative (the SDK is the source of truth), but enough
14
- * to render the "全部任务清单" footer on every Task* panel. */
15
- export interface Todo {
16
- id: number
17
- subject?: string
18
- description?: string
19
- status: 'pending' | 'in_progress' | 'completed' | string
20
- owner?: string
21
- activeForm?: string
22
- }
23
-
24
- function todoStatusIcon(s: string): string {
25
- switch (s) {
26
- case 'pending': return '☐'
27
- case 'in_progress': return '🔄'
28
- case 'completed': return '✅'
29
- default: return '·'
30
- }
31
- }
32
-
33
- /** Render the session's full todo mirror as a markdown list. Empty list
34
- * yields '' so callers can unconditionally concat. Sorted by numeric id
35
- * so the order matches creation order regardless of Map iteration. */
36
- function renderTodoList(todos: Todo[]): string {
37
- if (!todos || todos.length === 0) return ''
38
- const sorted = [...todos].sort((a, b) => a.id - b.id)
39
- const lines = ['', '---', `**📋 当前任务清单(${sorted.length} 项)**`, '']
40
- for (const t of sorted) {
41
- const icon = todoStatusIcon(t.status)
42
- const subject = t.subject ?? '(无 subject)'
43
- const ownerTag = t.owner ? ` · ${t.owner}` : ''
44
- lines.push(`- ${icon} **#${t.id}** ${subject}${ownerTag}`)
45
- }
46
- return lines.join('\n')
47
- }
48
-
49
- /** Single-line summary used as a collapsible-panel header for a tool call. */
50
- export function summarizeToolInput(name: string, input: any): string {
51
- if (!input || typeof input !== 'object') return ''
52
- const truncate = (s: string, n: number) => s.length > n ? s.slice(0, n) + '…' : s
53
- // Task workflow tools (TaskCreate / TaskUpdate / TaskList / ...) carry
54
- // structured fields that summarize much better as natural language than
55
- // as truncated JSON. Routed first so they don't fall through to the
56
- // generic Agent/Task case below.
57
- if (name.startsWith('Task') && name !== 'Task') {
58
- return truncate(summarizeTaskWorkflow(name, input), 80)
59
- }
60
- switch (name) {
61
- case 'Bash': return truncate(String(input.description ?? input.command ?? ''), 80)
62
- case 'Read':
63
- case 'Write':
64
- case 'Edit':
65
- case 'NotebookEdit': return truncate(String(input.file_path ?? ''), 80)
66
- case 'Glob': return truncate(String(input.pattern ?? ''), 80)
67
- case 'Grep': return truncate(`${input.pattern ?? ''}${input.path ? ' in ' + input.path : ''}`, 80)
68
- case 'WebFetch':
69
- case 'WebSearch': return truncate(String(input.url ?? input.query ?? ''), 80)
70
- case 'Agent':
71
- case 'Task': return truncate(String(input.description ?? input.subject ?? ''), 80)
72
- case 'Skill': return truncate(String(input.skill ?? ''), 80)
73
- }
74
- // generic fallback: first string-valued field
75
- for (const v of Object.values(input)) {
76
- if (typeof v === 'string' && v) return truncate(v, 80)
77
- }
78
- return ''
79
- }
80
-
81
- /** Header summary for Task* workflow tools — `Task` (singular) is the
82
- * separate subagent-spawn tool and is handled above; everything else
83
- * (TaskCreate / TaskUpdate / TaskList / TaskGet / TaskStop / TaskOutput /
84
- * TaskDelete) summarises through here. */
85
- function summarizeTaskWorkflow(name: string, input: any): string {
86
- switch (name) {
87
- case 'TaskCreate':
88
- return `📝 创建: ${input.subject ?? '(无 subject)'}`
89
- case 'TaskUpdate': {
90
- const parts: string[] = []
91
- if (input.status) parts.push(`→ ${input.status}`)
92
- if (input.owner) parts.push(`owner=${input.owner}`)
93
- if (input.subject) parts.push(`subject="${input.subject}"`)
94
- if (input.addBlocks) parts.push(`blocks=[${(input.addBlocks ?? []).join(',')}]`)
95
- if (input.addBlockedBy) parts.push(`blockedBy=[${(input.addBlockedBy ?? []).join(',')}]`)
96
- const tail = parts.length ? ' ' + parts.join(', ') : ''
97
- return `✏️ #${input.taskId ?? '?'}${tail}`
98
- }
99
- case 'TaskList': return '📋 查询任务列表'
100
- case 'TaskGet': return `🔍 查询 #${input.taskId ?? '?'}`
101
- case 'TaskStop': return `⏹ 停止 #${input.taskId ?? '?'}`
102
- case 'TaskOutput': return `📤 取输出 #${input.taskId ?? '?'}`
103
- case 'TaskDelete': return `🗑 删除 #${input.taskId ?? '?'}`
104
- }
105
- return name
106
- }
107
-
108
- /** Markdown body for Task* workflow tools — replaces the generic JSON
109
- * dump with a human-readable description of the operation plus, once the
110
- * tool result is in, the SDK's text reply (which already contains "Task
111
- * #N created" / "Updated task #X" / a rendered list for TaskList). When
112
- * `todos` is non-empty, the full mirror is appended as a "📋 当前任务
113
- * 清单" footer so every Task* panel doubles as a current-state readout. */
114
- function renderTaskWorkflowBody(name: string, input: any, output: string | null, todos?: Todo[]): string {
115
- const lines: string[] = []
116
- switch (name) {
117
- case 'TaskCreate':
118
- lines.push(`**📝 创建任务**`)
119
- if (input.subject) lines.push(`- subject: ${input.subject}`)
120
- if (input.description) lines.push(`- 描述: ${input.description}`)
121
- if (input.activeForm) lines.push(`- 进行时: ${input.activeForm}`)
122
- break
123
- case 'TaskUpdate': {
124
- lines.push(`**✏️ 更新 #${input.taskId ?? '?'}**`)
125
- if (input.status) lines.push(`- status → \`${input.status}\``)
126
- if (input.subject) lines.push(`- subject: ${input.subject}`)
127
- if (input.description) lines.push(`- description: ${input.description}`)
128
- if (input.owner) lines.push(`- owner: ${input.owner}`)
129
- if (input.activeForm) lines.push(`- 进行时: ${input.activeForm}`)
130
- if (input.addBlocks) lines.push(`- blocks → ${(input.addBlocks).join(', ')}`)
131
- if (input.addBlockedBy) lines.push(`- blockedBy → ${(input.addBlockedBy).join(', ')}`)
132
- if (input.metadata) lines.push(`- metadata: \`${JSON.stringify(input.metadata)}\``)
133
- break
134
- }
135
- case 'TaskList': lines.push('**📋 查询当前任务清单**'); break
136
- case 'TaskGet': lines.push(`**🔍 查询 #${input.taskId ?? '?'}**`); break
137
- case 'TaskStop': lines.push(`**⏹ 停止 #${input.taskId ?? '?'}**`); break
138
- case 'TaskOutput': lines.push(`**📤 取 #${input.taskId ?? '?'} 输出**`); break
139
- case 'TaskDelete': lines.push(`**🗑 删除 #${input.taskId ?? '?'}**`); break
140
- default:
141
- lines.push(`**${name}**`)
142
- lines.push('```\n' + JSON.stringify(input ?? {}, null, 2).slice(0, 1000) + '\n```')
143
- }
144
- if (output != null) {
145
- lines.push('')
146
- lines.push('---')
147
- lines.push('**结果**')
148
- lines.push(output.slice(0, 3000))
149
- }
150
- return lines.join('\n') + renderTodoList(todos ?? [])
151
- }
152
-
153
- interface MainCardOpts {
154
- sessionName: string
155
- turn: number
156
- model?: string
157
- effort?: string
158
- /** What started this turn. `'scheduled'` adds a top-of-card banner so
159
- * the user can tell a cron-fired wakeup apart from one of their own
160
- * messages — the user's message bubble is otherwise the only visual
161
- * cue, and scheduled turns have no preceding bubble in the chat. */
162
- kind?: 'user_message' | 'scheduled'
163
- }
164
-
165
- /** Initial card sent at the start of each turn. Streaming on. */
166
- export function mainConversationCard(opts: MainCardOpts): object {
167
- const banner = opts.kind === 'scheduled'
168
- ? [{ tag: 'markdown', content: '⏰ 触发' }]
169
- : []
170
- return {
171
- schema: '2.0',
172
- config: {
173
- streaming_mode: true,
174
- summary: { content: '[Lodestar 正在生成…]' },
175
- streaming_config: {
176
- print_frequency_ms: { default: 60, android: 60, ios: 60, pc: 30 },
177
- print_step: { default: 2, android: 2, ios: 2, pc: 4 },
178
- print_strategy: 'fast',
179
- },
180
- },
181
- body: {
182
- // Initial body has just thinking + footer; assistant segments and tool
183
- // panels are inserted between them in real time as Claude streams.
184
- // Note: empty-string content is rejected by CardKit PUT so the
185
- // thinking element starts with a single space placeholder; the first
186
- // real append overwrites it. No echo of the user's message inside
187
- // the card — the chat bubble above already shows it.
188
- elements: [
189
- ...banner,
190
- { tag: 'markdown', element_id: ELEMENTS.thinking, content: ' ' },
191
- { tag: 'markdown', element_id: ELEMENTS.footer, content: '⏳ working…' },
192
- ],
193
- },
194
- }
195
- }
196
-
197
- /** Empty assistant segment to be inserted just before the footer. */
198
- export function assistantSegmentElement(i: number): object {
199
- return { tag: 'markdown', element_id: ELEMENTS.assistant(i), content: ' ' }
200
- }
201
-
202
- /** Final state for the thinking section once a turn closes — collapse the
203
- * full thinking text into a panel so the card stays clean. Replaces the
204
- * top-level `thinking` markdown element via PUT /elements/:id. */
205
- export function thinkingCollapsedPanel(fullText: string): object {
206
- const trimmed = fullText.trim()
207
- return {
208
- tag: 'collapsible_panel',
209
- element_id: ELEMENTS.thinking,
210
- header: { title: { tag: 'plain_text', content: `💭 思考过程 (${trimmed.length} 字)` } },
211
- expanded: false,
212
- elements: [
213
- { tag: 'markdown', content: trimmed.slice(0, 8000) || '_(空)_' },
214
- ],
215
- }
216
- }
217
-
218
- /** Element to insert for each tool call. Expandable for big results.
219
- *
220
- * Header is a one-line summary: status + name + summarized input.
221
- * Body holds the full input + (after completion) the full output.
222
- * `resolvedNote` is an optional one-liner appended below the input —
223
- * used to surface "✅ 允许 by Alice" inline after a permission decision
224
- * lands but before the actual tool execution completes. */
225
- export function toolCallElement(
226
- i: number,
227
- name: string,
228
- input: any,
229
- output: string | null,
230
- status: '⏳' | '✅' | '❌' = '⏳',
231
- resolvedNote?: string,
232
- /** Session's full todo mirror — only rendered when the tool is a Task*
233
- * workflow op. Other tools ignore it. Passed in by Session so every
234
- * Task* panel shows the *current* state, not just this op's diff. */
235
- todos?: Todo[],
236
- ): object {
237
- const summary = summarizeToolInput(name, input)
238
- const headerText = summary
239
- ? `${status} 🔧 ${name}: ${summary}`
240
- : `${status} 🔧 ${name}`
241
- const isTaskWorkflow = name.startsWith('Task') && name !== 'Task'
242
- const noteBlock = resolvedNote ? `\n\n${resolvedNote}` : ''
243
- // Task* gets a narrative body (operation + result + current todo list),
244
- // the rest keeps the JSON-input + raw-output split — generic dump is
245
- // better for unfamiliar tools where users can't predict what fields
246
- // matter.
247
- const body = isTaskWorkflow
248
- ? renderTaskWorkflowBody(name, input, output, todos) + noteBlock
249
- : '```\n' + JSON.stringify(input ?? {}, null, 2).slice(0, 2000) + '\n```'
250
- + noteBlock
251
- + (output != null ? '\n---\n**output:**\n```\n' + output.slice(0, 3000) + '\n```' : '')
252
- return {
253
- tag: 'collapsible_panel',
254
- element_id: ELEMENTS.tool(i),
255
- header: { title: { tag: 'plain_text', content: headerText } },
256
- expanded: false,
257
- elements: [
258
- { tag: 'markdown', content: body },
259
- ],
260
- }
261
- }
262
-
263
- /** Merged panel for a run of consecutive `Read` tool calls in one turn.
264
- * Header shows the dynamic count (`Read · 3 次`), body lists one row per
265
- * Read with its own status + file path. Replaces the individual panels
266
- * once a second Read joins the batch — single Reads still render as the
267
- * full `toolCallElement` (with file-contents dump on completion). */
268
- export function readBatchElement(
269
- i: number,
270
- items: Array<{ input: any; output: string | null; isError: boolean }>,
271
- ): object {
272
- const n = items.length
273
- const anyError = items.some(it => it.isError)
274
- const allDone = items.every(it => it.output !== null)
275
- const status = anyError ? '❌' : allDone ? '✅' : '⏳'
276
- const headerText = `${status} 🔧 Read · ${n} 次`
277
- const lines = items.map(it => `\`${String(it.input.file_path ?? '(无 path)')}\``)
278
- return {
279
- tag: 'collapsible_panel',
280
- element_id: ELEMENTS.tool(i),
281
- header: { title: { tag: 'plain_text', content: headerText } },
282
- expanded: false,
283
- elements: [{ tag: 'markdown', content: lines.join('\n') }],
284
- }
285
- }
286
-
287
- /** Same tool panel as `toolCallElement`, but with the 🔐 status and
288
- * three inline action buttons (allow / allow_always / deny). Expanded
289
- * by default so the user can read the request without clicking through.
290
- * This is the "merge into tool panel" UX — the permission decision
291
- * lives on the same row as the tool call instead of as a separate
292
- * floating card. */
293
- export function toolCallPermissionElement(
294
- i: number,
295
- name: string,
296
- input: any,
297
- requestId: string,
298
- ): object {
299
- const summary = summarizeToolInput(name, input)
300
- const headerText = summary
301
- ? `🔐 等审批 · ${name}: ${summary}`
302
- : `🔐 等审批 · ${name}`
303
- const inputBlock = '```\n' + JSON.stringify(input ?? {}, null, 2).slice(0, 2000) + '\n```'
304
- return {
305
- tag: 'collapsible_panel',
306
- element_id: ELEMENTS.tool(i),
307
- header: { title: { tag: 'plain_text', content: headerText } },
308
- expanded: true,
309
- elements: [
310
- { tag: 'markdown', content: inputBlock },
311
- {
312
- tag: 'column_set',
313
- columns: [
314
- permissionButtonColumn('✅ 允许', 'primary', requestId, 'allow'),
315
- permissionButtonColumn('♾️ 始终允许', 'default', requestId, 'allow_always'),
316
- permissionButtonColumn('❌ 拒绝', 'danger', requestId, 'deny'),
317
- ],
318
- },
319
- ],
320
- }
321
- }
322
-
323
- function permissionButtonColumn(label: string, type: string, requestId: string, decision: string): object {
324
- return {
325
- tag: 'column', width: 'weighted', weight: 1,
326
- elements: [{
327
- tag: 'button',
328
- text: { tag: 'plain_text', content: label },
329
- type,
330
- behaviors: [{ type: 'callback', value: { kind: 'permission', request_id: requestId, decision } }],
331
- }],
332
- }
333
- }
334
-
335
- /** Schema of an AskUserQuestion question, projected to just the fields
336
- * the panel needs. Mirrors the SDK tool's input — kept loose since the
337
- * runtime guarantees it matches. */
338
- export interface AskQuestion {
339
- question: string
340
- header?: string
341
- multiSelect?: boolean
342
- options: { label: string; description?: string }[]
343
- }
344
-
345
- /** Per-question final-state. Mutually-exclusive branches: option pick
346
- * vs. free-form custom text. */
347
- export interface AskAnswered {
348
- optionIdx?: number
349
- customText?: string
350
- user?: string
351
- }
352
-
353
- /** State the panel renders against. `currentIdx` undefined → terminal
354
- * (every question answered). Otherwise it's the question currently on
355
- * screen; everything in `answered` is history. */
356
- export interface AskState {
357
- currentIdx?: number
358
- answered: Map<number, AskAnswered>
359
- }
360
-
361
- /** Render one question's body — either as clickable interactive_container
362
- * rows (when picked === undefined) or as plain markdown summary
363
- * (already-answered, shown in history-panel context). */
364
- function renderAskQuestionBody(
365
- q: AskQuestion,
366
- toolUseId: string,
367
- questionIdx: number,
368
- picked?: AskAnswered,
369
- ): any[] {
370
- const els: any[] = []
371
- els.push({ tag: 'markdown', content: `**${q.question}**` })
372
- for (let oi = 0; oi < q.options.length; oi++) {
373
- const opt = q.options[oi]
374
- const desc = opt.description ? ` · ${opt.description}` : ''
375
- if (picked) {
376
- const isPicked = picked.optionIdx === oi
377
- els.push({
378
- tag: 'markdown',
379
- content: isPicked
380
- ? `✅ **${opt.label}**${desc}`
381
- : `~~◯ ${opt.label}${desc}~~`,
382
- })
383
- } else {
384
- els.push({
385
- tag: 'interactive_container',
386
- background_style: 'default',
387
- has_border: true,
388
- corner_radius: '6px',
389
- padding: '8px 12px',
390
- margin: '4px 0px 4px 0px',
391
- behaviors: [{
392
- type: 'callback',
393
- value: {
394
- kind: 'ask',
395
- tool_use_id: toolUseId,
396
- question_idx: questionIdx,
397
- option_idx: oi,
398
- },
399
- }],
400
- elements: [{ tag: 'markdown', content: `**${opt.label}**${desc}` }],
401
- })
402
- }
403
- }
404
- if (picked?.customText) {
405
- els.push({ tag: 'markdown', content: `✏️ **自定义回答**:${picked.customText}` })
406
- }
407
- return els
408
- }
409
-
410
- /** Folded "📜 已答 N 题" panel — option C from the multi-question
411
- * design discussion. Returns null when there's no history to show. */
412
- function renderAskHistoryPanel(
413
- questions: AskQuestion[],
414
- answered: Map<number, AskAnswered>,
415
- ): any | null {
416
- if (answered.size === 0) return null
417
- const lines: string[] = []
418
- const sortedIdx = [...answered.keys()].sort((a, b) => a - b)
419
- for (const idx of sortedIdx) {
420
- const q = questions[idx]
421
- const a = answered.get(idx)!
422
- const tag = q?.header ?? `Q${idx + 1}`
423
- const value = a.customText
424
- ?? (a.optionIdx !== undefined ? q?.options[a.optionIdx]?.label : undefined)
425
- ?? '?'
426
- lines.push(`- ✅ **${tag}**:${value}`)
427
- }
428
- return {
429
- tag: 'collapsible_panel',
430
- header: {
431
- title: { tag: 'plain_text', content: `📜 已答 ${answered.size} 题(点击展开)` },
432
- },
433
- expanded: false,
434
- elements: [{ tag: 'markdown', content: lines.join('\n') }],
435
- }
436
- }
437
-
438
- /** Tool-panel renderer for `AskUserQuestion` — the SDK's structured
439
- * multiple-choice question. Daemon takes over the client-side role:
440
- * instead of letting the request fall through to the generic JSON
441
- * dump (or worse, the permission flow that misappropriates it), we
442
- * render each question with one button per option, callbacks tagged
443
- * `kind:'ask'` so the Lark handler can route the answer back as a
444
- * `tool_result`.
445
- *
446
- * Single-question is the common case; multi-question gets buttons on
447
- * the first question only and a text-only listing for the rest (an
448
- * acceptable limitation — these are rare in practice and we can lift
449
- * it once the UX is validated). */
450
- export function askUserQuestionElement(
451
- i: number,
452
- toolUseId: string,
453
- questions: AskQuestion[],
454
- status: '🤔' | '✅' | '❌' = '🤔',
455
- state?: AskState,
456
- ): object {
457
- const total = questions.length
458
- const answered = state?.answered ?? new Map<number, AskAnswered>()
459
- const currentIdx = state?.currentIdx
460
- const isTerminal = currentIdx === undefined && answered.size > 0
461
- const bodyElements: any[] = []
462
- let headerText: string
463
-
464
- if (isTerminal) {
465
- // All questions resolved — collapse and roll up answers in header
466
- // + body. Single-question case keeps the old "已回答:xxx" header
467
- // style; multi-question gets a "已回答 · N 题" count and a flat
468
- // listing of Q→A pairs in the body.
469
- if (total === 1) {
470
- const q0 = questions[0]
471
- const a0 = answered.get(0)
472
- const value = a0?.customText
473
- ?? (a0?.optionIdx !== undefined ? q0?.options[a0.optionIdx]?.label : undefined)
474
- ?? '?'
475
- const headerTag = q0?.header ? ` · ${q0.header}` : ''
476
- headerText = `${status} 已回答${headerTag}:${value}`
477
- } else {
478
- headerText = `${status} 已回答 · ${total} 题`
479
- }
480
- const sortedIdx = [...answered.keys()].sort((a, b) => a - b)
481
- for (const idx of sortedIdx) {
482
- const q = questions[idx]
483
- const a = answered.get(idx)!
484
- const tag = q?.header ?? `Q${idx + 1}`
485
- const value = a.customText
486
- ?? (a.optionIdx !== undefined ? q?.options[a.optionIdx]?.label : undefined)
487
- ?? '?'
488
- bodyElements.push({
489
- tag: 'markdown',
490
- content: `**${tag}**:${value}`,
491
- })
492
- }
493
- const lastUser = [...answered.values()].reverse().find(a => a.user)?.user
494
- if (lastUser) {
495
- bodyElements.push({
496
- tag: 'markdown',
497
- content: `\n*— 由 ${lastUser} 回答*`,
498
- })
499
- }
500
- } else if (currentIdx !== undefined && questions[currentIdx]) {
501
- // In-progress: render current question + folded history above.
502
- // Progress tag in header lets the user see how many are left,
503
- // even with the history panel folded.
504
- const q = questions[currentIdx]
505
- const headerTag = q.header ? ` · ${q.header}` : ''
506
- const progress = total > 1 ? ` (${currentIdx + 1}/${total})` : ''
507
- headerText = `${status} 🤔 AskUserQuestion${headerTag}${progress}`
508
- const history = renderAskHistoryPanel(questions, answered)
509
- if (history) bodyElements.push(history)
510
- bodyElements.push(...renderAskQuestionBody(q, toolUseId, currentIdx))
511
- bodyElements.push({
512
- tag: 'markdown',
513
- content: '_💬 也可以直接在群里回复你的答案(裸词命令 `hi`/`kill`/`restart`/`clear` 仍然优先)_',
514
- })
515
- } else {
516
- // Defensive fallback — neither answered nor a valid currentIdx.
517
- headerText = `${status} 🤔 AskUserQuestion`
518
- if (questions[0]) {
519
- bodyElements.push({ tag: 'markdown', content: `**${questions[0].question}**` })
520
- }
521
- }
522
-
523
- return {
524
- tag: 'collapsible_panel',
525
- element_id: ELEMENTS.tool(i),
526
- header: { title: { tag: 'plain_text', content: headerText } },
527
- expanded: !isTerminal,
528
- elements: bodyElements,
529
- }
530
- }
package/src/cards.ts DELETED
@@ -1,32 +0,0 @@
1
- /**
2
- * Schema 2.0 Feishu card templates — barrel re-export. Each call site
3
- * uses `import * as cards from './cards'` and reaches everything through
4
- * this file. Internal split so each module stays under Claude Code's
5
- * per-read token budget:
6
- * - cards/elements.ts — ELEMENTS (shared element-id helpers)
7
- * - cards/turn.ts — main turn card, tool panels, ask panels, todos
8
- * - cards/console.ts — console + menu cards, formatters,
9
- * streamingOffSettings
10
- */
11
-
12
- export { ELEMENTS } from './cards/elements'
13
- export {
14
- type Todo,
15
- type AskQuestion,
16
- type AskAnswered,
17
- type AskState,
18
- summarizeToolInput,
19
- mainConversationCard,
20
- assistantSegmentElement,
21
- thinkingCollapsedPanel,
22
- toolCallElement,
23
- readBatchElement,
24
- toolCallPermissionElement,
25
- askUserQuestionElement,
26
- } from './cards/turn'
27
- export {
28
- consoleUsageContent,
29
- consoleCard,
30
- menuCard,
31
- streamingOffSettings,
32
- } from './cards/console'