toolcraft-openapi 0.0.23 → 0.0.25

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.
Files changed (63) hide show
  1. package/README.md +2 -3
  2. package/dist/auth/bearer-token-auth.js +12 -3
  3. package/dist/auth/types.d.ts +1 -1
  4. package/dist/bin/generate.d.ts +5 -4
  5. package/dist/bin/generate.js +57 -23
  6. package/dist/generate.js +6 -2
  7. package/dist/http.js +29 -17
  8. package/dist/interpreter.js +12 -3
  9. package/dist/mock/fetch.js +22 -5
  10. package/dist/network-error.js +5 -3
  11. package/dist/redaction.d.ts +3 -0
  12. package/dist/redaction.js +38 -0
  13. package/node_modules/@poe-code/design-system/dist/acp/components.js +3 -1
  14. package/node_modules/@poe-code/design-system/dist/components/browser.d.ts +1 -1
  15. package/node_modules/@poe-code/design-system/dist/components/browser.js +6 -1
  16. package/node_modules/@poe-code/design-system/dist/components/color.js +9 -8
  17. package/node_modules/@poe-code/design-system/dist/components/command-errors.js +3 -2
  18. package/node_modules/@poe-code/design-system/dist/components/detail-card.d.ts +22 -0
  19. package/node_modules/@poe-code/design-system/dist/components/detail-card.js +69 -0
  20. package/node_modules/@poe-code/design-system/dist/components/help-formatter.js +88 -11
  21. package/node_modules/@poe-code/design-system/dist/components/index.d.ts +1 -1
  22. package/node_modules/@poe-code/design-system/dist/components/index.js +1 -1
  23. package/node_modules/@poe-code/design-system/dist/components/table.d.ts +2 -0
  24. package/node_modules/@poe-code/design-system/dist/components/table.js +82 -5
  25. package/node_modules/@poe-code/design-system/dist/components/template.d.ts +4 -0
  26. package/node_modules/@poe-code/design-system/dist/components/template.js +198 -32
  27. package/node_modules/@poe-code/design-system/dist/components/text.js +29 -5
  28. package/node_modules/@poe-code/design-system/dist/dashboard/ansi.d.ts +2 -2
  29. package/node_modules/@poe-code/design-system/dist/dashboard/ansi.js +77 -32
  30. package/node_modules/@poe-code/design-system/dist/dashboard/buffer.js +28 -5
  31. package/node_modules/@poe-code/design-system/dist/dashboard/components/output-pane.js +45 -28
  32. package/node_modules/@poe-code/design-system/dist/dashboard/terminal-width.d.ts +4 -0
  33. package/node_modules/@poe-code/design-system/dist/dashboard/terminal-width.js +71 -0
  34. package/node_modules/@poe-code/design-system/dist/dashboard/types.d.ts +1 -0
  35. package/node_modules/@poe-code/design-system/dist/explorer/events.d.ts +6 -0
  36. package/node_modules/@poe-code/design-system/dist/explorer/reducer.js +32 -10
  37. package/node_modules/@poe-code/design-system/dist/explorer/render/detail.js +3 -0
  38. package/node_modules/@poe-code/design-system/dist/explorer/runtime.js +57 -6
  39. package/node_modules/@poe-code/design-system/dist/explorer/state.d.ts +1 -0
  40. package/node_modules/@poe-code/design-system/dist/explorer/state.js +12 -15
  41. package/node_modules/@poe-code/design-system/dist/index.d.ts +3 -1
  42. package/node_modules/@poe-code/design-system/dist/index.js +2 -1
  43. package/node_modules/@poe-code/design-system/dist/prompts/primitives/intro.js +2 -1
  44. package/node_modules/@poe-code/design-system/dist/prompts/primitives/log.js +8 -5
  45. package/node_modules/@poe-code/design-system/dist/prompts/primitives/note.js +1 -1
  46. package/node_modules/@poe-code/design-system/dist/static/menu.js +8 -2
  47. package/node_modules/@poe-code/design-system/dist/static/spinner.js +10 -4
  48. package/node_modules/@poe-code/design-system/dist/terminal-markdown/parser/frontmatter.js +9 -2
  49. package/node_modules/@poe-code/design-system/dist/terminal-markdown/renderer.js +19 -2
  50. package/node_modules/@poe-code/design-system/package.json +2 -1
  51. package/node_modules/auth-store/dist/create-secret-store.js +4 -1
  52. package/node_modules/auth-store/dist/encrypted-file-store.d.ts +8 -0
  53. package/node_modules/auth-store/dist/encrypted-file-store.js +104 -8
  54. package/node_modules/auth-store/dist/index.d.ts +1 -1
  55. package/node_modules/auth-store/dist/keychain-store.d.ts +4 -1
  56. package/node_modules/auth-store/dist/keychain-store.js +18 -16
  57. package/node_modules/auth-store/dist/provider-store.d.ts +5 -1
  58. package/node_modules/auth-store/dist/provider-store.js +55 -7
  59. package/node_modules/auth-store/dist/types.d.ts +3 -1
  60. package/node_modules/auth-store/package.json +2 -1
  61. package/package.json +3 -3
  62. package/dist/lock.d.ts +0 -14
  63. package/dist/lock.js +0 -152
@@ -1,4 +1,5 @@
1
1
  import { color } from "../components/color.js";
2
+ import { expandTabs, graphemes, graphemeWidth } from "./terminal-width.js";
2
3
  const EMPTY_CELL = { ch: " ", style: {} };
3
4
  export class ScreenBuffer {
4
5
  _width;
@@ -21,13 +22,19 @@ export class ScreenBuffer {
21
22
  }
22
23
  const normalizedStyle = normalizeStyle(style);
23
24
  let offset = 0;
24
- for (const ch of text) {
25
+ for (const ch of graphemes(expandTabs(text, Math.max(0, x)))) {
25
26
  const targetX = x + offset;
26
- offset += 1;
27
+ const width = graphemeWidth(ch);
28
+ offset += width;
27
29
  if (!this.isInBoundsX(targetX)) {
28
30
  continue;
29
31
  }
30
32
  this._cells[this.index(targetX, y)] = { ch, style: normalizedStyle };
33
+ for (let continuation = 1; continuation < width; continuation += 1) {
34
+ if (this.isInBoundsX(targetX + continuation)) {
35
+ this._cells[this.index(targetX + continuation, y)] = { ch: "", style: normalizedStyle };
36
+ }
37
+ }
31
38
  }
32
39
  }
33
40
  get(x, y) {
@@ -77,16 +84,22 @@ export class ScreenBuffer {
77
84
  const normalizedStyle = normalizeStyle(style);
78
85
  const rectEndX = rect.x + rect.width;
79
86
  let offset = 0;
80
- for (const ch of text) {
87
+ for (const ch of graphemes(expandTabs(text))) {
81
88
  const targetX = rect.x + offset;
82
- offset += 1;
83
- if (targetX >= rectEndX) {
89
+ const width = graphemeWidth(ch);
90
+ offset += width;
91
+ if (targetX + width > rectEndX) {
84
92
  break;
85
93
  }
86
94
  if (!this.isInBoundsX(targetX)) {
87
95
  continue;
88
96
  }
89
97
  this._cells[this.index(targetX, y)] = { ch, style: normalizedStyle };
98
+ for (let continuation = 1; continuation < width; continuation += 1) {
99
+ if (this.isInBoundsX(targetX + continuation)) {
100
+ this._cells[this.index(targetX + continuation, y)] = { ch: "", style: normalizedStyle };
101
+ }
102
+ }
90
103
  }
91
104
  }
92
105
  index(x, y) {
@@ -118,6 +131,9 @@ export function diff(prev, next) {
118
131
  return changes;
119
132
  }
120
133
  export function cellToAnsi(cell) {
134
+ if (cell.ch.length === 0) {
135
+ return "";
136
+ }
121
137
  const style = cell.style ?? {};
122
138
  let painter = color;
123
139
  if (style.bold) {
@@ -126,6 +142,9 @@ export function cellToAnsi(cell) {
126
142
  if (style.dim) {
127
143
  painter = painter.dim;
128
144
  }
145
+ if (style.inverse) {
146
+ painter = painter.inverse;
147
+ }
129
148
  if (style.underline) {
130
149
  painter = painter.underline;
131
150
  }
@@ -161,6 +180,9 @@ function normalizeStyle(style) {
161
180
  if (style?.dim !== undefined) {
162
181
  next.dim = style.dim;
163
182
  }
183
+ if (style?.inverse !== undefined) {
184
+ next.inverse = style.inverse;
185
+ }
164
186
  if (style?.underline !== undefined) {
165
187
  next.underline = style.underline;
166
188
  }
@@ -175,6 +197,7 @@ function cellsEqual(left, right) {
175
197
  && left.style.bg === right.style.bg
176
198
  && left.style.bold === right.style.bold
177
199
  && left.style.dim === right.style.dim
200
+ && left.style.inverse === right.style.inverse
178
201
  && left.style.underline === right.style.underline;
179
202
  }
180
203
  function applyForegroundColor(instance, ansiColor) {
@@ -1,5 +1,6 @@
1
1
  import { resolveThemeName } from "../../internal/theme-detect.js";
2
2
  import { hasAnsi, parseAnsi } from "../ansi.js";
3
+ import { displayWidth, expandTabs, graphemes, graphemeWidth } from "../terminal-width.js";
3
4
  const TEXT_OFFSET = 3;
4
5
  const CONTINUATION_PREFIX = "│";
5
6
  export function renderOutputPane(buffer, rect, items) {
@@ -37,7 +38,7 @@ export function renderOutputPane(buffer, rect, items) {
37
38
  width: remaining,
38
39
  height: textRect.height
39
40
  }, row, segment.text, segment.style);
40
- offsetX += countCells(segment.text);
41
+ offsetX += displayWidth(segment.text, offsetX);
41
42
  }
42
43
  continue;
43
44
  }
@@ -54,8 +55,8 @@ export function computeVisualLines(items, width) {
54
55
  const visualLines = [];
55
56
  for (const item of items) {
56
57
  const itemStyle = getItemStyle(item.kind, themeName);
57
- if (hasAnsi(item.text)) {
58
- const styledLines = parseAnsi(item.text, {});
58
+ if (hasAnsi(item.text) || hasCursorControls(item.text)) {
59
+ const styledLines = parseAnsi(item.text, hasAnsi(item.text) ? {} : itemStyle);
59
60
  let firstRow = true;
60
61
  for (const styledLine of styledLines) {
61
62
  const rows = hardWrapSegments(styledLine.segments, textWidth);
@@ -84,6 +85,9 @@ export function computeVisualLines(items, width) {
84
85
  }
85
86
  return visualLines;
86
87
  }
88
+ function hasCursorControls(text) {
89
+ return text.includes("\r") || text.includes("\b");
90
+ }
87
91
  function hardWrapSegments(segments, width) {
88
92
  if (width <= 0) {
89
93
  return [[]];
@@ -91,33 +95,34 @@ function hardWrapSegments(segments, width) {
91
95
  const rows = [[]];
92
96
  let rowWidth = 0;
93
97
  for (const segment of segments) {
94
- if (segment.text.length === 0) {
95
- continue;
96
- }
97
- const chars = [...segment.text];
98
- let cursor = 0;
99
- while (cursor < chars.length) {
100
- const space = width - rowWidth;
101
- if (space <= 0) {
102
- rows.push([]);
103
- rowWidth = 0;
104
- continue;
105
- }
106
- const take = chars.slice(cursor, cursor + space).join("");
107
- const currentRow = rows[rows.length - 1];
108
- currentRow.push({ text: take, style: { ...segment.style } });
109
- rowWidth += Math.min(space, chars.length - cursor);
110
- cursor += space;
111
- if (cursor < chars.length) {
98
+ for (const grapheme of graphemes(expandTabs(segment.text, rowWidth))) {
99
+ const graphemeCells = graphemeWidth(grapheme);
100
+ if (rowWidth > 0 && rowWidth + graphemeCells > width) {
112
101
  rows.push([]);
113
102
  rowWidth = 0;
114
103
  }
104
+ appendSegment(rows[rows.length - 1], grapheme, segment.style);
105
+ rowWidth += graphemeCells;
115
106
  }
116
107
  }
117
108
  return rows;
118
109
  }
119
- function countCells(text) {
120
- return Array.from(text).length;
110
+ function appendSegment(segments, text, style) {
111
+ const last = segments[segments.length - 1];
112
+ if (last && stylesEqual(last.style, style)) {
113
+ last.text += text;
114
+ }
115
+ else {
116
+ segments.push({ text, style: { ...style } });
117
+ }
118
+ }
119
+ function stylesEqual(left, right) {
120
+ return left.fg === right.fg
121
+ && left.bg === right.bg
122
+ && left.bold === right.bold
123
+ && left.dim === right.dim
124
+ && left.inverse === right.inverse
125
+ && left.underline === right.underline;
121
126
  }
122
127
  function getPrefix(kind) {
123
128
  if (kind === "success") {
@@ -160,7 +165,7 @@ function wrapText(value, width) {
160
165
  if (width <= 0) {
161
166
  return logicalLines.map(() => "");
162
167
  }
163
- return logicalLines.flatMap((line) => wrapParagraph(line, width));
168
+ return logicalLines.flatMap((line) => wrapParagraph(expandTabs(line), width));
164
169
  }
165
170
  function wrapParagraph(value, width) {
166
171
  if (value.length === 0) {
@@ -186,7 +191,7 @@ function wrapParagraph(value, width) {
186
191
  for (let index = 0; index < chunks.length; index += 1) {
187
192
  const chunk = chunks[index] ?? "";
188
193
  const gap = index === 0 ? pendingSpace : "";
189
- if (currentLine.length > 0 && currentLine.length + gap.length + chunk.length > width) {
194
+ if (currentLine.length > 0 && displayWidth(`${currentLine}${gap}${chunk}`) > width) {
190
195
  flushLine();
191
196
  }
192
197
  if (currentLine.length > 0 && gap.length > 0) {
@@ -205,12 +210,24 @@ function wrapParagraph(value, width) {
205
210
  return lines;
206
211
  }
207
212
  function splitWord(value, width) {
208
- if (value.length <= width) {
213
+ if (displayWidth(value) <= width) {
209
214
  return [value];
210
215
  }
211
216
  const chunks = [];
212
- for (let index = 0; index < value.length; index += width) {
213
- chunks.push(value.slice(index, index + width));
217
+ let chunk = "";
218
+ let chunkWidth = 0;
219
+ for (const grapheme of graphemes(value)) {
220
+ const graphemeCells = graphemeWidth(grapheme);
221
+ if (chunk.length > 0 && chunkWidth + graphemeCells > width) {
222
+ chunks.push(chunk);
223
+ chunk = "";
224
+ chunkWidth = 0;
225
+ }
226
+ chunk += grapheme;
227
+ chunkWidth += graphemeCells;
228
+ }
229
+ if (chunk.length > 0) {
230
+ chunks.push(chunk);
214
231
  }
215
232
  return chunks;
216
233
  }
@@ -0,0 +1,4 @@
1
+ export declare function graphemes(value: string): string[];
2
+ export declare function displayWidth(value: string, startColumn?: number): number;
3
+ export declare function expandTabs(value: string, startColumn?: number): string;
4
+ export declare function graphemeWidth(segment: string): number;
@@ -0,0 +1,71 @@
1
+ const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
2
+ export function graphemes(value) {
3
+ return Array.from(graphemeSegmenter.segment(value), ({ segment }) => segment);
4
+ }
5
+ export function displayWidth(value, startColumn = 0) {
6
+ let column = startColumn;
7
+ for (const segment of graphemes(value)) {
8
+ if (segment === "\t") {
9
+ column += 8 - (column % 8);
10
+ continue;
11
+ }
12
+ column += graphemeWidth(segment);
13
+ }
14
+ return column - startColumn;
15
+ }
16
+ export function expandTabs(value, startColumn = 0) {
17
+ let column = startColumn;
18
+ let expanded = "";
19
+ for (const segment of graphemes(value)) {
20
+ if (segment === "\t") {
21
+ const spaces = 8 - (column % 8);
22
+ expanded += " ".repeat(spaces);
23
+ column += spaces;
24
+ continue;
25
+ }
26
+ expanded += segment;
27
+ column += graphemeWidth(segment);
28
+ }
29
+ return expanded;
30
+ }
31
+ export function graphemeWidth(segment) {
32
+ const codePoint = segment.codePointAt(0);
33
+ if (codePoint === undefined || isZeroWidthCodePoint(codePoint)) {
34
+ return 0;
35
+ }
36
+ return isWideCodePoint(codePoint) || isFlagSegment(segment) ? 2 : 1;
37
+ }
38
+ function isZeroWidthCodePoint(codePoint) {
39
+ return (codePoint >= 0x0300 && codePoint <= 0x036f)
40
+ || (codePoint >= 0x1ab0 && codePoint <= 0x1aff)
41
+ || (codePoint >= 0x1dc0 && codePoint <= 0x1dff)
42
+ || (codePoint >= 0x20d0 && codePoint <= 0x20ff)
43
+ || (codePoint >= 0xfe20 && codePoint <= 0xfe2f);
44
+ }
45
+ function isFlagSegment(segment) {
46
+ const codePoints = [...segment].map((character) => character.codePointAt(0));
47
+ return codePoints.length === 2
48
+ && codePoints.every((codePoint) => codePoint !== undefined && codePoint >= 0x1f1e6 && codePoint <= 0x1f1ff);
49
+ }
50
+ function isWideCodePoint(codePoint) {
51
+ return ((codePoint >= 0x1100 && codePoint <= 0x115f)
52
+ || codePoint === 0x2329
53
+ || codePoint === 0x232a
54
+ || (codePoint >= 0x2e80 && codePoint <= 0x303e)
55
+ || (codePoint >= 0x3041 && codePoint <= 0x33bf)
56
+ || (codePoint >= 0x3400 && codePoint <= 0x4dbf)
57
+ || (codePoint >= 0x4e00 && codePoint <= 0xa4cf)
58
+ || (codePoint >= 0xa960 && codePoint <= 0xa97f)
59
+ || (codePoint >= 0xac00 && codePoint <= 0xd7af)
60
+ || (codePoint >= 0xf900 && codePoint <= 0xfaff)
61
+ || (codePoint >= 0xfe10 && codePoint <= 0xfe19)
62
+ || (codePoint >= 0xfe30 && codePoint <= 0xfe6f)
63
+ || (codePoint >= 0xff00 && codePoint <= 0xff60)
64
+ || (codePoint >= 0xffe0 && codePoint <= 0xffe6)
65
+ || (codePoint >= 0x1b000 && codePoint <= 0x1b0ff)
66
+ || codePoint === 0x1f004
67
+ || codePoint === 0x1f0cf
68
+ || (codePoint >= 0x1f200 && codePoint <= 0x1fffd)
69
+ || (codePoint >= 0x20000 && codePoint <= 0x2fffd)
70
+ || (codePoint >= 0x30000 && codePoint <= 0x3fffd));
71
+ }
@@ -23,6 +23,7 @@ export type CellStyle = {
23
23
  bg?: string;
24
24
  bold?: boolean;
25
25
  dim?: boolean;
26
+ inverse?: boolean;
26
27
  underline?: boolean;
27
28
  };
28
29
  export type Cell = {
@@ -35,6 +35,12 @@ export type ExplorerEvent = {
35
35
  rowId: string;
36
36
  token: number;
37
37
  items: DetailItem[];
38
+ } | {
39
+ type: "detailItemRendered";
40
+ rowId: string;
41
+ token: number;
42
+ itemIndex: number;
43
+ content: string;
38
44
  } | {
39
45
  type: "detailError";
40
46
  rowId: string;
@@ -21,6 +21,8 @@ export function step(state, event, runtimeHandles = DEFAULT_ACTION_HANDLES) {
21
21
  return detailLoading(state, event.rowId, event.token);
22
22
  case "detailLoaded":
23
23
  return detailLoaded(state, event.rowId, event.token, event.items);
24
+ case "detailItemRendered":
25
+ return detailItemRendered(state, event.rowId, event.token, event.itemIndex, event.content);
24
26
  case "detailError":
25
27
  return detailError(state, event.rowId, event.token, event.error);
26
28
  case "actionResolved":
@@ -42,6 +44,9 @@ function stepKey(state, key, runtimeHandles) {
42
44
  return stepFilterKey(state, key, target, runtimeHandles);
43
45
  }
44
46
  if (target?.type === "action") {
47
+ if (state.actionState.get(target.id)?.source === "detail" && state.focused !== "detail") {
48
+ return mark(state, 0);
49
+ }
45
50
  const action = resolveAction(state, key);
46
51
  return action === null ? mark(state, 0) : dispatchAction(state, action, false, runtimeHandles);
47
52
  }
@@ -170,20 +175,27 @@ function resize(state, cols, rows) {
170
175
  };
171
176
  }
172
177
  function rowsLoaded(state, rows) {
178
+ const rowIds = new Set();
179
+ for (const row of rows) {
180
+ if (rowIds.has(row.id)) {
181
+ throw new Error(`Duplicate explorer row id: ${row.id}`);
182
+ }
183
+ rowIds.add(row.id);
184
+ }
173
185
  const matches = filterRows(state.filter, rows);
174
186
  const filtered = matches.map((match) => match.index);
175
187
  const matchPositions = createMatchPositions(matches);
176
188
  const cursor = clamp(state.cursor, 0, Math.max(0, filtered.length - 1));
189
+ const selected = pruneSelection(state.selected, rows);
190
+ const detail = resetDetailForCursor(state, rows, filtered, cursor);
191
+ const modal = modalStillValid(state.modal, rows);
192
+ if (state.modal?.kind === "confirm" && modal === null) {
193
+ state.modal.resolver(false);
194
+ }
195
+ const nextView = { ...state, rows, filtered, matchPositions, cursor, selected, detail, modal };
177
196
  const next = {
178
- ...state,
179
- rows,
180
- filtered,
181
- matchPositions,
182
- cursor,
183
- selected: pruneSelection(state.selected, rows),
184
- detail: resetDetailForCursor(state, rows, filtered, cursor),
185
- modal: modalStillValid(state.modal, rows),
186
- actionState: recomputeActionState({ ...state, rows, filtered, matchPositions, cursor }),
197
+ ...nextView,
198
+ actionState: recomputeActionState(nextView),
187
199
  dirty: REGION_HEADER | REGION_LIST | REGION_DETAIL | REGION_FOOTER | REGION_MODAL
188
200
  };
189
201
  const effect = detailEffect(next);
@@ -222,6 +234,16 @@ function detailLoaded(state, rowId, token, items) {
222
234
  effects: NO_EFFECTS
223
235
  };
224
236
  }
237
+ function detailItemRendered(state, rowId, token, itemIndex, content) {
238
+ if (state.detail.rowId !== rowId || state.detail.token !== token || state.detail.items?.[itemIndex] === undefined) {
239
+ return mark(state, 0);
240
+ }
241
+ const items = state.detail.items.map((item, index) => index === itemIndex ? { ...item, renderedContent: content } : item);
242
+ return {
243
+ state: { ...state, detail: { ...state.detail, items }, dirty: REGION_DETAIL },
244
+ effects: NO_EFFECTS
245
+ };
246
+ }
225
247
  function detailError(state, rowId, token, error) {
226
248
  if (state.detail.rowId !== rowId || state.detail.token !== token) {
227
249
  return mark(state, 0);
@@ -530,7 +552,7 @@ function dispatchPaletteAction(state, runtimeHandles) {
530
552
  }
531
553
  function dispatchPrimary(state, runtimeHandles) {
532
554
  for (const [id, entry] of state.actionState.entries()) {
533
- if (entry.action?.primary === true) {
555
+ if (entry.action?.primary === true && entry.available === true && entry.running !== true) {
534
556
  return dispatchActionById(state, id, false, runtimeHandles);
535
557
  }
536
558
  }
@@ -66,6 +66,9 @@ function renderBlob(screen, rect, text, scroll) {
66
66
  }
67
67
  }
68
68
  function renderItem(item, rect, row) {
69
+ if (item.renderedContent !== undefined) {
70
+ return item.renderedContent;
71
+ }
69
72
  try {
70
73
  const rendered = item.render({
71
74
  width: rect.width,
@@ -25,6 +25,8 @@ class ExplorerRuntime {
25
25
  unsubscribeKeypress;
26
26
  unsubscribeResize;
27
27
  toastTimer;
28
+ rowsRequestToken = 0;
29
+ reorderToken = 0;
28
30
  stopped = false;
29
31
  settle;
30
32
  constructor(config, driver) {
@@ -32,6 +34,10 @@ class ExplorerRuntime {
32
34
  this.driver = driver;
33
35
  this.state = createInitialState(config, driver.getSize());
34
36
  this.detailJobs = createDetailJobs((event) => {
37
+ if (event.type === "detailLoaded") {
38
+ this.loadDetailContent(event.rowId, event.token, event.items);
39
+ return;
40
+ }
35
41
  this.dispatch(event);
36
42
  });
37
43
  this.runtimeHandles = {
@@ -76,13 +82,16 @@ class ExplorerRuntime {
76
82
  this.dispatch({ type: "resize", cols: size.cols, rows: size.rows });
77
83
  });
78
84
  }
79
- async loadRows() {
85
+ async loadRows(requestToken = ++this.rowsRequestToken) {
80
86
  const rows = await this.config.rows();
81
- this.dispatch({ type: "rowsLoaded", rows });
87
+ if (requestToken === this.rowsRequestToken) {
88
+ this.dispatch({ type: "rowsLoaded", rows });
89
+ }
82
90
  }
83
91
  async refreshRowsFromSource() {
92
+ const requestToken = ++this.rowsRequestToken;
84
93
  await this.config.refresh?.();
85
- await this.loadRows();
94
+ await this.loadRows(requestToken);
86
95
  }
87
96
  dispatch(event) {
88
97
  if (this.stopped) {
@@ -101,7 +110,7 @@ class ExplorerRuntime {
101
110
  continue;
102
111
  }
103
112
  if (effect.type === "persistOrder") {
104
- this.track(this.persistOrder(effect.orderedIds, previousState.rows));
113
+ this.track(this.persistOrder(effect.orderedIds, previousState.rows, ++this.reorderToken));
105
114
  continue;
106
115
  }
107
116
  if (effect.type === "suspend") {
@@ -130,7 +139,47 @@ class ExplorerRuntime {
130
139
  signal: new AbortController().signal
131
140
  });
132
141
  }
133
- async persistOrder(orderedIds, previousRows) {
142
+ loadDetailContent(rowId, token, items) {
143
+ const row = this.state.rows.find((candidate) => candidate.id === rowId);
144
+ if (row === undefined) {
145
+ return;
146
+ }
147
+ const layout = computeExplorerLayout({
148
+ cols: this.state.size.cols,
149
+ rows: this.state.size.rows,
150
+ detailHidden: this.state.layout === "narrow-list-only" || this.state.layout === "too-narrow"
151
+ });
152
+ const context = {
153
+ width: layout.detail.width,
154
+ height: layout.detail.height,
155
+ row,
156
+ signal: new AbortController().signal
157
+ };
158
+ const preparedItems = items.map((item, itemIndex) => {
159
+ try {
160
+ const content = item.render(context);
161
+ if (typeof content === "string") {
162
+ return { ...item, renderedContent: content };
163
+ }
164
+ this.track(content.then((resolved) => this.dispatch({ type: "detailItemRendered", rowId, token, itemIndex, content: resolved }), (error) => this.dispatch({
165
+ type: "detailItemRendered",
166
+ rowId,
167
+ token,
168
+ itemIndex,
169
+ content: error instanceof Error ? `Error: ${error.message}` : "Error: detail failed"
170
+ })));
171
+ return { ...item, renderedContent: "Loading detail..." };
172
+ }
173
+ catch (error) {
174
+ return {
175
+ ...item,
176
+ renderedContent: error instanceof Error ? `Error: ${error.message}` : "Error: detail failed"
177
+ };
178
+ }
179
+ });
180
+ this.dispatch({ type: "detailLoaded", rowId, token, items: preparedItems });
181
+ }
182
+ async persistOrder(orderedIds, previousRows, token) {
134
183
  try {
135
184
  await this.config.reorder?.onReorder(orderedIds, {
136
185
  refresh: this.runtimeHandles.refresh,
@@ -139,7 +188,9 @@ class ExplorerRuntime {
139
188
  }
140
189
  catch (error) {
141
190
  this.showToast(error instanceof Error ? error.message : "Could not persist order", "error");
142
- this.dispatch({ type: "rowsLoaded", rows: previousRows });
191
+ if (token === this.reorderToken) {
192
+ this.dispatch({ type: "rowsLoaded", rows: previousRows });
193
+ }
143
194
  }
144
195
  }
145
196
  async runActionEffect(effect) {
@@ -19,6 +19,7 @@ export interface DetailItem {
19
19
  tone?: Tone;
20
20
  };
21
21
  render: (ctx: DetailCtx) => string | Promise<string>;
22
+ renderedContent?: string;
22
23
  }
23
24
  export interface Detail<R> {
24
25
  items: (row: Row, ctx: DetailCtx) => Promise<DetailItem[]>;
@@ -46,21 +46,18 @@ export function createInitialState(config, size) {
46
46
  }
47
47
  function createInitialActionState(config) {
48
48
  const state = new Map();
49
- for (const action of config.actions) {
50
- state.set(action.id, {
51
- available: true,
52
- label: typeof action.label === "function" ? action.id : action.label,
53
- action: action,
54
- source: "row"
55
- });
56
- }
57
- for (const action of config.detail.actions ?? []) {
58
- state.set(action.id, {
59
- available: true,
60
- label: typeof action.label === "function" ? action.id : action.label,
61
- action: action,
62
- source: "detail"
63
- });
49
+ for (const [source, actions] of [["row", config.actions], ["detail", config.detail.actions ?? []]]) {
50
+ for (const action of actions) {
51
+ if (state.has(action.id)) {
52
+ throw new Error(`Duplicate explorer action id: ${action.id}`);
53
+ }
54
+ state.set(action.id, {
55
+ available: true,
56
+ label: typeof action.label === "function" ? action.id : action.label,
57
+ action: action,
58
+ source
59
+ });
60
+ }
64
61
  }
65
62
  return state;
66
63
  }
@@ -17,7 +17,9 @@ export { formatCommandNotFound } from "./components/command-errors.js";
17
17
  export { formatCommandNotFoundPanel } from "./components/command-errors.js";
18
18
  export { renderTable } from "./components/table.js";
19
19
  export type { TableColumn, RenderTableOptions } from "./components/table.js";
20
- export { renderTemplate } from "./components/template.js";
20
+ export { renderDetailCard } from "./components/detail-card.js";
21
+ export type { DetailCardRow, DetailCardSection, RenderDetailCardOptions } from "./components/detail-card.js";
22
+ export { getTemplatePartialNames, renderTemplate, resolveTemplatePartials } from "./components/template.js";
21
23
  export type { RenderTemplateOptions, TemplateEscape } from "./components/template.js";
22
24
  export { openExternal } from "./components/browser.js";
23
25
  export * as acp from "./acp/index.js";
@@ -14,7 +14,8 @@ export * as helpFormatterPlain from "./components/help-formatter-plain.js";
14
14
  export { formatCommandNotFound } from "./components/command-errors.js";
15
15
  export { formatCommandNotFoundPanel } from "./components/command-errors.js";
16
16
  export { renderTable } from "./components/table.js";
17
- export { renderTemplate } from "./components/template.js";
17
+ export { renderDetailCard } from "./components/detail-card.js";
18
+ export { getTemplatePartialNames, renderTemplate, resolveTemplatePartials } from "./components/template.js";
18
19
  export { openExternal } from "./components/browser.js";
19
20
  // ACP rendering
20
21
  export * as acp from "./acp/index.js";
@@ -8,7 +8,8 @@ export function intro(title) {
8
8
  return;
9
9
  }
10
10
  if (format === "markdown") {
11
- process.stdout.write(`# ${stripAnsi(title)}\n\n`);
11
+ const safeTitle = stripAnsi(title).replaceAll("\r\n", " ").replaceAll("\n", " ").replaceAll("\r", " ");
12
+ process.stdout.write(`# ${safeTitle}\n\n`);
12
13
  return;
13
14
  }
14
15
  process.stdout.write(`${color.gray("┌")} ${text.intro(title)}\n`);
@@ -2,6 +2,9 @@ import { color } from "../../components/color.js";
2
2
  import { symbols } from "../../components/symbols.js";
3
3
  import { resolveOutputFormat } from "../../internal/output-format.js";
4
4
  import { stripAnsi } from "../../internal/strip-ansi.js";
5
+ function renderMarkdownInline(value) {
6
+ return stripAnsi(value).replaceAll("\r\n", " ").replaceAll("\n", " ").replaceAll("\r", " ");
7
+ }
5
8
  function writeTerminalMessage(msg, { symbol = color.gray("│"), secondarySymbol = color.gray("│"), spacing = 1, withGuide = true } = {}) {
6
9
  const lines = [];
7
10
  const showGuide = withGuide !== false;
@@ -35,7 +38,7 @@ function writeTerminalMessage(msg, { symbol = color.gray("│"), secondarySymbol
35
38
  export function message(msg, options) {
36
39
  const format = resolveOutputFormat();
37
40
  if (format === "markdown") {
38
- process.stdout.write(`- ${stripAnsi(msg)}\n`);
41
+ process.stdout.write(`- ${renderMarkdownInline(msg)}\n`);
39
42
  return;
40
43
  }
41
44
  if (format === "json") {
@@ -47,7 +50,7 @@ export function message(msg, options) {
47
50
  export function info(msg) {
48
51
  const format = resolveOutputFormat();
49
52
  if (format === "markdown") {
50
- process.stdout.write(`- **info:** ${stripAnsi(msg)}\n`);
53
+ process.stdout.write(`- **info:** ${renderMarkdownInline(msg)}\n`);
51
54
  return;
52
55
  }
53
56
  if (format === "json") {
@@ -59,7 +62,7 @@ export function info(msg) {
59
62
  export function success(msg) {
60
63
  const format = resolveOutputFormat();
61
64
  if (format === "markdown") {
62
- process.stdout.write(`- **success:** ${stripAnsi(msg)}\n`);
65
+ process.stdout.write(`- **success:** ${renderMarkdownInline(msg)}\n`);
63
66
  return;
64
67
  }
65
68
  if (format === "json") {
@@ -71,7 +74,7 @@ export function success(msg) {
71
74
  export function warn(msg) {
72
75
  const format = resolveOutputFormat();
73
76
  if (format === "markdown") {
74
- process.stdout.write(`- **warning:** ${stripAnsi(msg)}\n`);
77
+ process.stdout.write(`- **warning:** ${renderMarkdownInline(msg)}\n`);
75
78
  return;
76
79
  }
77
80
  if (format === "json") {
@@ -83,7 +86,7 @@ export function warn(msg) {
83
86
  export function error(msg) {
84
87
  const format = resolveOutputFormat();
85
88
  if (format === "markdown") {
86
- process.stdout.write(`- **error:** ${stripAnsi(msg)}\n`);
89
+ process.stdout.write(`- **error:** ${renderMarkdownInline(msg)}\n`);
87
90
  return;
88
91
  }
89
92
  if (format === "json") {
@@ -19,7 +19,7 @@ function renderTerminalNote(message, title) {
19
19
  export function note(message, title) {
20
20
  const format = resolveOutputFormat();
21
21
  const strippedMessage = stripAnsi(message);
22
- const strippedTitle = stripAnsi(title ?? "");
22
+ const strippedTitle = stripAnsi(title ?? "").replaceAll("\r\n", " ").replaceAll("\n", " ").replaceAll("\r", " ");
23
23
  if (format === "markdown") {
24
24
  const lines = strippedMessage.split("\n");
25
25
  const heading = strippedTitle ? `> **${strippedTitle}**\n` : "";