@leviyuan/lodestar 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leviyuan/lodestar",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/src/cards.ts CHANGED
@@ -337,6 +337,11 @@ export function askUserQuestionElement(
337
337
  toolUseId: string,
338
338
  questions: AskQuestion[],
339
339
  status: '🤔' | '✅' | '❌' = '🤔',
340
+ /** When set, only this option is rendered as "picked" — every other
341
+ * option turns into plain text (no click target). Used after the
342
+ * user has answered so the panel freezes in a sensible terminal
343
+ * state instead of inviting another click. */
344
+ pickedOptionIdx?: number,
340
345
  resolvedNote?: string,
341
346
  ): object {
342
347
  const primary = questions[0]
@@ -345,18 +350,38 @@ export function askUserQuestionElement(
345
350
  const bodyElements: any[] = []
346
351
  if (primary) {
347
352
  bodyElements.push({ tag: 'markdown', content: `**${primary.question}**` })
348
- // Stack option buttons in a column_set one button per option.
349
- // Each carries `kind:'ask'` + the toolUseId + question/option idx
350
- // so the daemon's card action handler can map a click back to
351
- // exactly one (question, choice) pair.
352
- bodyElements.push({
353
- tag: 'column_set',
354
- columns: primary.options.map((opt, optIdx) => ({
355
- tag: 'column', width: 'weighted', weight: 1,
356
- elements: [{
357
- tag: 'button',
358
- text: { tag: 'plain_text', content: opt.label },
359
- type: 'default',
353
+ // One row per option, each a full-width interactive_container so
354
+ // the entire row (label + description) is the click target. Looks
355
+ // cleaner than a row of squat buttons and matches the way IM
356
+ // quick-replies usually present themselves. After the user picks,
357
+ // we still render the same row layout (no JSON dump) but strip
358
+ // the callbacks — selected option marked ✅, others dimmed.
359
+ for (let optIdx = 0; optIdx < primary.options.length; optIdx++) {
360
+ const opt = primary.options[optIdx]
361
+ const isPicked = pickedOptionIdx === optIdx
362
+ const isAnswered = pickedOptionIdx !== undefined
363
+ const labelLine = isPicked
364
+ ? `**✅ ${opt.label}**`
365
+ : isAnswered
366
+ ? `~~${opt.label}~~`
367
+ : `**${opt.label}**`
368
+ const descLine = opt.description ? `\n${opt.description}` : ''
369
+ const rowContent = { tag: 'markdown', content: `${labelLine}${descLine}` }
370
+ if (isAnswered) {
371
+ // Frozen — no behaviors, plain container so it stops looking
372
+ // clickable.
373
+ bodyElements.push({
374
+ tag: 'div',
375
+ elements: [rowContent],
376
+ })
377
+ } else {
378
+ bodyElements.push({
379
+ tag: 'interactive_container',
380
+ background_style: 'default',
381
+ has_border: true,
382
+ corner_radius: '6px',
383
+ padding: '8px 12px',
384
+ margin: '4px 0',
360
385
  behaviors: [{
361
386
  type: 'callback',
362
387
  value: {
@@ -366,15 +391,10 @@ export function askUserQuestionElement(
366
391
  option_idx: optIdx,
367
392
  },
368
393
  }],
369
- }],
370
- })),
371
- })
372
- // Inline option descriptions below the buttons so the user can
373
- // read context without hovering.
374
- const descLines = primary.options
375
- .map((o, idx) => o.description ? `- **${o.label}** — ${o.description}` : `- **${o.label}**`)
376
- .join('\n')
377
- if (descLines) bodyElements.push({ tag: 'markdown', content: descLines })
394
+ elements: [rowContent],
395
+ })
396
+ }
397
+ }
378
398
  }
379
399
  // Secondary questions get text-only treatment (TODO: multi-question
380
400
  // panels when actually requested by a real prompt).
package/src/session.ts CHANGED
@@ -382,8 +382,8 @@ export class Session {
382
382
  if (turn && meta) {
383
383
  meta.output = JSON.stringify({ answers })
384
384
  meta.isError = false
385
- const resolvedNote = `\n\n **已回答** by ${user || '匿名'}: ${opt.label}`
386
- const el = cards.askUserQuestionElement(meta.i, toolUseId, pending.questions, '✅', resolvedNote)
385
+ const resolvedNote = `\n\n*已由 ${user || '匿名'} 回答*`
386
+ const el = cards.askUserQuestionElement(meta.i, toolUseId, pending.questions, '✅', optionIdx, resolvedNote)
387
387
  void cardkit.replaceElement(turn.cardId, cards.ELEMENTS.tool(meta.i), el)
388
388
  }
389
389
 
@@ -606,6 +606,12 @@ export class Session {
606
606
  // can't discard the output after the first paint.
607
607
  meta.output = output
608
608
  meta.isError = isError
609
+ // AskUserQuestion already had its final panel painted by resolveAsk
610
+ // (✅ + the chosen option marked, others dimmed). The tool_result
611
+ // arriving here is just the SDK's synthesised echo — re-rendering
612
+ // via toolCallElement would clobber the nice option-row layout
613
+ // with a generic JSON dump. Bail out; the panel is done.
614
+ if (meta.name === 'AskUserQuestion') return
609
615
  // Update the local todo mirror BEFORE rendering so the just-
610
616
  // completed panel shows the new state too (e.g. a TaskCreate panel
611
617
  // already lists the task it just created).