@mawaru/sdk 0.39.0 → 0.40.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.
Files changed (84) hide show
  1. package/_schemas/graph.test.ts +15 -9
  2. package/_schemas/graph.ts +20 -6
  3. package/_schemas/human-manifest.test.ts +129 -0
  4. package/_schemas/human-manifest.ts +158 -0
  5. package/_schemas/port-spec.test.ts +40 -40
  6. package/_schemas/port-spec.ts +10 -24
  7. package/dist/_cli/login.js +1 -15
  8. package/dist/_cli/openBrowser.d.ts +1 -0
  9. package/dist/_cli/openBrowser.js +15 -0
  10. package/dist/_cli/previewApi.d.ts +1 -1
  11. package/dist/_cli/previewCommand.d.ts +8 -0
  12. package/dist/_cli/previewCommand.js +95 -37
  13. package/dist/_cli/previewFiles.d.ts +2 -1
  14. package/dist/_cli/previewFiles.js +42 -19
  15. package/dist/_cli/previewSlack.d.ts +1 -1
  16. package/dist/_cli/previewSlack.js +1 -1
  17. package/dist/_schemas/graph.d.ts +21 -3
  18. package/dist/_schemas/graph.js +17 -6
  19. package/dist/_schemas/human-manifest.d.ts +27 -0
  20. package/dist/_schemas/human-manifest.js +121 -0
  21. package/dist/_schemas/port-spec.d.ts +6 -9
  22. package/dist/_schemas/port-spec.js +7 -7
  23. package/dist/cli.js +3 -2
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.js +1 -0
  26. package/dist/init.js +22 -3
  27. package/dist/typegen.d.ts +3 -1
  28. package/dist/typegen.js +96 -14
  29. package/dist/validate.js +55 -0
  30. package/dist/views/_approval/ApprovalBar.d.ts +20 -0
  31. package/dist/views/_approval/ApprovalBar.js +74 -0
  32. package/dist/views/_approvalActions.d.ts +18 -0
  33. package/dist/views/_approvalActions.js +10 -0
  34. package/dist/views/_bridge.d.ts +12 -0
  35. package/dist/views/_bridge.js +10 -0
  36. package/dist/views/_parts/DocumentCard.d.ts +2 -0
  37. package/dist/views/_parts/DocumentCard.js +16 -0
  38. package/dist/views/_parts/FilePreview.d.ts +5 -0
  39. package/dist/views/_parts/FilePreview.js +101 -0
  40. package/dist/views/_parts/Linkify.d.ts +7 -0
  41. package/dist/views/_parts/Linkify.js +28 -0
  42. package/dist/views/_parts/Markdown.d.ts +44 -0
  43. package/dist/views/_parts/Markdown.js +313 -0
  44. package/dist/views/_parts/SchemaForm.d.ts +19 -0
  45. package/dist/views/_parts/SchemaForm.js +310 -0
  46. package/dist/views/_parts/fileRef.d.ts +7 -0
  47. package/dist/views/_parts/fileRef.js +11 -0
  48. package/dist/views/_parts/useNarrow.d.ts +5 -0
  49. package/dist/views/_parts/useNarrow.js +23 -0
  50. package/dist/views/host.d.ts +18 -0
  51. package/dist/views/host.js +20 -0
  52. package/dist/views/human.d.ts +73 -0
  53. package/dist/views/human.js +6 -0
  54. package/dist/views/index.d.ts +8 -0
  55. package/dist/views/index.js +12 -0
  56. package/dist/views/protocol.d.ts +34 -0
  57. package/dist/views/protocol.js +5 -0
  58. package/dist/views/runtime.d.ts +3 -0
  59. package/dist/views/runtime.js +86 -0
  60. package/dist/{slack-view.d.ts → views/slack.d.ts} +1 -1
  61. package/docs/development.md +40 -2
  62. package/index.ts +1 -0
  63. package/package.json +36 -4
  64. package/skills/create-loop/SKILL.md +6 -3
  65. package/views/_approval/ApprovalBar.tsx +239 -0
  66. package/views/_approvalActions.tsx +31 -0
  67. package/views/_bridge.tsx +36 -0
  68. package/views/_parts/DocumentCard.ts +18 -0
  69. package/views/_parts/FilePreview.tsx +180 -0
  70. package/views/_parts/Linkify.test.ts +40 -0
  71. package/views/_parts/Linkify.tsx +40 -0
  72. package/views/_parts/Markdown.test.ts +131 -0
  73. package/views/_parts/Markdown.tsx +472 -0
  74. package/views/_parts/SchemaForm.tsx +742 -0
  75. package/views/_parts/fileRef.ts +21 -0
  76. package/views/_parts/useNarrow.ts +24 -0
  77. package/views/host.ts +23 -0
  78. package/views/human.ts +91 -0
  79. package/views/index.ts +16 -0
  80. package/views/protocol.ts +40 -0
  81. package/views/runtime.tsx +132 -0
  82. package/views/slack.test.ts +40 -0
  83. package/{slack-view.ts → views/slack.ts} +3 -3
  84. /package/dist/{slack-view.js → views/slack.js} +0 -0
@@ -0,0 +1,472 @@
1
+ import {
2
+ type CSSProperties,
3
+ type ReactNode,
4
+ useCallback,
5
+ useLayoutEffect,
6
+ useRef,
7
+ } from "react"
8
+ import { linkify } from "./Linkify.js"
9
+
10
+ // AI の下書き・調査レポートは Markdown で届く。生ソースのまま読ませると読み物として
11
+ // 読めないので整形して出す。frontend は依存最小主義なのでレンダラは自前で持ち、
12
+ // 実際に出る記法だけを見る:見出し / 箇条書き・番号付き / 引用 / コードブロック /
13
+ // 水平線 / 表 / 段落と、インラインの強調・コード・リンク(素の URL を含む)。
14
+ // 対応しない記法は素のテキストとしてそのまま出す=壊さない
15
+
16
+ /** 長文(=Markdown として描く)か。入力欄の昇格判定と共通の基準 */
17
+ export const isLongText = (value: string): boolean =>
18
+ value.includes("\n") || value.length > 80
19
+
20
+ const bodyText: CSSProperties = {
21
+ fontSize: "var(--text-base)",
22
+ lineHeight: 1.9,
23
+ margin: 0,
24
+ overflowWrap: "anywhere",
25
+ }
26
+
27
+ const codeChip: CSSProperties = {
28
+ fontFamily: "var(--font-mono)",
29
+ fontSize: "0.9em",
30
+ background: "var(--surface-2)",
31
+ border: "var(--border-w) solid var(--border)",
32
+ borderRadius: "var(--radius-s)",
33
+ padding: "1px 5px",
34
+ }
35
+
36
+ // インライン:**太字** / `コード` / [リンク](url)。分割は捕捉グループ付きなので
37
+ // 区切り自身も配列に残る(split の仕様)
38
+ const inlineToken = /(`[^`\n]+`|\*\*[^*\n]+\*\*|\[[^\]\n]+\]\([^)\s]+\))/g
39
+
40
+ const renderInline = (text: string, prefix = ""): ReactNode[] =>
41
+ text.split(inlineToken).map((token, i) => {
42
+ if (!token) return null
43
+ const key = `${prefix}${i}`
44
+ if (token.startsWith("`") && token.endsWith("`") && token.length > 2) {
45
+ return (
46
+ <code key={key} style={codeChip}>
47
+ {token.slice(1, -1)}
48
+ </code>
49
+ )
50
+ }
51
+ if (token.startsWith("**") && token.endsWith("**") && token.length > 4) {
52
+ return (
53
+ <strong
54
+ key={key}
55
+ style={{
56
+ fontWeight: "var(--weight-bold)" as CSSProperties["fontWeight"],
57
+ }}
58
+ >
59
+ {token.slice(2, -2)}
60
+ </strong>
61
+ )
62
+ }
63
+ const link = /^\[([^\]]+)\]\(([^)\s]+)\)$/.exec(token)
64
+ if (link) {
65
+ return (
66
+ <a key={key} href={link[2]} target="_blank" rel="noreferrer">
67
+ {link[1]}
68
+ </a>
69
+ )
70
+ }
71
+ // 素の URL もリンクにする
72
+ return <span key={key}>{linkify(token, `${key}-`)}</span>
73
+ })
74
+
75
+ // 段落内の改行は改行として残す(日本語の下書きは1行1文で書かれることが多い)
76
+ const renderLines = (lines: string[]): ReactNode[] =>
77
+ lines.flatMap((line, i) =>
78
+ i === 0
79
+ ? renderInline(line, "0-")
80
+ : [
81
+ // biome-ignore lint/suspicious/noArrayIndexKey: 表示専用(テキスト全体が置き換わる)
82
+ <br key={`br${i}`} />,
83
+ ...renderInline(line, `${i}-`),
84
+ ],
85
+ )
86
+
87
+ type ListItem = { text: string; nested: boolean }
88
+
89
+ export type TableAlign = "left" | "center" | "right" | null
90
+
91
+ export type Block =
92
+ | { kind: "heading"; level: number; text: string }
93
+ | { kind: "list"; ordered: boolean; items: ListItem[] }
94
+ | { kind: "quote"; lines: string[] }
95
+ | { kind: "code"; lines: string[] }
96
+ | { kind: "rule" }
97
+ | { kind: "table"; align: TableAlign[]; header: string[]; rows: string[][] }
98
+ | { kind: "para"; lines: string[] }
99
+
100
+ const headingOf = /^(#{1,6})\s+(.*)$/
101
+ const bulletOf = /^(\s*)[-*+]\s+(.*)$/
102
+ const orderedOf = /^(\s*)\d+[.)]\s+(.*)$/
103
+ const quoteOf = /^>\s?(.*)$/
104
+ const ruleOf = /^(-{3,}|\*{3,}|_{3,})\s*$/
105
+
106
+ // 表(GFM):`| a | b |` の行の直後に区切り行(`|---|:-:|--:|`)があれば表。
107
+ // 外側の | は無くてもよい。セルの分割は `\|` でエスケープした | を除く
108
+ const splitCells = (line: string): string[] => {
109
+ let body = line.trim()
110
+ if (body.startsWith("|")) body = body.slice(1)
111
+ if (body.endsWith("|") && !body.endsWith("\\|")) body = body.slice(0, -1)
112
+ return body
113
+ .split(/(?<!\\)\|/)
114
+ .map((cell) => cell.trim().replace(/\\\|/g, "|"))
115
+ }
116
+
117
+ const delimiterCellOf = /^:?-+:?$/
118
+
119
+ const alignOf = (cell: string): TableAlign =>
120
+ cell.startsWith(":") && cell.endsWith(":")
121
+ ? "center"
122
+ : cell.endsWith(":")
123
+ ? "right"
124
+ : cell.startsWith(":")
125
+ ? "left"
126
+ : null
127
+
128
+ const isTableStart = (lines: string[], i: number): boolean => {
129
+ const head = lines[i] ?? ""
130
+ const delimiter = lines[i + 1] ?? ""
131
+ if (!head.includes("|") || !delimiter.includes("-")) return false
132
+ const cells = splitCells(delimiter)
133
+ return (
134
+ cells.length > 0 &&
135
+ cells.every((cell) => delimiterCellOf.test(cell)) &&
136
+ cells.length === splitCells(head).length
137
+ )
138
+ }
139
+
140
+ // 表の本体行は空行か別ブロックの始まりまで(| を含まない行も1セルの行として受ける)
141
+ const isBlockStart = (line: string): boolean =>
142
+ headingOf.test(line) ||
143
+ bulletOf.test(line) ||
144
+ orderedOf.test(line) ||
145
+ quoteOf.test(line) ||
146
+ ruleOf.test(line.trim()) ||
147
+ line.trimStart().startsWith("```")
148
+
149
+ /** 行を読んでブロックに割る(描画から切り離してテストできるよう公開している) */
150
+ export const parseBlocks = (source: string): Block[] => {
151
+ const lines = source.replace(/\r\n?/g, "\n").split("\n")
152
+ const blocks: Block[] = []
153
+ let i = 0
154
+
155
+ while (i < lines.length) {
156
+ const line = lines[i] ?? ""
157
+
158
+ if (line.trim() === "") {
159
+ i += 1
160
+ continue
161
+ }
162
+
163
+ // コードブロック(閉じないまま終わっても、そこまでをコードとして出す)
164
+ if (line.trimStart().startsWith("```")) {
165
+ const body: string[] = []
166
+ i += 1
167
+ while (
168
+ i < lines.length &&
169
+ !(lines[i] ?? "").trimStart().startsWith("```")
170
+ ) {
171
+ body.push(lines[i] ?? "")
172
+ i += 1
173
+ }
174
+ // 閉じの ``` を飛ばす(閉じないまま終端に着いたときは何も飛ばさない)
175
+ i += 1
176
+ blocks.push({ kind: "code", lines: body })
177
+ continue
178
+ }
179
+
180
+ if (isTableStart(lines, i)) {
181
+ const header = splitCells(line)
182
+ const align = splitCells(lines[i + 1] ?? "").map(alignOf)
183
+ const rows: string[][] = []
184
+ i += 2
185
+ while (i < lines.length) {
186
+ const current = lines[i] ?? ""
187
+ if (current.trim() === "" || isBlockStart(current)) break
188
+ const cells = splitCells(current)
189
+ // 列数は見出しに揃える(足りなければ空セル、余れば捨てる)
190
+ rows.push(header.map((_, col) => cells[col] ?? ""))
191
+ i += 1
192
+ }
193
+ blocks.push({ kind: "table", align, header, rows })
194
+ continue
195
+ }
196
+
197
+ if (ruleOf.test(line.trim())) {
198
+ blocks.push({ kind: "rule" })
199
+ i += 1
200
+ continue
201
+ }
202
+
203
+ const heading = headingOf.exec(line)
204
+ if (heading) {
205
+ blocks.push({
206
+ kind: "heading",
207
+ level: (heading[1] ?? "#").length,
208
+ text: heading[2] ?? "",
209
+ })
210
+ i += 1
211
+ continue
212
+ }
213
+
214
+ const quote = quoteOf.exec(line)
215
+ if (quote) {
216
+ const body: string[] = []
217
+ while (i < lines.length) {
218
+ const next = quoteOf.exec(lines[i] ?? "")
219
+ if (!next) break
220
+ body.push(next[1] ?? "")
221
+ i += 1
222
+ }
223
+ blocks.push({ kind: "quote", lines: body })
224
+ continue
225
+ }
226
+
227
+ const bullet = bulletOf.exec(line)
228
+ const ordered = orderedOf.exec(line)
229
+ if (bullet || ordered) {
230
+ const isOrdered = ordered !== null && bullet === null
231
+ const items: ListItem[] = []
232
+ while (i < lines.length) {
233
+ const current = lines[i] ?? ""
234
+ const next = isOrdered
235
+ ? orderedOf.exec(current)
236
+ : bulletOf.exec(current)
237
+ if (!next) break
238
+ items.push({
239
+ text: next[2] ?? "",
240
+ // ネストは1段だけ表現する(2段以上は同じ深さに畳む)
241
+ nested: (next[1] ?? "").length >= 2,
242
+ })
243
+ i += 1
244
+ }
245
+ blocks.push({ kind: "list", ordered: isOrdered, items })
246
+ continue
247
+ }
248
+
249
+ // 段落:空行 or 他のブロックの始まりまで
250
+ const body: string[] = []
251
+ while (i < lines.length) {
252
+ const current = lines[i] ?? ""
253
+ if (
254
+ current.trim() === "" ||
255
+ isBlockStart(current) ||
256
+ isTableStart(lines, i)
257
+ ) {
258
+ break
259
+ }
260
+ body.push(current)
261
+ i += 1
262
+ }
263
+ blocks.push({ kind: "para", lines: body })
264
+ }
265
+
266
+ return blocks
267
+ }
268
+
269
+ const headingStyle = (level: number): CSSProperties => ({
270
+ margin: 0,
271
+ fontSize:
272
+ level <= 1
273
+ ? "var(--text-lg)"
274
+ : level === 2
275
+ ? "var(--text-md)"
276
+ : "var(--text-base)",
277
+ fontWeight: "var(--weight-bold)" as CSSProperties["fontWeight"],
278
+ lineHeight: "var(--leading-tight)",
279
+ ...(level <= 2
280
+ ? {
281
+ borderBottom: "var(--border-w) solid var(--border)",
282
+ paddingBottom: "var(--space-2)",
283
+ }
284
+ : { color: "var(--ink)" }),
285
+ })
286
+
287
+ const tableCell: CSSProperties = {
288
+ padding: "var(--space-1) var(--space-3)",
289
+ borderBottom: "var(--border-w) solid var(--border)",
290
+ verticalAlign: "top",
291
+ overflowWrap: "anywhere",
292
+ }
293
+
294
+ const Blocks = ({ blocks }: { blocks: Block[] }) => (
295
+ <>
296
+ {blocks.map((block, i) => {
297
+ // ブロックは位置で安定(テキスト全体が置き換わる表示専用リスト)
298
+ const key = `${block.kind}${i}`
299
+ switch (block.kind) {
300
+ case "heading": {
301
+ // 見出しレベルは Markdown の意味をそのまま持たせる(h1..h6)
302
+ const Tag = `h${Math.min(6, block.level)}` as "h1"
303
+ return (
304
+ <Tag key={key} style={headingStyle(block.level)}>
305
+ {renderInline(block.text)}
306
+ </Tag>
307
+ )
308
+ }
309
+ case "list": {
310
+ const Tag = block.ordered ? "ol" : "ul"
311
+ return (
312
+ <Tag
313
+ key={key}
314
+ style={{
315
+ ...bodyText,
316
+ paddingLeft: "var(--space-5)",
317
+ display: "grid",
318
+ gap: "var(--space-1)",
319
+ }}
320
+ >
321
+ {block.items.map((item, j) => (
322
+ <li
323
+ // biome-ignore lint/suspicious/noArrayIndexKey: 表示専用
324
+ key={j}
325
+ style={item.nested ? { marginLeft: "var(--space-5)" } : {}}
326
+ >
327
+ {renderInline(item.text)}
328
+ </li>
329
+ ))}
330
+ </Tag>
331
+ )
332
+ }
333
+ case "quote":
334
+ return (
335
+ <blockquote
336
+ key={key}
337
+ style={{
338
+ ...bodyText,
339
+ margin: 0,
340
+ paddingLeft: "var(--space-4)",
341
+ borderLeft: "var(--border-w-strong) solid var(--border-strong)",
342
+ color: "var(--muted)",
343
+ }}
344
+ >
345
+ {renderLines(block.lines)}
346
+ </blockquote>
347
+ )
348
+ case "code":
349
+ return (
350
+ <pre
351
+ key={key}
352
+ style={{
353
+ margin: 0,
354
+ padding: "var(--space-3) var(--space-4)",
355
+ background: "var(--surface-2)",
356
+ border: "var(--border-w) solid var(--border)",
357
+ borderRadius: "var(--radius-s)",
358
+ fontSize: "var(--text-sm)",
359
+ lineHeight: 1.7,
360
+ overflowX: "auto",
361
+ }}
362
+ >
363
+ {block.lines.join("\n")}
364
+ </pre>
365
+ )
366
+ case "rule":
367
+ return (
368
+ <hr
369
+ key={key}
370
+ style={{
371
+ margin: 0,
372
+ border: "none",
373
+ borderTop: "var(--border-w) solid var(--border)",
374
+ }}
375
+ />
376
+ )
377
+ case "table":
378
+ return (
379
+ <div key={key} style={{ overflowX: "auto" }}>
380
+ <table
381
+ style={{
382
+ borderCollapse: "collapse",
383
+ fontSize: "var(--text-sm)",
384
+ lineHeight: 1.6,
385
+ minWidth: "100%",
386
+ }}
387
+ >
388
+ <thead>
389
+ <tr>
390
+ {block.header.map((cell, col) => (
391
+ <th
392
+ // biome-ignore lint/suspicious/noArrayIndexKey: 表示専用
393
+ key={col}
394
+ style={{
395
+ ...tableCell,
396
+ textAlign: block.align[col] ?? "left",
397
+ fontWeight:
398
+ "var(--weight-bold)" as CSSProperties["fontWeight"],
399
+ color: "var(--muted)",
400
+ background: "var(--surface-2)",
401
+ whiteSpace: "nowrap",
402
+ }}
403
+ >
404
+ {renderInline(cell)}
405
+ </th>
406
+ ))}
407
+ </tr>
408
+ </thead>
409
+ <tbody>
410
+ {block.rows.map((row, r) => (
411
+ // biome-ignore lint/suspicious/noArrayIndexKey: 表示専用
412
+ <tr key={r}>
413
+ {row.map((cell, col) => (
414
+ <td
415
+ // biome-ignore lint/suspicious/noArrayIndexKey: 表示専用
416
+ key={col}
417
+ style={{
418
+ ...tableCell,
419
+ textAlign: block.align[col] ?? "left",
420
+ }}
421
+ >
422
+ {renderInline(cell)}
423
+ </td>
424
+ ))}
425
+ </tr>
426
+ ))}
427
+ </tbody>
428
+ </table>
429
+ </div>
430
+ )
431
+ default:
432
+ return (
433
+ <p key={key} style={bodyText}>
434
+ {renderLines(block.lines)}
435
+ </p>
436
+ )
437
+ }
438
+ })}
439
+ </>
440
+ )
441
+
442
+ export const Markdown = ({ text }: { text: string }) => (
443
+ <div style={{ display: "grid", gap: "var(--space-4)" }}>
444
+ <Blocks blocks={parseBlocks(text)} />
445
+ </div>
446
+ )
447
+
448
+ // 一度縮めてから測らないと、削ったときに高さが戻らない
449
+ const fitToContent = (el: HTMLTextAreaElement | null) => {
450
+ if (!el) return
451
+ el.style.height = "auto"
452
+ el.style.height = `${el.scrollHeight}px`
453
+ }
454
+
455
+ /**
456
+ * 内容の高さまで伸びる textarea(上限を超えたらその中でスクロール)。
457
+ * 返り値は ref にそのまま渡す。「編集」を押して後から現れる欄でも、
458
+ * 付いた瞬間に測るので初回から正しい高さで出る
459
+ */
460
+ export const useAutoGrow = (
461
+ value: string,
462
+ ): ((el: HTMLTextAreaElement | null) => void) => {
463
+ const ref = useRef<HTMLTextAreaElement | null>(null)
464
+ // biome-ignore lint/correctness/useExhaustiveDependencies: value は測り直しの引き金(本文では読まない)
465
+ useLayoutEffect(() => {
466
+ fitToContent(ref.current)
467
+ }, [value])
468
+ return useCallback((el: HTMLTextAreaElement | null) => {
469
+ ref.current = el
470
+ fitToContent(el)
471
+ }, [])
472
+ }