@hachej/boring-ui-kit 0.4.32 → 0.4.33
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/dist/task-board.d.ts +10 -3
- package/dist/task-board.js +8 -2
- package/package.json +2 -2
- package/r/agent-target.json +4 -4
- package/r/app-shell.json +10 -10
- package/r/board-viewer.json +6 -6
- package/r/button.json +1 -1
- package/r/dock.json +1 -1
- package/r/documents.json +12 -12
- package/r/feedback-preview.json +1 -1
- package/r/feedback.json +6 -6
- package/r/file-tree.json +4 -4
- package/r/highlights.json +3 -3
- package/r/html-viewer.json +3 -3
- package/r/looks-viewer.json +2 -2
- package/r/maquette-viewer.json +1 -1
- package/r/markdown-editor.json +3 -3
- package/r/mockup-viewer.json +1 -1
- package/r/pi-ambient.json +8 -8
- package/r/pi-app-kinds.json +10 -10
- package/r/pi-app.json +10 -10
- package/r/pi-artifacts.json +2 -2
- package/r/pi-chat-approval.json +2 -2
- package/r/pi-chat-attachments.json +1 -1
- package/r/pi-chat-markdown.json +2 -2
- package/r/pi-chat-mentions.json +1 -1
- package/r/pi-chat-pickers.json +1 -1
- package/r/pi-chat-plugins.json +11 -11
- package/r/pi-chat-question.json +1 -1
- package/r/pi-chat-slash.json +2 -2
- package/r/pi-chat-subagent.json +1 -1
- package/r/pi-chat-voice.json +3 -3
- package/r/pi-chat.json +5 -5
- package/r/pi-workspace.json +3 -3
- package/r/placed-agent.json +4 -4
- package/r/pm-panel.json +12 -12
- package/r/provider-setup.json +1 -1
- package/r/registry.json +180 -180
- package/r/requests-board.json +1 -1
- package/r/shared-chat.json +6 -6
- package/r/task-board.json +3 -3
- package/r/task-list-viewer.json +2 -2
- package/r/task-mentions.json +4 -4
- package/r/version-history.json +2 -2
- package/r/viewer-code.json +2 -2
- package/r/viewer-frame.json +4 -4
- package/r/viewer-html.json +4 -4
- package/r/viewer-image.json +2 -2
- package/r/viewer-markdown.json +5 -5
- package/r/viewer-pdf.json +3 -3
- package/r/viewers.json +6 -6
- package/r/windows.json +1 -1
package/r/pi-chat-voice.json
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
"title": "Pi chat voice messages",
|
|
5
5
|
"description": "Tap-to-record voice messages: a compatibility export of the kit's chat-react/voice recorder hook, the composer's record button and the pending voice bubble, plus useHoldToRecord. Plug it in with `voiceMessage={{ ...config, ...voiceMessagePlugin }}`; without it pi-chat has no voice button.",
|
|
6
6
|
"dependencies": [
|
|
7
|
-
"@hachej/boring-ui-kit@^0.4.
|
|
7
|
+
"@hachej/boring-ui-kit@^0.4.33",
|
|
8
8
|
"react@^19.0.0",
|
|
9
9
|
"lucide-react@^1.51.0"
|
|
10
10
|
],
|
|
11
11
|
"registryDependencies": [
|
|
12
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
13
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
12
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.33/r/utils.json",
|
|
13
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.33/r/pi-chat.json"
|
|
14
14
|
],
|
|
15
15
|
"files": [
|
|
16
16
|
{
|
package/r/pi-chat.json
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
"title": "Pi chat",
|
|
5
5
|
"description": "A complete Tailwind chat over the borrowed native chat controller: composer, messages, one collapsible activity block per turn (tool calls and reasoning), message queue, history and feedback cards for the opt-in feedback tool (lists, and Show offers that the host page resolves). Chat only: no layout. Owned, editable source; no AI SDK. The question, approval, sub-agent and artifact cards are separate items (pi-chat-question, pi-chat-approval, pi-chat-subagent, pi-artifacts); without them a question or an approval shows a \"waiting for your answer\" notice, a sub-agent call is an ordinary tool step and no artifact is offered. The optional pi-ambient and pi-workspace blocks build on it.",
|
|
6
6
|
"dependencies": [
|
|
7
|
-
"@hachej/boring-ui-kit@^0.4.
|
|
8
|
-
"@hachej/boring-files@^0.4.
|
|
9
|
-
"@hachej/boring-feedback@^0.4.
|
|
7
|
+
"@hachej/boring-ui-kit@^0.4.33",
|
|
8
|
+
"@hachej/boring-files@^0.4.33",
|
|
9
|
+
"@hachej/boring-feedback@^0.4.33",
|
|
10
10
|
"react@^19.0.0",
|
|
11
11
|
"react-dom@^19.0.0",
|
|
12
12
|
"@base-ui/react@1.9.0",
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
"zod@^4.6.5"
|
|
20
20
|
],
|
|
21
21
|
"registryDependencies": [
|
|
22
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
23
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
22
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.33/r/utils.json",
|
|
23
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.33/r/button.json"
|
|
24
24
|
],
|
|
25
25
|
"files": [
|
|
26
26
|
{
|
package/r/pi-workspace.json
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
"react-dom@^19.0.0"
|
|
9
9
|
],
|
|
10
10
|
"registryDependencies": [
|
|
11
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
12
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
13
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
11
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.33/r/pi-chat.json",
|
|
12
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.33/r/utils.json",
|
|
13
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.33/r/dock.json"
|
|
14
14
|
],
|
|
15
15
|
"files": [
|
|
16
16
|
{
|
package/r/placed-agent.json
CHANGED
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
"lucide-react@^1.51.0"
|
|
10
10
|
],
|
|
11
11
|
"registryDependencies": [
|
|
12
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
13
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
14
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
15
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
12
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.33/r/pi-app.json",
|
|
13
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.33/r/pi-ambient.json",
|
|
14
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.33/r/app-shell.json",
|
|
15
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.33/r/utils.json"
|
|
16
16
|
],
|
|
17
17
|
"files": [
|
|
18
18
|
{
|
package/r/pm-panel.json
CHANGED
|
@@ -4,22 +4,22 @@
|
|
|
4
4
|
"title": "Shared PM composer and in-app panel",
|
|
5
5
|
"description": "« Améliorer l'app »: one PmComposer (touch and desktop presets, FR/EN strings) with « Montrer », the point strip, numbered pins and timestamped chips; AppShell improve composes the in-flow sheet, strip and the full panel, and PmChat gives Studio the same conversation. Pins and chips come from the public feedback picker; the request card and « Mes demandes » render through StudioTaskBoardSeam (D-417).",
|
|
6
6
|
"dependencies": [
|
|
7
|
-
"@hachej/boring-ui-kit@^0.4.
|
|
8
|
-
"@hachej/boring-feedback@^0.4.
|
|
7
|
+
"@hachej/boring-ui-kit@^0.4.33",
|
|
8
|
+
"@hachej/boring-feedback@^0.4.33",
|
|
9
9
|
"@earendil-works/pi-durable@~1.4.0",
|
|
10
10
|
"lucide-react@^1.51.0",
|
|
11
11
|
"react@^19.0.0",
|
|
12
12
|
"react-dom@^19.0.0"
|
|
13
13
|
],
|
|
14
14
|
"registryDependencies": [
|
|
15
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
16
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
17
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
18
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
19
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
20
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
21
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
22
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
15
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.33/r/pi-chat.json",
|
|
16
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.33/r/pi-chat-markdown.json",
|
|
17
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.33/r/pi-chat-plugins.json",
|
|
18
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.33/r/pi-chat-voice.json",
|
|
19
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.33/r/feedback.json",
|
|
20
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.33/r/task-board.json",
|
|
21
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.33/r/task-mentions.json",
|
|
22
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.33/r/utils.json"
|
|
23
23
|
],
|
|
24
24
|
"files": [
|
|
25
25
|
{
|
|
@@ -78,13 +78,13 @@
|
|
|
78
78
|
},
|
|
79
79
|
{
|
|
80
80
|
"path": "registry/pm-panel/pm-task-source.tsx",
|
|
81
|
-
"content": "'use client';\n\n// The one-line app's task source (spec 2 « Mes demandes » and C7's request card): the host's same-origin task API, behind a feature\n// check. This file is the one adapter between the host's records and `TaskRecord`: the host contract's field names change here only.\nimport { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from 'react';\nimport type { EntryRecord } from '@earendil-works/pi-durable';\nimport type { TaskRecord, TaskState } from '@hachej/boring-ui-kit/task-board';\nimport { taskReference } from '@hachej/boring-ui-kit/task-board';\nimport { Button } from '@/components/ui/button';\nimport { TaskRequestCard } from '../task-board/task-board';\nimport type { ImproveSpec, StudioTaskBoardSeam } from './use-pm-panel';\nimport type { PmText } from './pm-text';\n\n/**\n * One task of `GET {tasks}?mine=1` (`{ tasks, cursor }`, hachej/boring-factory#892): the viewer's own requests for this app, as requester\n * or waiting on them. The D-ref is `ref` (list) or `number` (`tasks_create`), the update time `updated` or `updated_at`.\n */\nexport interface HostTask {\n readonly id: string;\n readonly ref?: string;\n readonly number?: string;\n readonly title: string;\n readonly state: string;\n /** Host wording, shown as is. */\n readonly status?: string;\n readonly created?: string | number;\n readonly updated?: string | number;\n readonly updated_at?: string | number;\n readonly waiting_on_you?: boolean;\n /** The question, when the task waits on the viewer. */\n readonly question?: string;\n}\n\nconst STATES: readonly TaskState[] = ['waiting', 'working', 'open', 'done', 'closed'];\nconst stamp = (value: string | number | undefined) => value === undefined ? undefined : new Date(value).toISOString();\n\n/** Every listed task is the viewer's (the host filtered them), so the viewer is their requester for « Mes demandes ». */\nexport function toTaskRecord(task: HostTask, viewer: string): TaskRecord {\n const waiting = task.waiting_on_you === true;\n const created = stamp(task.created), updated = stamp(task.updated ?? task.updated_at), ref = task.ref ?? task.number;\n return {\n id: task.id, ...(ref ? { number: ref } : {}), title: task.title, ...(task.status ? { status: task.status } : {}), requesterId: viewer, waiting_on_you: waiting,\n state: waiting ? 'waiting' : STATES.includes(task.state as TaskState) ? task.state as TaskState : 'open',\n ...(waiting ? { waiting_on: { who: viewer, type: 'person' as const, ...(task.question ? { question: task.question } : {}) } } : {}),\n ...(created ? { created_at: created } : {}), ...(updated ? { updated_at: updated } : {}),\n };\n}\n\n/** `null`: the host has no task source for this viewer (404 or 403), so « Mes demandes » stays disabled. */\nexport async function readTasks(url: string, viewer: string, signal?: AbortSignal): Promise<readonly TaskRecord[] | null> {\n const response = await fetch(`${url}?mine=1`, { credentials: 'same-origin', ...(signal ? { signal } : {}) });\n if (response.status === 404 || response.status === 403) return null;\n if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);\n const body = await response.json() as { readonly tasks?: readonly HostTask[] };\n return (body.tasks ?? []).map(task => toTaskRecord(task, viewer));\n}\n\n/** Answers the question the task waits on, as the viewer. « Confirmer » is such an answer. */\nexport async function answerTask(url: string, id: string, text: string): Promise<void> {\n const response = await fetch(`${url}/${encodeURIComponent(id)}/answer`, { method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ text }) });\n if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);\n}\n\nconst REF = /\\bD-\\d+\\b/, UUID = /\\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\b/i;\n/** A D-ref or task id in a tool result: `details` fields first (`ref`, `number`, `id`, nested `task`), then the text. */\nfunction referenceIn(value: unknown): { readonly ref?: string; readonly id?: string } | undefined {\n if (typeof value === 'string') { const ref = REF.exec(value)?.[0], id = UUID.exec(value)?.[0]; return ref || id ? { ...(ref ? { ref } : {}), ...(id ? { id } : {}) } : undefined; }\n if (!value || typeof value !== 'object') return undefined;\n const record = value as Record<string, unknown>;\n for (const key of ['ref', 'number', 'id', 'task']) { const found = record[key] === undefined ? undefined : referenceIn(record[key]); if (found) return found; }\n return undefined;\n}\n\n/** The PM's task tool (hachej/boring-factory#892): `content[0].text` is JSON `{ created, task: { id, number: 'D-n', title, state, status, updated_at } }`. */\nexport const TASK_CREATE_TOOL = 'tasks_create';\nfunction exactTask(message: { readonly toolName: string; readonly content: readonly { readonly type: string; readonly text?: string }[] }): { readonly ref?: string; readonly id?: string } | undefined {\n if (message.toolName !== TASK_CREATE_TOOL || message.content[0]?.type !== 'text') return undefined;\n try {\n const task = (JSON.parse(message.content[0].text ?? '') as { readonly task?: { readonly id?: unknown; readonly number?: unknown } }).task;\n const id = typeof task?.id === 'string' ? task.id : undefined, ref = typeof task?.number === 'string' ? task.number : undefined;\n return id || ref ? { ...(ref ? { ref } : {}), ...(id ? { id } : {}) } : undefined;\n } catch { return undefined; }\n}\n\n/**\n * The task a PM reply created, from a tool result run in the same turn before that reply: `tasks_create`'s JSON first; then, tolerant\n * (an older or renamed tool), any tool whose name contains `task` and whose result carries a D-ref or an id. Adjust here when it changes.\n */\nexport function createdTask(entries: readonly EntryRecord[], reply: EntryRecord): { readonly ref?: string; readonly id?: string } | undefined {\n const at = entries.findIndex(entry => entry.id === reply.id);\n for (let index = (at < 0 ? entries.length : at) - 1; index >= 0; index--) {\n for (const message of [...(entries[index]!.model ?? [])].reverse()) {\n if (message.role === 'user') return undefined;\n if (message.role !== 'toolResult' || message.isError || !/task/i.test(message.toolName)) continue;\n const found = exactTask(message) ?? referenceIn(message.details) ?? referenceIn(message.content.map(part => part.type === 'text' ? part.text : '').join('\\n'));\n if (found) return found;\n }\n }\n return undefined;\n}\n\n/**\n * `spec.tasks` for the one line: read when `active`, again when the conversation grows (a PM reply may have created a task) and after an\n * answer. `undefined` until the host answers, and for good when it has no task source.\n */\nexport function useHostTasks(url: string | undefined, viewer: string | undefined, controller: ImproveSpec['controller'] | undefined, text: PmText): StudioTaskBoardSeam | undefined {\n const [tasks, setTasks] = useState<readonly TaskRecord[] | null>();\n const entries = useSyncExternalStore(controller?.subscribe ?? noSubscribe, () => controller?.getSnapshot().view?.entries.length ?? 0, () => 0);\n const [answered, setAnswered] = useState(0);\n const absent = tasks === null;\n useEffect(() => {\n if (!url || !viewer || absent) return;\n const abort = new AbortController();\n const timer = setTimeout(() => { readTasks(url, viewer, abort.signal).then(setTasks, () => undefined); }, 300);\n return () => { clearTimeout(timer); abort.abort(); };\n }, [url, viewer, absent, entries, answered]);\n const confirm = useCallback(async (task: TaskRecord) => {\n if (!url) return;\n setTasks(current => current?.map(each => each.id === task.id ? { ...each, state: 'working', waiting_on_you: false } : each));\n await answerTask(url, task.id, text.confirm).catch(() => undefined);\n setAnswered(count => count + 1);\n }, [url, text.confirm]);\n return useMemo(() => !tasks || !viewer ? undefined : {\n tasks: [...tasks], currentUser: viewer,\n // No task route in the one line: a card is the request, its answer happens in the PM chat.\n onOpen: () => undefined,\n renderRequest: ({ entry, edit }) => {\n const made = createdTask(controller?.getSnapshot().view?.entries ?? [], entry);\n const task = made && tasks.find(each => each.id === made.id || (made.ref !== undefined && taskReference(each) === made.ref));\n if (!task) return undefined;\n return <TaskRequestCard task={task} variant=\"compact\" locale={text.locale} viewerAction=\"confirmation\" actions={task.waiting_on_you ? <>\n <Button variant=\"outline\" data-testid=\"pm-request-modify\" onClick={() => edit(task.title)}>{text.modify}</Button>\n <Button data-testid=\"pm-request-confirm\" onClick={() => { void confirm(task); }}>{text.confirm}</Button>\n </> : undefined} />;\n },\n }, [tasks, viewer, text, confirm, controller]);\n}\nconst noSubscribe = () => () => undefined;\n",
|
|
81
|
+
"content": "'use client';\n\n// The one-line app's task source (spec 2 « Mes demandes » and C7's request card): the host's same-origin task API, behind a feature\n// check. This file is the one adapter between the host's records and `TaskRecord`: the host contract's field names change here only.\nimport { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from 'react';\nimport type { EntryRecord } from '@earendil-works/pi-durable';\nimport type { TaskRecord, TaskState } from '@hachej/boring-ui-kit/task-board';\nimport { taskReference } from '@hachej/boring-ui-kit/task-board';\nimport { Button } from '@/components/ui/button';\nimport { TaskRequestCard } from '../task-board/task-board';\nimport type { ImproveSpec, StudioTaskBoardSeam } from './use-pm-panel';\nimport type { PmText } from './pm-text';\n\n/**\n * One task of `GET {tasks}?mine=1` (`{ tasks, cursor }`, hachej/boring-factory#892): the viewer's own requests for this app, as requester\n * or waiting on them. The D-ref is `ref` (list) or `number` (`tasks_create`), the update time `updated` or `updated_at`.\n */\nexport interface HostTask {\n readonly id: string;\n readonly ref?: string;\n readonly number?: string;\n readonly title: string;\n readonly state: string;\n /** Host wording, shown as is. */\n readonly status?: string;\n readonly created?: string | number;\n readonly updated?: string | number;\n readonly updated_at?: string | number;\n readonly waiting_on_you?: boolean;\n /** The question, when the task waits on the viewer. */\n readonly question?: string;\n /** ISO time the requester answered « Confirmer »; absent until then. */\n readonly confirmed_at?: string;\n /** The viewer filed the task. `mine=1` also lists tasks only waiting on the viewer, so only `true` lets them withdraw it. */\n readonly is_requester?: boolean;\n}\n\n/** A host task as the one line draws it: `is_requester` stays here, in the adapter, for « Retirer » only. */\nexport type HostTaskRecord = TaskRecord & { readonly is_requester?: true };\n\nconst STATES: readonly TaskState[] = ['waiting', 'working', 'open', 'done', 'closed'];\nconst stamp = (value: string | number | undefined) => value === undefined ? undefined : new Date(value).toISOString();\n\n/** Every listed task is the viewer's (the host filtered them), so the viewer is their requester for « Mes demandes ». */\nexport function toTaskRecord(task: HostTask, viewer: string): HostTaskRecord {\n const waiting = task.waiting_on_you === true;\n const created = stamp(task.created), updated = stamp(task.updated ?? task.updated_at), confirmed = stamp(task.confirmed_at), ref = task.ref ?? task.number;\n return {\n id: task.id, ...(ref ? { number: ref } : {}), title: task.title, ...(task.status ? { status: task.status } : {}), requesterId: viewer, waiting_on_you: waiting,\n state: waiting ? 'waiting' : STATES.includes(task.state as TaskState) ? task.state as TaskState : 'open',\n ...(waiting ? { waiting_on: { who: viewer, type: 'person' as const, ...(task.question ? { question: task.question } : {}) } } : {}),\n ...(created ? { created_at: created } : {}), ...(updated ? { updated_at: updated } : {}), ...(confirmed ? { confirmed_at: confirmed } : {}),\n ...(task.is_requester === true ? { is_requester: true as const } : {}),\n };\n}\n\n/** `null`: the host has no task source for this viewer (404 or 403), so « Mes demandes » stays disabled. */\nexport async function readTasks(url: string, viewer: string, signal?: AbortSignal): Promise<readonly HostTaskRecord[] | null> {\n const response = await fetch(`${url}?mine=1`, { credentials: 'same-origin', ...(signal ? { signal } : {}) });\n if (response.status === 404 || response.status === 403) return null;\n if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);\n const body = await response.json() as { readonly tasks?: readonly HostTask[] };\n return (body.tasks ?? []).map(task => toTaskRecord(task, viewer));\n}\n\n/** Answers the question the task waits on, as the viewer. « Confirmer » is such an answer. */\nexport async function answerTask(url: string, id: string, text: string): Promise<void> {\n const response = await fetch(`${url}/${encodeURIComponent(id)}/answer`, { method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ text }) });\n if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);\n}\n\n/**\n * Withdraws the viewer's own request (`POST {tasks}/<id>/withdraw`, requester only): `withdrawn` on 200; `started` on 409, once work has\n * started; `refused` otherwise (404 for a non-requester, a network failure).\n */\nexport async function withdrawTask(url: string, id: string): Promise<'withdrawn' | 'started' | 'refused'> {\n const response = await fetch(`${url}/${encodeURIComponent(id)}/withdraw`, { method: 'POST', credentials: 'same-origin' }).catch(() => undefined);\n return response?.ok ? 'withdrawn' : response?.status === 409 ? 'started' : 'refused';\n}\n\n/** Withdraws only what the viewer filed and nobody started: `is_requester`, and `open` or waiting on the viewer. */\nexport const withdrawable = (task: TaskRecord) => (task as HostTaskRecord).is_requester === true && (task.state === 'open' || task.waiting_on_you === true);\n\n/** « Retirer » on the requester's card in « Mes demandes »: one tap asks « Retirer cette demande ? », the second withdraws. */\nfunction WithdrawAction({ task, failed, text, onWithdraw }: { readonly task: HostTaskRecord; readonly failed: boolean; readonly text: PmText; readonly onWithdraw: (task: HostTaskRecord) => Promise<void> }) {\n const [asking, setAsking] = useState(false), [busy, setBusy] = useState(false);\n const withdraw = () => { setBusy(true); void onWithdraw(task).finally(() => { setBusy(false); setAsking(false); }); };\n return <>\n {withdrawable(task) && (asking ? <>\n <p data-testid=\"pm-withdraw-ask\" className=\"col-span-2 m-0 flex min-h-0! items-center text-sm font-medium\">{text.withdrawAsk}</p>\n <Button variant=\"destructive\" data-testid=\"pm-withdraw-confirm\" disabled={busy} onClick={withdraw}>{text.withdraw}</Button>\n <Button variant=\"outline\" data-testid=\"pm-withdraw-cancel\" disabled={busy} onClick={() => setAsking(false)}>{text.cancel}</Button>\n </> : <Button variant=\"outline\" data-testid=\"pm-withdraw\" className=\"col-span-2\" onClick={() => setAsking(true)}>{text.withdraw}</Button>)}\n {failed && <p role=\"status\" data-testid=\"pm-withdraw-failed\" className=\"col-span-2 m-0 min-h-0! text-[13px] text-muted-foreground\">{text.withdrawFailed}</p>}\n </>;\n}\n\nconst REF = /\\bD-\\d+\\b/, UUID = /\\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\b/i;\n/** A D-ref or task id in a tool result: `details` fields first (`ref`, `number`, `id`, nested `task`), then the text. */\nfunction referenceIn(value: unknown): { readonly ref?: string; readonly id?: string } | undefined {\n if (typeof value === 'string') { const ref = REF.exec(value)?.[0], id = UUID.exec(value)?.[0]; return ref || id ? { ...(ref ? { ref } : {}), ...(id ? { id } : {}) } : undefined; }\n if (!value || typeof value !== 'object') return undefined;\n const record = value as Record<string, unknown>;\n for (const key of ['ref', 'number', 'id', 'task']) { const found = record[key] === undefined ? undefined : referenceIn(record[key]); if (found) return found; }\n return undefined;\n}\n\n/** The PM's task tool (hachej/boring-factory#892): `content[0].text` is JSON `{ created, task: { id, number: 'D-n', title, state, status, updated_at } }`. */\nexport const TASK_CREATE_TOOL = 'tasks_create';\nfunction exactTask(message: { readonly toolName: string; readonly content: readonly { readonly type: string; readonly text?: string }[] }): { readonly ref?: string; readonly id?: string } | undefined {\n if (message.toolName !== TASK_CREATE_TOOL || message.content[0]?.type !== 'text') return undefined;\n try {\n const task = (JSON.parse(message.content[0].text ?? '') as { readonly task?: { readonly id?: unknown; readonly number?: unknown } }).task;\n const id = typeof task?.id === 'string' ? task.id : undefined, ref = typeof task?.number === 'string' ? task.number : undefined;\n return id || ref ? { ...(ref ? { ref } : {}), ...(id ? { id } : {}) } : undefined;\n } catch { return undefined; }\n}\n\n/**\n * The task a PM reply created, from a tool result run in the same turn before that reply: `tasks_create`'s JSON first; then, tolerant\n * (an older or renamed tool), any tool whose name contains `task` and whose result carries a D-ref or an id. Adjust here when it changes.\n */\nexport function createdTask(entries: readonly EntryRecord[], reply: EntryRecord): { readonly ref?: string; readonly id?: string } | undefined {\n const at = entries.findIndex(entry => entry.id === reply.id);\n for (let index = (at < 0 ? entries.length : at) - 1; index >= 0; index--) {\n for (const message of [...(entries[index]!.model ?? [])].reverse()) {\n if (message.role === 'user') return undefined;\n if (message.role !== 'toolResult' || message.isError || !/task/i.test(message.toolName)) continue;\n const found = exactTask(message) ?? referenceIn(message.details) ?? referenceIn(message.content.map(part => part.type === 'text' ? part.text : '').join('\\n'));\n if (found) return found;\n }\n }\n return undefined;\n}\n\n/**\n * `spec.tasks` for the one line: read when `active`, again when the conversation grows (a PM reply may have created a task), after an\n * answer and after a withdraw refused with 409. `undefined` until the host answers, and for good when it has no task source. A withdrawn\n * request leaves the list; « Confirmer » shows « Envoyée » at once.\n */\nexport function useHostTasks(url: string | undefined, viewer: string | undefined, controller: ImproveSpec['controller'] | undefined, text: PmText): StudioTaskBoardSeam | undefined {\n const [read, setTasks] = useState<readonly HostTaskRecord[] | null>();\n const [withdrawn, setWithdrawn] = useState<ReadonlySet<string>>(() => new Set());\n const [refused, setRefused] = useState<ReadonlySet<string>>(() => new Set());\n const tasks = useMemo(() => read?.filter(task => !withdrawn.has(task.id)) ?? read, [read, withdrawn]);\n const entries = useSyncExternalStore(controller?.subscribe ?? noSubscribe, () => controller?.getSnapshot().view?.entries.length ?? 0, () => 0);\n const [answered, setAnswered] = useState(0);\n const absent = read === null;\n useEffect(() => {\n if (!url || !viewer || absent) return;\n const abort = new AbortController();\n const timer = setTimeout(() => { readTasks(url, viewer, abort.signal).then(setTasks, () => undefined); }, 300);\n return () => { clearTimeout(timer); abort.abort(); };\n }, [url, viewer, absent, entries, answered]);\n const confirm = useCallback(async (task: TaskRecord) => {\n if (!url) return;\n // « Envoyée » at once: confirmed, nobody started yet.\n setTasks(current => current?.map(each => {\n if (each.id !== task.id) return each;\n const { waiting_on: _, ...rest } = each;\n return { ...rest, state: 'open', waiting_on_you: false, confirmed_at: each.confirmed_at ?? new Date().toISOString() };\n }));\n await answerTask(url, task.id, text.confirm).catch(() => undefined);\n setAnswered(count => count + 1);\n }, [url, text.confirm]);\n const withdraw = useCallback(async (task: HostTaskRecord) => {\n if (!url) return;\n setRefused(current => { const next = new Set(current); next.delete(task.id); return next; });\n const outcome = await withdrawTask(url, task.id);\n if (outcome === 'withdrawn') { setWithdrawn(current => new Set(current).add(task.id)); return; }\n setRefused(current => new Set(current).add(task.id));\n // Started meanwhile: read again, so the card shows its real state.\n if (outcome === 'started') setAnswered(count => count + 1);\n }, [url]);\n return useMemo(() => !tasks || !viewer ? undefined : {\n tasks: [...tasks], currentUser: viewer,\n // No task route in the one line: a card is the request, its answer happens in the PM chat.\n onOpen: () => undefined,\n cardActions: (task: TaskRecord) => withdrawable(task) || refused.has(task.id) ? <WithdrawAction key={task.id} task={task} failed={refused.has(task.id)} text={text} onWithdraw={withdraw} /> : undefined,\n renderRequest: ({ entry, edit }) => {\n const made = createdTask(controller?.getSnapshot().view?.entries ?? [], entry);\n const task = made && tasks.find(each => each.id === made.id || (made.ref !== undefined && taskReference(each) === made.ref));\n if (!task) return undefined;\n return <TaskRequestCard task={task} variant=\"compact\" locale={text.locale} viewerAction=\"confirmation\" actions={task.waiting_on_you ? <>\n <Button variant=\"outline\" data-testid=\"pm-request-modify\" onClick={() => edit(task.title)}>{text.modify}</Button>\n <Button data-testid=\"pm-request-confirm\" onClick={() => { void confirm(task); }}>{text.confirm}</Button>\n </> : undefined} />;\n },\n }, [tasks, viewer, text, confirm, withdraw, refused, controller]);\n}\nconst noSubscribe = () => () => undefined;\n",
|
|
82
82
|
"type": "registry:component",
|
|
83
83
|
"target": "components/pm-panel/pm-task-source.tsx"
|
|
84
84
|
},
|
|
85
85
|
{
|
|
86
86
|
"path": "registry/pm-panel/pm-text.ts",
|
|
87
|
-
"content": "/** Spec 1 §9 and spec 2's strings, character for character: FR for the app, EN for Studio. */\nexport interface PmText {\n readonly locale: 'fr' | 'en';\n readonly placeholderBefore: string;\n readonly placeholderAfter: string;\n readonly hintIdle: string;\n readonly hintRec: string;\n /** The touch strip's C4 hint when the whole one would take more than two lines (spec 1 §2). */\n readonly hintRecShort: string;\n readonly mode: string;\n readonly select: string;\n readonly navigate: string;\n readonly done: string;\n readonly doneLabel: string;\n readonly speak: string;\n readonly recorder: string;\n readonly attach: string;\n readonly dictate: string;\n readonly send: string;\n readonly remove: string;\n readonly show: string;\n readonly showTitle: string;\n readonly meta: (seconds: number | undefined, captures: number) => string;\n readonly sentMeta: (seconds: number | undefined, captures: number) => string;\n readonly mentionHeader: (query: string) => string;\n readonly onYou: string;\n readonly added: (n: number, kind: string, label: string) => string;\n readonly removed: (n: number) => string;\n readonly title: string;\n readonly openPanel: string;\n readonly close: string;\n readonly review: string;\n readonly requests: string;\n readonly back: string;\n readonly subtitle: (app: string) => string;\n /** Click to comment (desktop, spec 1 §3b). */\n readonly commentOn: (kind: string, label: string) => string;\n readonly commentPlaceholder: string;\n readonly addToMessage: string;\n readonly sendToPm: string;\n readonly sentToPm: string;\n /** The request card's two answers (C7): « Modifier » goes back to the composer, « Confirmer » is the viewer's answer to the task. */\n readonly modify: string;\n readonly confirm: string;\n /** The app panel's one muted line when the PM's run ends in error without a reply: the member never sees a tool or a raw error (C7). */\n readonly failed: string;\n}\n\n/** `0:14`: whole seconds, never a fraction. */\nexport const clock = (seconds: number): string => {\n const whole = Math.max(0, Math.floor(seconds));\n return `${Math.floor(whole / 60)}:${String(whole % 60).padStart(2, '0')}`;\n};\nconst captures = (n: number, one: string, many: string) => `${n} ${n === 1 ? one : many}`;\n\nexport const pmTextFr: PmText = {\n locale: 'fr',\n placeholderBefore: 'Décrivez le problème, ou ', placeholderAfter: ' pour me le montrer.',\n hintIdle: 'Touchez ce que vous voulez montrer', hintRec: 'Parlez et touchez ce que vous montrez', hintRecShort: 'Parlez et touchez',\n mode: 'Mode', select: 'Sélectionner', navigate: 'Naviguer', done: 'Terminer', doneLabel: 'Terminer : relire dans le composer', speak: 'Parler',\n recorder: \"Ouvrir l'enregistreur\", attach: 'Joindre un fichier', dictate: 'Dicter', send: 'Envoyer', remove: 'Retirer', show: 'Montrer', showTitle: 'Montrer',\n meta: (seconds, n) => seconds === undefined ? `Écrit · ${captures(n, 'capture', 'captures')}` : `Dicté · ${clock(seconds)} · ${captures(n, 'capture', 'captures')} · modifiable`,\n sentMeta: (seconds, n) => seconds === undefined ? `Écrit · ${captures(n, 'capture', 'captures')}` : `Dicté · ${clock(seconds)} · ${captures(n, 'capture', 'captures')}`,\n mentionHeader: query => `TÂCHES · ${query}`, onYou: 'À vous',\n added: (n, kind, label) => `Élément ${n} ajouté : ${kind} · ${label}`, removed: n => `Élément ${n} retiré`,\n title: \"Améliorer l'app\", openPanel: 'Ouvrir le panneau', close: 'Fermer', review: 'Relisez, puis envoyez', requests: 'Mes demandes', back: '‹ Retour',\n subtitle: app => `Le chef de projet · ${app}`,\n commentOn: (kind, label) => `Commenter ${kind} · ${label}`, commentPlaceholder: 'Votre commentaire…', addToMessage: 'Ajouter au message', sendToPm: 'Envoyer au PM', sentToPm: 'Envoyé au PM',\n modify: 'Modifier', confirm: 'Confirmer', failed: \"Le chef de projet n'a pas pu terminer. Réessayez.\",\n};\n\nexport const pmTextEn: PmText = {\n locale: 'en',\n placeholderBefore: 'Describe the problem, or ', placeholderAfter: ' to show me',\n hintIdle: 'Tap what you want to show', hintRec: 'Speak and tap what you show', hintRecShort: 'Speak and tap',\n mode: 'Mode', select: 'Select', navigate: 'Navigate', done: 'Done', doneLabel: 'Done: reread in the composer', speak: 'Speak',\n recorder: 'Open the recorder', attach: 'Attach a file', dictate: 'Dictate', send: 'Send', remove: 'Remove', show: 'Montrer', showTitle: 'Montrer: point at the app',\n meta: (seconds, n) => seconds === undefined ? `Written · ${captures(n, 'capture', 'captures')}` : `Dictated · ${clock(seconds)} · ${captures(n, 'capture', 'captures')} · editable`,\n sentMeta: (seconds, n) => seconds === undefined ? `Written · ${captures(n, 'capture', 'captures')}` : `Dictated · ${clock(seconds)} · ${captures(n, 'capture', 'captures')}`,\n mentionHeader: query => `TASKS · ${query}`, onYou: 'On you',\n added: (n, kind, label) => `Item ${n} added: ${kind} · ${label}`, removed: n => `Item ${n} removed`,\n title: 'Improve the app', openPanel: 'Open the panel', close: 'Close', review: 'Review, then send', requests: 'My requests', back: '‹ Back',\n subtitle: app => `The project lead · ${app}`,\n commentOn: (kind, label) => `Comment on ${kind} · ${label}`, commentPlaceholder: 'Your comment…', addToMessage: 'Add to message', sendToPm: 'Send to the PM', sentToPm: 'Sent to the PM',\n modify: 'Modify', confirm: 'Confirm', failed: \"The PM couldn't finish. Try again.\",\n};\n\nexport const pmTextFor = (locale: string | undefined): PmText => locale?.startsWith('en') ? pmTextEn : pmTextFr;\n",
|
|
87
|
+
"content": "/** Spec 1 §9 and spec 2's strings, character for character: FR for the app, EN for Studio. */\nexport interface PmText {\n readonly locale: 'fr' | 'en';\n readonly placeholderBefore: string;\n readonly placeholderAfter: string;\n readonly hintIdle: string;\n readonly hintRec: string;\n /** The touch strip's C4 hint when the whole one would take more than two lines (spec 1 §2). */\n readonly hintRecShort: string;\n readonly mode: string;\n readonly select: string;\n readonly navigate: string;\n readonly done: string;\n readonly doneLabel: string;\n readonly speak: string;\n readonly recorder: string;\n readonly attach: string;\n readonly dictate: string;\n readonly send: string;\n readonly remove: string;\n readonly show: string;\n readonly showTitle: string;\n readonly meta: (seconds: number | undefined, captures: number) => string;\n readonly sentMeta: (seconds: number | undefined, captures: number) => string;\n readonly mentionHeader: (query: string) => string;\n readonly onYou: string;\n readonly added: (n: number, kind: string, label: string) => string;\n readonly removed: (n: number) => string;\n readonly title: string;\n readonly openPanel: string;\n readonly close: string;\n readonly review: string;\n readonly requests: string;\n readonly back: string;\n readonly subtitle: (app: string) => string;\n /** Click to comment (desktop, spec 1 §3b). */\n readonly commentOn: (kind: string, label: string) => string;\n readonly commentPlaceholder: string;\n readonly addToMessage: string;\n readonly sendToPm: string;\n readonly sentToPm: string;\n /** The request card's two answers (C7): « Modifier » goes back to the composer, « Confirmer » is the viewer's answer to the task. */\n readonly modify: string;\n readonly confirm: string;\n /** The requester withdraws a request nobody started (« Mes demandes », decided 2026-10-29): the action, its confirm step and its error line. */\n readonly withdraw: string;\n readonly withdrawAsk: string;\n readonly cancel: string;\n readonly withdrawFailed: string;\n /** The app panel's one muted line when the PM's run ends in error without a reply: the member never sees a tool or a raw error (C7). */\n readonly failed: string;\n}\n\n/** `0:14`: whole seconds, never a fraction. */\nexport const clock = (seconds: number): string => {\n const whole = Math.max(0, Math.floor(seconds));\n return `${Math.floor(whole / 60)}:${String(whole % 60).padStart(2, '0')}`;\n};\nconst captures = (n: number, one: string, many: string) => `${n} ${n === 1 ? one : many}`;\n\nexport const pmTextFr: PmText = {\n locale: 'fr',\n placeholderBefore: 'Décrivez le problème, ou ', placeholderAfter: ' pour me le montrer.',\n hintIdle: 'Touchez ce que vous voulez montrer', hintRec: 'Parlez et touchez ce que vous montrez', hintRecShort: 'Parlez et touchez',\n mode: 'Mode', select: 'Sélectionner', navigate: 'Naviguer', done: 'Terminer', doneLabel: 'Terminer : relire dans le composer', speak: 'Parler',\n recorder: \"Ouvrir l'enregistreur\", attach: 'Joindre un fichier', dictate: 'Dicter', send: 'Envoyer', remove: 'Retirer', show: 'Montrer', showTitle: 'Montrer',\n meta: (seconds, n) => seconds === undefined ? `Écrit · ${captures(n, 'capture', 'captures')}` : `Dicté · ${clock(seconds)} · ${captures(n, 'capture', 'captures')} · modifiable`,\n sentMeta: (seconds, n) => seconds === undefined ? `Écrit · ${captures(n, 'capture', 'captures')}` : `Dicté · ${clock(seconds)} · ${captures(n, 'capture', 'captures')}`,\n mentionHeader: query => `TÂCHES · ${query}`, onYou: 'À vous',\n added: (n, kind, label) => `Élément ${n} ajouté : ${kind} · ${label}`, removed: n => `Élément ${n} retiré`,\n title: \"Améliorer l'app\", openPanel: 'Ouvrir le panneau', close: 'Fermer', review: 'Relisez, puis envoyez', requests: 'Mes demandes', back: '‹ Retour',\n subtitle: app => `Le chef de projet · ${app}`,\n commentOn: (kind, label) => `Commenter ${kind} · ${label}`, commentPlaceholder: 'Votre commentaire…', addToMessage: 'Ajouter au message', sendToPm: 'Envoyer au PM', sentToPm: 'Envoyé au PM',\n modify: 'Modifier', confirm: 'Confirmer', failed: \"Le chef de projet n'a pas pu terminer. Réessayez.\",\n withdraw: 'Retirer', withdrawAsk: 'Retirer cette demande\\u00a0?', cancel: 'Annuler', withdrawFailed: 'Impossible de retirer la demande.',\n};\n\nexport const pmTextEn: PmText = {\n locale: 'en',\n placeholderBefore: 'Describe the problem, or ', placeholderAfter: ' to show me',\n hintIdle: 'Tap what you want to show', hintRec: 'Speak and tap what you show', hintRecShort: 'Speak and tap',\n mode: 'Mode', select: 'Select', navigate: 'Navigate', done: 'Done', doneLabel: 'Done: reread in the composer', speak: 'Speak',\n recorder: 'Open the recorder', attach: 'Attach a file', dictate: 'Dictate', send: 'Send', remove: 'Remove', show: 'Montrer', showTitle: 'Montrer: point at the app',\n meta: (seconds, n) => seconds === undefined ? `Written · ${captures(n, 'capture', 'captures')}` : `Dictated · ${clock(seconds)} · ${captures(n, 'capture', 'captures')} · editable`,\n sentMeta: (seconds, n) => seconds === undefined ? `Written · ${captures(n, 'capture', 'captures')}` : `Dictated · ${clock(seconds)} · ${captures(n, 'capture', 'captures')}`,\n mentionHeader: query => `TASKS · ${query}`, onYou: 'On you',\n added: (n, kind, label) => `Item ${n} added: ${kind} · ${label}`, removed: n => `Item ${n} removed`,\n title: 'Improve the app', openPanel: 'Open the panel', close: 'Close', review: 'Review, then send', requests: 'My requests', back: '‹ Back',\n subtitle: app => `The project lead · ${app}`,\n commentOn: (kind, label) => `Comment on ${kind} · ${label}`, commentPlaceholder: 'Your comment…', addToMessage: 'Add to message', sendToPm: 'Send to the PM', sentToPm: 'Sent to the PM',\n modify: 'Modify', confirm: 'Confirm', failed: \"The PM couldn't finish. Try again.\",\n withdraw: 'Withdraw', withdrawAsk: 'Withdraw this request?', cancel: 'Cancel', withdrawFailed: \"Couldn't withdraw the request.\",\n};\n\nexport const pmTextFor = (locale: string | undefined): PmText => locale?.startsWith('en') ? pmTextEn : pmTextFr;\n",
|
|
88
88
|
"type": "registry:lib",
|
|
89
89
|
"target": "components/pm-panel/pm-text.ts"
|
|
90
90
|
},
|
package/r/provider-setup.json
CHANGED