@hachej/boring-ui-kit 0.4.52 → 0.4.54
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/chat-react-session.d.ts +6 -0
- package/dist/chat-react-session.js +33 -6
- package/dist/chat-view.js +3 -1
- package/package.json +2 -2
- package/r/agent-target.json +4 -4
- package/r/app-shell.json +9 -9
- 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/drawing-viewer.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 +2 -2
- 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 +7 -7
- package/r/pi-workspace.json +3 -3
- package/r/placed-agent.json +4 -4
- package/r/provider-setup.json +1 -1
- package/r/registry.json +162 -162
- package/r/shared-chat.json +6 -6
- package/r/spaces.json +1 -1
- package/r/task-list-viewer.json +2 -2
- 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-plugins.json
CHANGED
|
@@ -7,17 +7,17 @@
|
|
|
7
7
|
"react@^19.0.0"
|
|
8
8
|
],
|
|
9
9
|
"registryDependencies": [
|
|
10
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
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.
|
|
14
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
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.
|
|
10
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.54/r/pi-chat.json",
|
|
11
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.54/r/pi-chat-markdown.json",
|
|
12
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.54/r/pi-chat-slash.json",
|
|
13
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.54/r/pi-chat-mentions.json",
|
|
14
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.54/r/pi-chat-pickers.json",
|
|
15
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.54/r/pi-chat-voice.json",
|
|
16
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.54/r/pi-chat-attachments.json",
|
|
17
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.54/r/pi-chat-question.json",
|
|
18
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.54/r/pi-chat-approval.json",
|
|
19
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.54/r/pi-chat-subagent.json",
|
|
20
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.54/r/pi-artifacts.json"
|
|
21
21
|
],
|
|
22
22
|
"files": [
|
|
23
23
|
{
|
package/r/pi-chat-question.json
CHANGED
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
"@earendil-works/pi-ai@~1.4.0"
|
|
10
10
|
],
|
|
11
11
|
"registryDependencies": [
|
|
12
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
12
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.54/r/pi-chat.json"
|
|
13
13
|
],
|
|
14
14
|
"files": [
|
|
15
15
|
{
|
|
16
16
|
"path": "registry/pi-chat-question/question-card.tsx",
|
|
17
|
-
"content": "'use client';\n\nimport { useEffect, useId, useRef, useState } from 'react';\nimport type { FormEvent } from 'react';\nimport type { ToolCall, ToolResultMessage } from '@earendil-works/pi-ai';\nimport { CheckIcon, CircleSlashIcon, SendIcon } from 'lucide-react';\nimport { Button } from '@/components/ui/button';\nimport { Input } from '@/components/ui/input';\nimport { Alert, AlertDescription } from '@/components/ui/alert';\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';\nimport { Item, ItemContent, ItemDescription, ItemMedia, ItemTitle } from '@/components/ui/item';\nimport { Spinner } from '@/components/ui/spinner';\nimport { useChatText } from '../pi-chat/labels';\nimport { ASK_USER_TOOL } from '../pi-chat/config';\nimport type { AnswerOutcome, QuestionCardProps } from '../pi-chat/config';\n\n// The core owns the tool name and the props: it places the question row and shows its waiting notice without this item.\nexport { ASK_USER_TOOL };\nexport type { AnswerOutcome, QuestionCardProps };\n\n/** One question of a form: its text and its options (chips); every question of a form also takes free text. */\nexport interface FormQuestion { readonly question: string; readonly options: readonly string[] }\ninterface Question {\n readonly question: string; readonly options: readonly string[]; readonly allowFreeText: boolean;\n /** Several questions in one form (`questions`), answered together as one text. Empty for a single question. */\n readonly items: readonly FormQuestion[];\n}\n\nconst strings = (value: unknown, max: number) => Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string' && item.length > 0).slice(0, max) : [];\n\n/**\n * Read `{ question, options?, allowFreeText?, questions? }` defensively: model arguments are untrusted input. Free text is allowed\n * unless `allowFreeText` is `false` (and always when there are no options). `questions` (up to 8) makes a form.\n */\nexport function parseQuestion(call: ToolCall): Question {\n const args = call.arguments as Record<string, unknown>;\n const options = strings(args['options'], 6);\n const items = Array.isArray(args['questions']) ? args['questions'].flatMap((item: unknown) => {\n const value = item !== null && typeof item === 'object' ? item as Record<string, unknown> : {};\n return typeof value['question'] === 'string' && value['question'] ? [{ question: value['question'], options: strings(value['options'], 6) }] : [];\n }).slice(0, 8) : [];\n return { question: typeof args['question'] === 'string' ? args['question'] : '', options, allowFreeText: args['allowFreeText'] !== false || options.length === 0, items };\n}\n\n/** The answers of a form as the one text the agent reads: each question, then its answer, or `skipped` when left blank. */\nexport function formAnswer(items: readonly FormQuestion[], values: readonly string[], skipped: string): string {\n return items.map((item, i) => `${i + 1}. ${item.question}\\n→ ${values[i]?.trim() || skipped}`).join('\\n\\n');\n}\n\n/** The answers of a `formAnswer` text, one per question; undefined when the text does not have that shape (then it is shown whole). */\nfunction splitFormAnswer(items: readonly FormQuestion[], text: string): readonly string[] | undefined {\n const values: string[] = [];\n let at = 0;\n for (let i = 0; i < items.length; i++) {\n const head = `${i + 1}. ${items[i]!.question}\\n→ `;\n if (!text.startsWith(head, at)) return undefined;\n at += head.length;\n const end = i + 1 < items.length ? text.indexOf(`\\n\\n${i + 2}. ${items[i + 1]!.question}\\n→ `, at) : text.length;\n if (end < 0) return undefined;\n values.push(text.slice(at, end));\n at = end + 2;\n }\n return values;\n}\n\n/** The answered text from a result such as `{\"kind\":\"answered\",\"answer\":\"...\"}`; undefined when the call did not produce an answer. */\nexport function parseAnswer(result: ToolResultMessage | undefined): { readonly answer: string } | { readonly failure: string } | undefined {\n if (!result) return undefined;\n const text = result.content.map(part => part.type === 'text' ? part.text : '').join('');\n if (!result.isError) {\n try {\n const value: unknown = JSON.parse(text);\n if (value !== null && typeof value === 'object' && (value as { kind?: unknown }).kind === 'answered' && typeof (value as { answer?: unknown }).answer === 'string') return { answer: (value as { answer: string }).answer };\n } catch { /* fall through to the failure text */ }\n }\n return { failure: text || 'The question was not answered.' };\n}\n\n\n/**\n * Human-in-the-loop question: writing (still generated, not answerable), pending (options and free text), then answered with the chosen\n * answer. With `questions` it is one form: each question has its option chips and a free-text field, and Answer sends one text\n * (`formAnswer`). It needs only a tool call and an `answer` callback, so a host can show it outside the chat too (for example in a\n * popover answering a request of its own); wrap it in `ChatTextProvider` for the chat's words there.\n */\nexport function QuestionCard({ call, result, answer, questionId, live = false }: QuestionCardProps) {\n const { labels } = useChatText();\n const question = parseQuestion(call);\n const settled = parseAnswer(result);\n const [sent, setSent] = useState<string | undefined>();\n const [sending, setSending] = useState<string | undefined>();\n const [refusal, setRefusal] = useState<string | undefined>();\n const [text, setText] = useState('');\n const [values, setValues] = useState<readonly string[]>([]);\n const formId = useId();\n const setValue = (i: number, value: string) => setValues(previous => question.items.map((_, j) => j === i ? value : previous[j] ?? ''));\n const mounted = useRef(true);\n useEffect(() => { mounted.current = true; return () => { mounted.current = false; }; }, []);\n const current = settled && 'answer' in settled ? settled.answer : sent;\n // `pending` means the question waits on the person; `writing` means the model is still generating it (options may be partial).\n const state = settled ? ('failure' in settled ? 'failed' : 'answered') : sent !== undefined ? 'sent' : live ? 'writing' : 'pending';\n const open = state === 'pending' || state === 'writing';\n\n async function submit(value: string) {\n const trimmed = value.trim();\n if (!answer || !questionId || live || !trimmed || sending !== undefined) return;\n setRefusal(undefined); setSending(trimmed);\n try {\n const outcome = await answer(questionId, trimmed);\n if (outcome.kind === 'answered') setSent(trimmed);\n else setRefusal(outcome.reason || labels.answerRefused(outcome.kind));\n } catch (cause) { setRefusal(cause instanceof Error ? cause.message : labels.answerNotSent); }\n finally { if (mounted.current) setSending(undefined); }\n }\n const onSubmit = (event: FormEvent) => { event.preventDefault(); void submit(text); };\n const onSubmitForm = (event: FormEvent) => { event.preventDefault(); if (values.some(value => value.trim())) void submit(formAnswer(question.items, values, labels.skipped)); };\n const busy = sending !== undefined || live;\n\n const unavailable = !answer || !questionId || busy;\n // `m-0`: a host without a global preflight keeps the paragraph margin of ItemDescription.\n const wrap = 'm-0 line-clamp-none [overflow-wrap:anywhere]';\n\n if (!open) {\n const values = question.items.length > 0 && current !== undefined ? splitFormAnswer(question.items, current) : undefined;\n return <Item role=\"group\" data-testid=\"question-card\" data-state={state} data-call-id={call.id} aria-label={labels.question}>\n <ItemMedia variant=\"icon\">\n {state === 'answered' ? <CheckIcon role=\"img\" aria-label={labels.answered} /> : state === 'sent' ? <Spinner aria-label={labels.waitingForAgent} /> : <CircleSlashIcon role=\"img\" aria-label={labels.notAnswered} />}\n </ItemMedia>\n <ItemContent className=\"min-w-0\">\n {state === 'failed' && <ItemDescription data-testid=\"question-text\" className={wrap}>{question.question || labels.askedQuestion}</ItemDescription>}\n {values ? <div className=\"flex min-w-0 flex-col gap-2\">{question.items.map((item, i) => <div key={i} data-testid=\"question-answer-item\" className=\"flex min-w-0 flex-col gap-1\">\n <ItemDescription className={wrap}>{item.question}</ItemDescription>\n {values[i] === labels.skipped\n ? <ItemDescription data-testid=\"question-answer\" data-skipped=\"true\" className={wrap}>{values[i]}</ItemDescription>\n : <ItemTitle data-testid=\"question-answer\" className={wrap}>{values[i]}</ItemTitle>}\n </div>)}</div>\n : current !== undefined && <ItemTitle className={wrap}><span className=\"sr-only\">{labels.yourAnswer} </span><span data-testid=\"question-answer\" className=\"whitespace-pre-line\">{current}</span></ItemTitle>}\n {state === 'failed' && settled && 'failure' in settled && <ItemDescription role=\"alert\" className={wrap}>{settled.failure}</ItemDescription>}\n </ItemContent>\n </Item>;\n }\n\n const option = (value: string, selected: boolean, onClick: () => void) => <Button key={value} variant={selected ? 'secondary' : 'outline'} data-testid=\"question-option\" data-option={value}\n aria-pressed={question.items.length > 0 ? selected : undefined} disabled={unavailable} onClick={onClick} className=\"h-auto min-h-8 w-full justify-start whitespace-normal text-start\">\n {selected && <CheckIcon data-icon=\"inline-start\" aria-hidden=\"true\" />}{value}\n </Button>;\n\n return <Card role=\"group\" data-testid=\"question-card\" data-state={state} data-call-id={call.id} aria-label={labels.question}>\n <CardHeader>\n <CardDescription>{labels.questionForYou}</CardDescription>\n <CardTitle data-testid=\"question-text\" className=\"[overflow-wrap:anywhere]\">{question.question || labels.askedQuestion}</CardTitle>\n </CardHeader>\n <CardContent className=\"flex flex-col gap-4\">\n {question.items.length > 0 ? <form data-testid=\"question-form\" onSubmit={onSubmitForm} className=\"flex flex-col gap-4\">\n {question.items.map((item, i) => <div key={i} role=\"group\" aria-labelledby={`${formId}-${i}`} data-testid=\"question-item\" className=\"flex min-w-0 flex-col gap-2\">\n <p id={`${formId}-${i}`} className=\"m-0 [overflow-wrap:anywhere]\">{i + 1}. {item.question}</p>\n {item.options.length > 0 && <div role=\"group\" aria-label={labels.answerOptions} className=\"flex flex-col gap-2\">\n {item.options.map(value => option(value, values[i] === value, () => setValue(i, values[i] === value ? '' : value)))}\n </div>}\n <Input data-testid=\"question-input\" value={item.options.includes(values[i] ?? '') ? '' : values[i] ?? ''} maxLength={2000} disabled={unavailable}\n placeholder={item.options.length ? labels.ownAnswer : labels.typeAnswer} aria-label={`${labels.yourAnswerInput} ${i + 1}`} onChange={event => setValue(i, event.currentTarget.value)} />\n </div>)}\n <div><Button type=\"submit\" data-testid=\"question-submit\" disabled={unavailable || !values.some(value => value.trim())}><SendIcon data-icon=\"inline-start\" aria-hidden=\"true\" />{labels.answer}</Button></div>\n </form> : <>\n {question.options.length > 0 && <div role=\"group\" aria-label={labels.answerOptions} className=\"flex flex-col gap-2\">\n {question.options.map(value => option(value, false, () => { void submit(value); }))}\n </div>}\n {question.allowFreeText && <form onSubmit={onSubmit} className=\"flex gap-2\">\n <Input data-testid=\"question-input\" value={text} maxLength={2000} disabled={unavailable} placeholder={question.options.length ? labels.ownAnswer : labels.typeAnswer}\n aria-label={labels.yourAnswerInput} onChange={event => setText(event.currentTarget.value)} />\n <Button type=\"submit\" data-testid=\"question-submit\" disabled={unavailable || !text.trim()}><SendIcon data-icon=\"inline-start\" aria-hidden=\"true\" />{labels.answer}</Button>\n </form>}\n </>}\n {live && <CardDescription>{labels.stillWritingQuestion}</CardDescription>}\n {!answer && !live && <CardDescription>{labels.answeringUnavailable}</CardDescription>}\n {refusal && <Alert variant=\"destructive\" data-testid=\"question-refusal\"><AlertDescription>{refusal}</AlertDescription></Alert>}\n </CardContent>\n </Card>;\n}\n",
|
|
17
|
+
"content": "'use client';\n\nimport { useEffect, useId, useRef, useState } from 'react';\nimport type { FormEvent } from 'react';\nimport type { ToolCall, ToolResultMessage } from '@earendil-works/pi-ai';\nimport { CheckIcon, CircleSlashIcon, SendIcon } from 'lucide-react';\nimport { Button } from '@/components/ui/button';\nimport { Input } from '@/components/ui/input';\nimport { Alert, AlertDescription } from '@/components/ui/alert';\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';\nimport { Item, ItemContent, ItemDescription, ItemMedia, ItemTitle } from '@/components/ui/item';\nimport { Spinner } from '@/components/ui/spinner';\nimport { useChatText } from '../pi-chat/labels';\nimport { ASK_USER_TOOL } from '../pi-chat/config';\nimport type { AnswerOutcome, QuestionCardProps } from '../pi-chat/config';\n\n// The core owns the tool name and the props: it places the question row and shows its waiting notice without this item.\nexport { ASK_USER_TOOL };\nexport type { AnswerOutcome, QuestionCardProps };\n\n/** One question of a form: its text and its options (chips); every question of a form also takes free text. */\nexport interface FormQuestion { readonly question: string; readonly options: readonly string[] }\ninterface Question {\n readonly question: string; readonly options: readonly string[]; readonly allowFreeText: boolean;\n /** Several questions in one form (`questions`), answered together as one text. Empty for a single question. */\n readonly items: readonly FormQuestion[];\n}\n\nconst strings = (value: unknown, max: number) => Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string' && item.length > 0).slice(0, max) : [];\n\n/**\n * Read `{ question, options?, allowFreeText?, questions? }` defensively: model arguments are untrusted input. Free text is allowed\n * unless `allowFreeText` is `false` (and always when there are no options). `questions` (up to 8) makes a form.\n */\nexport function parseQuestion(call: ToolCall): Question {\n const args = call.arguments as Record<string, unknown>;\n const options = strings(args['options'], 6);\n const items = Array.isArray(args['questions']) ? args['questions'].flatMap((item: unknown) => {\n const value = item !== null && typeof item === 'object' ? item as Record<string, unknown> : {};\n return typeof value['question'] === 'string' && value['question'] ? [{ question: value['question'], options: strings(value['options'], 6) }] : [];\n }).slice(0, 8) : [];\n return { question: typeof args['question'] === 'string' ? args['question'] : '', options, allowFreeText: args['allowFreeText'] !== false || options.length === 0, items };\n}\n\n/** The answers of a form as the one text the agent reads: each question, then its answer, or `skipped` when left blank. */\nexport function formAnswer(items: readonly FormQuestion[], values: readonly string[], skipped: string): string {\n return items.map((item, i) => `${i + 1}. ${item.question}\\n→ ${values[i]?.trim() || skipped}`).join('\\n\\n');\n}\n\n/** The answers of a `formAnswer` text, one per question; undefined when the text does not have that shape (then it is shown whole). */\nfunction splitFormAnswer(items: readonly FormQuestion[], text: string): readonly string[] | undefined {\n const values: string[] = [];\n let at = 0;\n for (let i = 0; i < items.length; i++) {\n const head = `${i + 1}. ${items[i]!.question}\\n→ `;\n if (!text.startsWith(head, at)) return undefined;\n at += head.length;\n const end = i + 1 < items.length ? text.indexOf(`\\n\\n${i + 2}. ${items[i + 1]!.question}\\n→ `, at) : text.length;\n if (end < 0) return undefined;\n values.push(text.slice(at, end));\n at = end + 2;\n }\n return values;\n}\n\n/** The answered text from a result such as `{\"kind\":\"answered\",\"answer\":\"...\"}`; undefined when the call did not produce an answer. */\nexport function parseAnswer(result: ToolResultMessage | undefined): { readonly answer: string } | { readonly failure: string } | undefined {\n if (!result) return undefined;\n // Pi appends its diagnostics to a result as a `<harness>` block of their own, for the model: never for the person.\n const text = result.content.map(part => part.type === 'text' && !/^<harness>[\\s\\S]*<\\/harness>$/.test(part.text) ? part.text : '').join('');\n if (!result.isError) {\n try {\n const value: unknown = JSON.parse(text);\n if (value !== null && typeof value === 'object' && (value as { kind?: unknown }).kind === 'answered' && typeof (value as { answer?: unknown }).answer === 'string') return { answer: (value as { answer: string }).answer };\n } catch { /* fall through to the failure text */ }\n }\n return { failure: text };\n}\n\n\n/**\n * Human-in-the-loop question: writing (still generated, not answerable), pending (options and free text), then answered with the chosen\n * answer. With `questions` it is one form: each question has its option chips and a free-text field, and Answer sends one text\n * (`formAnswer`). It needs only a tool call and an `answer` callback, so a host can show it outside the chat too (for example in a\n * popover answering a request of its own); wrap it in `ChatTextProvider` for the chat's words there.\n */\nexport function QuestionCard({ call, result, answer, questionId, live = false }: QuestionCardProps) {\n const { labels } = useChatText();\n const question = parseQuestion(call);\n const settled = parseAnswer(result);\n const [sent, setSent] = useState<string | undefined>();\n const [sending, setSending] = useState<string | undefined>();\n const [refusal, setRefusal] = useState<string | undefined>();\n const [text, setText] = useState('');\n const [values, setValues] = useState<readonly string[]>([]);\n const formId = useId();\n const setValue = (i: number, value: string) => setValues(previous => question.items.map((_, j) => j === i ? value : previous[j] ?? ''));\n const mounted = useRef(true);\n useEffect(() => { mounted.current = true; return () => { mounted.current = false; }; }, []);\n const current = settled && 'answer' in settled ? settled.answer : sent;\n // `pending` means the question waits on the person; `writing` means the model is still generating it (options may be partial).\n const state = settled ? ('failure' in settled ? 'failed' : 'answered') : sent !== undefined ? 'sent' : live ? 'writing' : 'pending';\n const open = state === 'pending' || state === 'writing';\n\n async function submit(value: string) {\n const trimmed = value.trim();\n if (!answer || !questionId || live || !trimmed || sending !== undefined) return;\n setRefusal(undefined); setSending(trimmed);\n try {\n const outcome = await answer(questionId, trimmed);\n if (outcome.kind === 'answered') setSent(trimmed);\n else setRefusal(outcome.reason || labels.answerRefused(outcome.kind));\n } catch (cause) { setRefusal(cause instanceof Error ? cause.message : labels.answerNotSent); }\n finally { if (mounted.current) setSending(undefined); }\n }\n const onSubmit = (event: FormEvent) => { event.preventDefault(); void submit(text); };\n const onSubmitForm = (event: FormEvent) => { event.preventDefault(); if (values.some(value => value.trim())) void submit(formAnswer(question.items, values, labels.skipped)); };\n const busy = sending !== undefined || live;\n\n const unavailable = !answer || !questionId || busy;\n // `m-0`: a host without a global preflight keeps the paragraph margin of ItemDescription.\n const wrap = 'm-0 line-clamp-none [overflow-wrap:anywhere]';\n\n if (!open) {\n const values = question.items.length > 0 && current !== undefined ? splitFormAnswer(question.items, current) : undefined;\n return <Item role=\"group\" data-testid=\"question-card\" data-state={state} data-call-id={call.id} aria-label={labels.question}>\n <ItemMedia variant=\"icon\">\n {state === 'answered' ? <CheckIcon role=\"img\" aria-label={labels.answered} /> : state === 'sent' ? <Spinner aria-label={labels.waitingForAgent} /> : <CircleSlashIcon role=\"img\" aria-label={labels.notAnswered} />}\n </ItemMedia>\n <ItemContent className=\"min-w-0\">\n {state === 'failed' && <ItemDescription data-testid=\"question-text\" className={wrap}>{question.question || labels.askedQuestion}</ItemDescription>}\n {values ? <div className=\"flex min-w-0 flex-col gap-2\">{question.items.map((item, i) => <div key={i} data-testid=\"question-answer-item\" className=\"flex min-w-0 flex-col gap-1\">\n <ItemDescription className={wrap}>{item.question}</ItemDescription>\n {values[i] === labels.skipped\n ? <ItemDescription data-testid=\"question-answer\" data-skipped=\"true\" className={wrap}>{values[i]}</ItemDescription>\n : <ItemTitle data-testid=\"question-answer\" className={wrap}>{values[i]}</ItemTitle>}\n </div>)}</div>\n : current !== undefined && <ItemTitle className={wrap}><span className=\"sr-only\">{labels.yourAnswer} </span><span data-testid=\"question-answer\" className=\"whitespace-pre-line\">{current}</span></ItemTitle>}\n {state === 'failed' && settled && 'failure' in settled && <ItemDescription role=\"alert\" className={wrap}>{settled.failure || labels.questionNotAnswered}</ItemDescription>}\n </ItemContent>\n </Item>;\n }\n\n const option = (value: string, selected: boolean, onClick: () => void) => <Button key={value} variant={selected ? 'secondary' : 'outline'} data-testid=\"question-option\" data-option={value}\n aria-pressed={question.items.length > 0 ? selected : undefined} disabled={unavailable} onClick={onClick} className=\"h-auto min-h-8 w-full justify-start whitespace-normal text-start\">\n {selected && <CheckIcon data-icon=\"inline-start\" aria-hidden=\"true\" />}{value}\n </Button>;\n\n return <Card role=\"group\" data-testid=\"question-card\" data-state={state} data-call-id={call.id} aria-label={labels.question}>\n <CardHeader>\n <CardDescription>{labels.questionForYou}</CardDescription>\n <CardTitle data-testid=\"question-text\" className=\"[overflow-wrap:anywhere]\">{question.question || labels.askedQuestion}</CardTitle>\n </CardHeader>\n <CardContent className=\"flex flex-col gap-4\">\n {question.items.length > 0 ? <form data-testid=\"question-form\" onSubmit={onSubmitForm} className=\"flex flex-col gap-4\">\n {question.items.map((item, i) => <div key={i} role=\"group\" aria-labelledby={`${formId}-${i}`} data-testid=\"question-item\" className=\"flex min-w-0 flex-col gap-2\">\n <p id={`${formId}-${i}`} className=\"m-0 [overflow-wrap:anywhere]\">{i + 1}. {item.question}</p>\n {item.options.length > 0 && <div role=\"group\" aria-label={labels.answerOptions} className=\"flex flex-col gap-2\">\n {item.options.map(value => option(value, values[i] === value, () => setValue(i, values[i] === value ? '' : value)))}\n </div>}\n <Input data-testid=\"question-input\" value={item.options.includes(values[i] ?? '') ? '' : values[i] ?? ''} maxLength={2000} disabled={unavailable}\n placeholder={item.options.length ? labels.ownAnswer : labels.typeAnswer} aria-label={`${labels.yourAnswerInput} ${i + 1}`} onChange={event => setValue(i, event.currentTarget.value)} />\n </div>)}\n <div><Button type=\"submit\" data-testid=\"question-submit\" disabled={unavailable || !values.some(value => value.trim())}><SendIcon data-icon=\"inline-start\" aria-hidden=\"true\" />{labels.answer}</Button></div>\n </form> : <>\n {question.options.length > 0 && <div role=\"group\" aria-label={labels.answerOptions} className=\"flex flex-col gap-2\">\n {question.options.map(value => option(value, false, () => { void submit(value); }))}\n </div>}\n {question.allowFreeText && <form onSubmit={onSubmit} className=\"flex gap-2\">\n <Input data-testid=\"question-input\" value={text} maxLength={2000} disabled={unavailable} placeholder={question.options.length ? labels.ownAnswer : labels.typeAnswer}\n aria-label={labels.yourAnswerInput} onChange={event => setText(event.currentTarget.value)} />\n <Button type=\"submit\" data-testid=\"question-submit\" disabled={unavailable || !text.trim()}><SendIcon data-icon=\"inline-start\" aria-hidden=\"true\" />{labels.answer}</Button>\n </form>}\n </>}\n {live && <CardDescription>{labels.stillWritingQuestion}</CardDescription>}\n {!answer && !live && <CardDescription>{labels.answeringUnavailable}</CardDescription>}\n {refusal && <Alert variant=\"destructive\" data-testid=\"question-refusal\"><AlertDescription>{refusal}</AlertDescription></Alert>}\n </CardContent>\n </Card>;\n}\n",
|
|
18
18
|
"type": "registry:component",
|
|
19
19
|
"target": "components/pi-chat-question/question-card.tsx"
|
|
20
20
|
}
|
package/r/pi-chat-slash.json
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
"title": "Pi chat slash menu",
|
|
5
5
|
"description": "The composer's `/` menu of commands and skills (and the Commands entry of the + menu). Plug it in with `slash={{ ...config, Picker: SlashPicker }}`; without it pi-chat has no `/` menu.",
|
|
6
6
|
"dependencies": [
|
|
7
|
-
"@hachej/boring-ui-kit@^0.4.
|
|
7
|
+
"@hachej/boring-ui-kit@^0.4.54",
|
|
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.
|
|
12
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.54/r/pi-chat.json"
|
|
13
13
|
],
|
|
14
14
|
"files": [
|
|
15
15
|
{
|
package/r/pi-chat-subagent.json
CHANGED
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.54",
|
|
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.54/r/utils.json",
|
|
13
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.54/r/pi-chat.json"
|
|
14
14
|
],
|
|
15
15
|
"files": [
|
|
16
16
|
{
|
package/r/pi-chat.json
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
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.
|
|
7
|
+
"@hachej/boring-ui-kit@^0.4.54",
|
|
8
|
+
"@hachej/boring-files@^0.4.54",
|
|
9
9
|
"react@^19.0.0",
|
|
10
10
|
"react-dom@^19.0.0",
|
|
11
11
|
"@base-ui/react@1.9.0",
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
"zod@^4.6.5"
|
|
19
19
|
],
|
|
20
20
|
"registryDependencies": [
|
|
21
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
22
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
21
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.54/r/utils.json",
|
|
22
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.54/r/button.json"
|
|
23
23
|
],
|
|
24
24
|
"files": [
|
|
25
25
|
{
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
},
|
|
109
109
|
{
|
|
110
110
|
"path": "registry/pi-chat/session.tsx",
|
|
111
|
-
"content": "'use client';\n\nimport { useCallback, useMemo, useRef } from 'react';\nimport type { KeyboardEvent, ReactNode } from 'react';\nimport type { EntryRecord, JsonObject } from '@earendil-works/pi-durable';\nimport type { ImageContent, ToolCall, ToolResultMessage } from '@earendil-works/pi-ai';\nimport { PAGE_FIRST, PAGE_MORE, useTranscriptWindow } from '@hachej/boring-ui-kit/chat-react';\nimport type { ChatPaging } from '@hachej/boring-ui-kit/chat-react';\nimport type { ChatHistoryState } from '@hachej/boring-ui-kit/native-chat';\nimport type { ChatController } from '@hachej/boring-ui-kit/native-chat';\nimport { useMessageScroller, useMessageScrollerScrollable } from '@/components/ui/message-scroller';\nimport { Button } from '@/components/ui/button';\nimport type { ComposerFeedback, ComposerProps } from './composer';\nimport type { AttachmentsConfig, EffortConfig, MentionsConfig, ModelConfig, SentContent, SlashConfig } from './config';\nimport type { PasteToFile } from '@hachej/boring-ui-kit/chat-text';\nimport type { CommandMentions } from './config';\nimport type { ChatCards, ChatTextPart, RowContext } from './message';\nimport type { QueuedMessage } from './queue';\nimport type { AnswerOutcome, ChatArtifactsConfig } from './config';\nimport type { ChatCard, Mode } from './rows';\nimport { useChatText } from './labels';\nimport type { BlockAction } from '../button/actions';\nimport type { ReplyRef } from './config';\n\n/** What the host lets the person do beyond typing. Pass `remote.answer` and `remote.withdraw` from `createRemoteChat()`. */\nexport interface PiChatActions {\n /** Resolve a pending `ask_user` question. Without it, question cards are read-only. */\n readonly answer?: (questionId: string, answer: string) => Promise<AnswerOutcome>;\n /** Withdraw a queued message before Pi places it. Without it, queued messages cannot be cancelled. */\n readonly withdraw?: (id: QueuedMessage['id']) => Promise<unknown>;\n}\n\n/** The props `PiChat` and `AmbientChat` both take and hand to `useChatSession`. */\nexport interface ChatFeatureProps {\n /** `expert` hides successful tool details and reasoning; `developer` shows everything. */\n readonly mode?: Mode;\n /** `natural` shows only messages, replies and question, approval and artifact cards, with one \"Preparing…\" status while the agent works: no tool steps, reasoning, file discovery or raw error text. Omit it and nothing changes. */\n readonly presentation?: 'natural';\n /** Show a retained voice message's audio player. Default false: transcript only; uploaded audio and stored tokens are unchanged. */\n readonly voiceReplay?: boolean;\n /** The words on the chip of a sent message that starts with a `<target>` block (point-and-speak). Undefined keeps the default label; the chip never shows the raw fields on hover. */\n readonly targetLabel?: (target: JsonObject) => string | undefined;\n /** A host's own card for a sent text. */\n readonly sentContent?: SentContent;\n readonly actions?: PiChatActions;\n readonly renderEntry?: (entry: EntryRecord) => ChatCard | undefined;\n /** Omit complete assistant lines from display, copy and actions. Live trailing fragments wait for a newline or settlement. Not transport redaction. */\n readonly hideLine?: (line: string) => boolean;\n /** Initial number of displayed rows (default 60). An explicit limit uses Show earlier, not automatic paging; nothing is deleted. */\n readonly displayLimit?: number;\n /** Automatic paging: rows shown first (default 60) and rows added each time the reader reaches the top (default 40). Ignored with `displayLimit`. */\n readonly paging?: ChatPaging;\n /** Explicitly enable messageActions for paged history too; other historical controls remain inert. */\n readonly historicalMessageActions?: boolean;\n /** Host-rendered unread indicator, keyed by the reply. The host owns seen state and its persistence. */\n readonly unread?: (reply: ReplyRef) => ReactNode;\n /** Message inside the single native response-error notice. Null/undefined keeps the default; native status and metadata stay unchanged. */\n readonly replyError?: (reply: ReplyRef) => ReactNode;\n /** Card for any tool call; the catch-all after `toolRenderers`. */\n readonly renderTool?: (call: ToolCall, result: ToolResultMessage | undefined) => ChatCard | undefined;\n /** Cards for tool calls, keyed by tool name (same contract as `renderTool`). An entry that returns undefined falls back to `renderTool`. */\n readonly toolRenderers?: Readonly<Record<string, (call: ToolCall, result: ToolResultMessage | undefined) => ChatCard | undefined>> | undefined;\n /** Draws an assistant text part. Default: plain text (`markdownText` of `pi-chat-markdown` draws Markdown). */\n readonly renderText?: ((part: ChatTextPart) => ReactNode) | undefined;\n /**\n * The question, approval and sub-agent cards (`pi-chat-question`, `pi-chat-approval`, `pi-chat-subagent`; `withChatPlugins` adds them all).\n * Without a question or approval card the chat shows that it waits for the person's answer, with no answer UI; without the sub-agent card a\n * sub-agent call is an ordinary tool step.\n */\n readonly cards?: ChatCards | undefined;\n readonly groupTool?: (call: ToolCall, result: ToolResultMessage) => boolean;\n readonly commandMentions?: CommandMentions;\n readonly onOpenImage?: (image: ImageContent) => void;\n readonly onCopy?: (text: string) => Promise<void>;\n readonly onComposerKeyDown?: (event: KeyboardEvent<HTMLTextAreaElement>) => void;\n readonly fileAccept?: string;\n /** Optional pills at the left of the composer's controls row. */\n readonly composerChips?: ComposerProps['chips'];\n /** Contextual host content above the input, for example a removable quote. */\n readonly composerHeader?: ReactNode;\n /** Host-owned content participates in send readiness without native image attachments. */\n readonly composerHasContent?: boolean;\n /** Retain files for the host's send operation instead of uploading immediately. */\n readonly onPickFiles?: (files: readonly File[]) => void;\n /** `/` menu of host commands and skills. Omit it and `/` is just text. */\n readonly slash?: SlashConfig;\n /** Show successful native write/edit paths as file cards under each reply. Off by default: only hosts with workspace file tools should opt in. */\n readonly fileChanges?: boolean;\n /** Opens a file from an upload or write/edit card. Falls back to mentions.open. */\n readonly onOpenFile?: (path: string) => void;\n /** Pastes above these limits become a file through `attachments` (on by default, 2000 characters or 40 lines). Needs `attachments`. */\n readonly pasteToFile?: PasteToFile;\n /** `@` workspace file mentions: a picker, composer chips and styled mentions in sent messages. */\n readonly mentions?: MentionsConfig;\n /** Attach button, paste and drop through the host's upload. Takes the place of `onFiles` and `fileAccept`. */\n readonly attachments?: AttachmentsConfig;\n /** Model picker in the composer bar. The current model comes from the view's `pi.agent` document. */\n readonly model?: ModelConfig;\n /** Thinking level picker in the composer bar. The current level comes from the view's `pi.agent` document. */\n readonly effort?: EffortConfig;\n /**\n * Artifact cards: tool results that carry a descriptor become a card that opens the artifact in the host's panel. The card and its recogniser\n * come from `pi-artifacts` (`withChatPlugins` adds them); without them, or without this prop, no artifact is offered.\n */\n readonly artifacts?: ChatArtifactsConfig;\n /**\n * Host actions under each settled reply, beside Copy (and Fork): `header` ones as buttons, `menu` ones in a \"…\" menu. Test ids\n * `reply-action-<id>`, the menu `reply-action-more`.\n */\n readonly messageActions?: ((reply: ReplyRef) => readonly BlockAction[]) | undefined;\n /** A line under each settled reply (for example the model that answered and the time it took), for the host to show to some people only. */\n readonly replyFooter?: ((reply: ReplyRef) => ReactNode) | undefined;\n}\n\n\n// Compatibility type export for one release.\nexport type { FilesHandler } from '@hachej/boring-ui-kit/chat-react';\nimport { useChatSession as useHeadlessChatSession } from '@hachej/boring-ui-kit/chat-react';\nimport type { FilesHandler } from '@hachej/boring-ui-kit/chat-react';\n\n/** The chat behaviour every surface over a `NativeChatController` shares (`PiChat`, `AmbientChat`): sending, queue actions, uploads, pickers, rows. */\nexport interface ChatSessionOptions extends Readonly<{ [Key in keyof ChatFeatureProps]: ChatFeatureProps[Key] | undefined }> {\n readonly controller: ChatController;\n /** The controller the host currently passes; a surface that outlives its controller stops acting on the old one. */\n readonly activeController: { readonly current: ChatController };\n readonly mode: Mode;\n readonly fileAccept: string;\n readonly onFiles?: FilesHandler | undefined;\n /** The phone camera for an `onFiles` chat (`attachments.Camera` is used with `attachments`). */\n readonly camera?: AttachmentsConfig['Camera'];\n /** Runs after a message was accepted for sending (for example to scroll to the bottom). */\n readonly afterSend?: (() => void) | undefined;\n /** Extra attributes of the row context a surface needs (for example per-reply actions). */\n readonly rowExtras?: Partial<RowContext> | undefined;\n /** The optional annotate toggle: when notes are pending (`count` > 0), Send attaches them to the message first. */\n readonly feedback?: ComposerFeedback | undefined;\n readonly sendContext?: import('@hachej/boring-ui-kit/chat-react').ChatSessionFeedback | undefined;\n}\n\n/** Presentation adapter; all session state and controller protocols live in the kit. */\nexport function useChatSession(options: ChatSessionOptions) {\n const { controller, activeController, mode, presentation, voiceReplay, targetLabel, sentContent, actions, renderEntry, hideLine, historicalMessageActions, unread, replyError, renderTool, toolRenderers, renderText, cards, groupTool, commandMentions, onOpenImage, onCopy, onComposerKeyDown, onFiles, fileAccept,\n slash, mentions, attachments, model, effort, artifacts, messageActions, replyFooter, afterSend, rowExtras, feedback, sendContext, onOpenFile, fileChanges, composerChips, composerHeader, composerHasContent, onPickFiles, pasteToFile } = options;\n const { labels } = useChatText();\n const textarea = useRef<HTMLTextAreaElement | null>(null);\n const offered = artifacts?.recognise && artifacts.Card ? artifacts : undefined;\n const sessionArtifacts = useMemo(() => offered?.recognise ? { recognise: offered.recognise, detect: offered.detect, open: offered.open } : undefined, [offered]);\n const session = useHeadlessChatSession<ReactNode>({ controller, activeController, mode, presentation, actions, renderEntry, hideLine, renderTool, toolRenderers,\n groupTool, subagent: cards?.subagent, onOpenImage, onFiles, mentions, slash, attachments, model, effort,\n artifacts: sessionArtifacts,\n afterSend, feedback: sendContext ?? feedback, onOpenFile, onPickFiles, commandMentions, labels, onFocus: () => textarea.current?.focus() });\n const { state, developer, natural, connected, working, sendBlocked, act } = session;\n const rowContext = useMemo<RowContext>(() => ({\n developer, natural, voiceReplay, targetLabel, sentContent, renderText, cards, groupTool, onOpenImage, onCopy, openFile: onOpenFile ?? mentions?.open, fileChanges,\n commandMentions: commandMentions ? { commands: commandMentions.commands, onActivate: session.activateMention } : undefined,\n answer: actions?.answer,\n artifacts: offered?.Card ? { open: offered.open, isOpen: offered.isOpen, kinds: offered.kinds, Card: offered.Card } : undefined,\n messageActions, historicalMessageActions, unread, replyError,\n replyFooter,\n pieces: slash?.skills?.length || mentions || attachments ? { kinds: mentions?.kinds, mentions: Boolean(mentions), uploads: Boolean(attachments), skills: (slash?.skills ?? []).map(skill => skill.name), openMention: mentions?.open, renderMention: mentions?.renderSent ?? mentions?.render, preview: mentions?.preview, audioUrl: mentions?.audioUrl } : undefined,\n ...rowExtras,\n // eslint-disable-next-line react-hooks/exhaustive-deps -- lists the fields rows read, not whole objects: activateMention changes every render and must not rebuild every row.\n }), [controller, developer, natural, voiceReplay, targetLabel, sentContent, renderText, cards, groupTool, onOpenImage, onCopy, onOpenFile, fileChanges, commandMentions, actions?.answer, slash?.skills, mentions, mentions?.open, mentions?.render, mentions?.renderSent, mentions?.preview, mentions?.audioUrl, attachments, offered?.open, offered?.isOpen, offered?.kinds, offered?.Card, messageActions, historicalMessageActions, unread, replyError, replyFooter, rowExtras]);\n\n /** The model-and-effort pill and the note that reports a refused change. */\n const pickers = () => {\n const disabled = session.changing !== null || state.disposed || !connected;\n // The pill is drawn by the component the config carries (`pi-chat-pickers`); without one there is no pill.\n const Picker = model?.Picker ?? effort?.Picker;\n const barStart = (model || effort) && Picker ? <Picker model={model} effort={effort} currentModel={session.currentModel} currentEffort={session.currentEffort} disabled={disabled} busy={session.changing}\n onModel={next => void session.changeModel(next)}\n onEffort={next => void session.changeEffort(next)} /> : undefined;\n const barNote = session.changeError ? <Button variant=\"ghost\" size=\"sm\" role=\"alert\" data-testid=\"configure-error\" title={`${session.changeError} (${labels.dismissHint})`} onClick={session.dismissChangeError}\n className=\"min-w-0 max-w-[16rem]\"><span className=\"truncate\">{session.changeError}</span></Button> : undefined;\n return { barStart, barNote };\n };\n\n const camera = attachments ? attachments.Camera : options.camera;\n /** Everything `Composer` needs from the session; a surface adds its layout and slots. */\n const composer = {\n text: state.draft.text, onText: (text: string) => controller.setText(text), onKeyDown: onComposerKeyDown, textareaRef: textarea,\n attachments: state.draft.attachments.map(item => ({ id: item.id, name: item.name, mimeType: item.content.mimeType, data: item.content.data })),\n onRemoveAttachment: (id: string) => controller.setAttachments(controller.getSnapshot().draft.attachments.filter(item => item.id !== id)),\n working, sendBlocked, disabled: state.disposed, stopRequested: state.stop === 'requested',\n onSend: session.send, onStop: () => act(controller.stop),\n hasFiles: Boolean(composerHasContent),\n uploading: session.uploading > 0, canAttach: Boolean(onPickFiles || onFiles || attachments), ...(camera ? { camera } : {}), fileAccept: attachments ? attachments.accept ?? '' : fileAccept, onPickFiles: (files: readonly File[]) => { void session.upload(files); },\n slash, onSlashError: session.report, mentions, mentionPaths: session.picked, onMentionPicked: session.pickMention,\n uploads: session.pending, onDismissUpload: session.dismissUpload,\n onPasteText: attachments ? session.pasteText : undefined, pasteToFile,\n quote: composerHeader,\n feedback, chips: composerChips,\n };\n\n return { ...session, textarea, rowContext, composer, pickers };\n}\n\nexport type ChatSession = ReturnType<typeof useChatSession>;\n\n// Compatibility exports for one release; MessageScroller remains the only scroll owner.\nexport { chunkKeys, useRowChunks, PAGE_FIRST, PAGE_MORE, CHUNK } from '@hachej/boring-ui-kit/chat-react';\nexport type { ChatPaging } from '@hachej/boring-ui-kit/chat-react';\n\n/** Thin presentation adapter to the host's shadcn MessageScroller. */\nexport function useTranscript(rows: readonly { readonly key: string }[], page = PAGE_FIRST, more = PAGE_MORE, history?: { readonly state: ChatHistoryState; readonly connected: boolean; readonly load: () => Promise<void> }, automatic = true) {\n const window = useTranscriptWindow(rows, page, more, history, automatic);\n const { scrollToEnd } = useMessageScroller();\n const scrollable = useMessageScrollerScrollable();\n const scrollToBottom = useCallback((behavior?: 'instant') => scrollToEnd({ behavior: behavior === 'instant' ? 'instant' : 'smooth' }), [scrollToEnd]);\n return { ...window, stick: { scrollRef: window.scrollRef, scrollToBottom, isAtBottom: !scrollable.end } };\n}\n",
|
|
111
|
+
"content": "'use client';\n\nimport { useCallback, useMemo, useRef } from 'react';\nimport type { KeyboardEvent, ReactNode } from 'react';\nimport type { EntryRecord, JsonObject } from '@earendil-works/pi-durable';\nimport type { ImageContent, ToolCall, ToolResultMessage } from '@earendil-works/pi-ai';\nimport { PAGE_FIRST, PAGE_MORE, useTranscriptWindow } from '@hachej/boring-ui-kit/chat-react';\nimport type { ChatPaging } from '@hachej/boring-ui-kit/chat-react';\nimport type { ChatHistoryState } from '@hachej/boring-ui-kit/native-chat';\nimport type { ChatController } from '@hachej/boring-ui-kit/native-chat';\nimport { useMessageScroller, useMessageScrollerScrollable } from '@/components/ui/message-scroller';\nimport { Button } from '@/components/ui/button';\nimport type { ComposerFeedback, ComposerProps } from './composer';\nimport type { AttachmentsConfig, EffortConfig, MentionsConfig, ModelConfig, SentContent, SlashConfig } from './config';\nimport type { PasteToFile } from '@hachej/boring-ui-kit/chat-text';\nimport type { CommandMentions } from './config';\nimport type { ChatCards, ChatTextPart, RowContext } from './message';\nimport type { QueuedMessage } from './queue';\nimport type { AnswerOutcome, ChatArtifactsConfig } from './config';\nimport type { ChatCard, Mode } from './rows';\nimport { useChatText } from './labels';\nimport type { BlockAction } from '../button/actions';\nimport type { ReplyRef } from './config';\n\n/** What the host lets the person do beyond typing. Pass `remote.answer` and `remote.withdraw` from `createRemoteChat()`. */\nexport interface PiChatActions {\n /** Resolve a pending `ask_user` question. Without it, question cards are read-only. */\n readonly answer?: (questionId: string, answer: string) => Promise<AnswerOutcome>;\n /** Withdraw a queued message before Pi places it. Without it, queued messages cannot be cancelled. */\n readonly withdraw?: (id: QueuedMessage['id']) => Promise<unknown>;\n}\n\n/** The props `PiChat` and `AmbientChat` both take and hand to `useChatSession`. */\nexport interface ChatFeatureProps {\n /** `expert` hides successful tool details and reasoning; `developer` shows everything. */\n readonly mode?: Mode;\n /** `natural` shows only messages, replies and question, approval and artifact cards, with one \"Preparing…\" status while the agent works: no tool steps, reasoning, file discovery or raw error text. Omit it and nothing changes. */\n readonly presentation?: 'natural';\n /** Show a retained voice message's audio player. Default false: transcript only; uploaded audio and stored tokens are unchanged. */\n readonly voiceReplay?: boolean;\n /** The words on the chip of a sent message that starts with a `<target>` block (point-and-speak). Undefined keeps the default label; the chip never shows the raw fields on hover. */\n readonly targetLabel?: (target: JsonObject) => string | undefined;\n /** A host's own card for a sent text. */\n readonly sentContent?: SentContent;\n readonly actions?: PiChatActions;\n readonly renderEntry?: (entry: EntryRecord) => ChatCard | undefined;\n /** Omit complete assistant lines from display, copy and actions. Live trailing fragments wait for a newline or settlement. Not transport redaction. */\n readonly hideLine?: (line: string) => boolean;\n /** Initial number of displayed rows (default 60). An explicit limit uses Show earlier, not automatic paging; nothing is deleted. */\n readonly displayLimit?: number;\n /** Automatic paging: rows shown first (default 60) and rows added each time the reader reaches the top (default 40). Ignored with `displayLimit`. */\n readonly paging?: ChatPaging;\n /** Explicitly enable messageActions for paged history too; other historical controls remain inert. */\n readonly historicalMessageActions?: boolean;\n /** Host-rendered unread indicator, keyed by the reply. The host owns seen state and its persistence. */\n readonly unread?: (reply: ReplyRef) => ReactNode;\n /** Message inside the single native response-error notice. Null/undefined keeps the default; native status and metadata stay unchanged. */\n readonly replyError?: (reply: ReplyRef) => ReactNode;\n /** Card for any tool call; the catch-all after `toolRenderers`. */\n readonly renderTool?: (call: ToolCall, result: ToolResultMessage | undefined) => ChatCard | undefined;\n /** Cards for tool calls, keyed by tool name (same contract as `renderTool`). An entry that returns undefined falls back to `renderTool`. */\n readonly toolRenderers?: Readonly<Record<string, (call: ToolCall, result: ToolResultMessage | undefined) => ChatCard | undefined>> | undefined;\n /** Draws an assistant text part. Default: plain text (`markdownText` of `pi-chat-markdown` draws Markdown). */\n readonly renderText?: ((part: ChatTextPart) => ReactNode) | undefined;\n /**\n * The question, approval and sub-agent cards (`pi-chat-question`, `pi-chat-approval`, `pi-chat-subagent`; `withChatPlugins` adds them all).\n * Without a question or approval card the chat shows that it waits for the person's answer, with no answer UI; without the sub-agent card a\n * sub-agent call is an ordinary tool step.\n */\n readonly cards?: ChatCards | undefined;\n readonly groupTool?: (call: ToolCall, result: ToolResultMessage) => boolean;\n readonly commandMentions?: CommandMentions;\n readonly onOpenImage?: (image: ImageContent) => void;\n readonly onCopy?: (text: string) => Promise<void>;\n readonly onComposerKeyDown?: (event: KeyboardEvent<HTMLTextAreaElement>) => void;\n readonly fileAccept?: string;\n /** Optional pills at the left of the composer's controls row. */\n readonly composerChips?: ComposerProps['chips'];\n /** Contextual host content above the input, for example a removable quote. */\n readonly composerHeader?: ReactNode;\n /** Host-owned content participates in send readiness without native image attachments. */\n readonly composerHasContent?: boolean;\n /** Retain files for the host's send operation instead of uploading immediately. */\n readonly onPickFiles?: (files: readonly File[]) => void;\n /** `/` menu of host commands and skills. Omit it and `/` is just text. */\n readonly slash?: SlashConfig;\n /** Show successful native write/edit paths as file cards under each reply. Off by default: only hosts with workspace file tools should opt in. */\n readonly fileChanges?: boolean;\n /** Opens a file from an upload or write/edit card. Falls back to mentions.open. */\n readonly onOpenFile?: (path: string) => void;\n /** Pastes above these limits become a file through `attachments` (on by default, 2000 characters or 40 lines). Needs `attachments`. */\n readonly pasteToFile?: PasteToFile;\n /** `@` workspace file mentions: a picker, composer chips and styled mentions in sent messages. */\n readonly mentions?: MentionsConfig;\n /** Attach button, paste and drop through the host's upload. Takes the place of `onFiles` and `fileAccept`. */\n readonly attachments?: AttachmentsConfig;\n /** Model picker in the composer bar. The current model comes from the view's `pi.agent` document. */\n readonly model?: ModelConfig;\n /** Thinking level picker in the composer bar. The current level comes from the view's `pi.agent` document. */\n readonly effort?: EffortConfig;\n /**\n * Artifact cards: tool results that carry a descriptor become a card that opens the artifact in the host's panel. The card and its recogniser\n * come from `pi-artifacts` (`withChatPlugins` adds them); without them, or without this prop, no artifact is offered.\n */\n readonly artifacts?: ChatArtifactsConfig;\n /**\n * Host actions under each settled reply, beside Copy (and Fork): `header` ones as buttons, `menu` ones in a \"…\" menu. Test ids\n * `reply-action-<id>`, the menu `reply-action-more`.\n */\n readonly messageActions?: ((reply: ReplyRef) => readonly BlockAction[]) | undefined;\n /** A line under each settled reply (for example the model that answered and the time it took), for the host to show to some people only. */\n readonly replyFooter?: ((reply: ReplyRef) => ReactNode) | undefined;\n}\n\n\n// Compatibility type export for one release.\nexport type { FilesHandler } from '@hachej/boring-ui-kit/chat-react';\nimport { useChatSession as useHeadlessChatSession } from '@hachej/boring-ui-kit/chat-react';\nimport type { FilesHandler } from '@hachej/boring-ui-kit/chat-react';\n\n/** The chat behaviour every surface over a `NativeChatController` shares (`PiChat`, `AmbientChat`): sending, queue actions, uploads, pickers, rows. */\nexport interface ChatSessionOptions extends Readonly<{ [Key in keyof ChatFeatureProps]: ChatFeatureProps[Key] | undefined }> {\n readonly controller: ChatController;\n /** The controller the host currently passes; a surface that outlives its controller stops acting on the old one. */\n readonly activeController: { readonly current: ChatController };\n readonly mode: Mode;\n readonly fileAccept: string;\n readonly onFiles?: FilesHandler | undefined;\n /** The phone camera for an `onFiles` chat (`attachments.Camera` is used with `attachments`). */\n readonly camera?: AttachmentsConfig['Camera'];\n /** Runs after a message was accepted for sending (for example to scroll to the bottom). */\n readonly afterSend?: (() => void) | undefined;\n /** Extra attributes of the row context a surface needs (for example per-reply actions). */\n readonly rowExtras?: Partial<RowContext> | undefined;\n /** The optional annotate toggle: when notes are pending (`count` > 0), Send attaches them to the message first. */\n readonly feedback?: ComposerFeedback | undefined;\n readonly sendContext?: import('@hachej/boring-ui-kit/chat-react').ChatSessionFeedback | undefined;\n}\n\n/** Presentation adapter; all session state and controller protocols live in the kit. */\nexport function useChatSession(options: ChatSessionOptions) {\n const { controller, activeController, mode, presentation, voiceReplay, targetLabel, sentContent, actions, renderEntry, hideLine, historicalMessageActions, unread, replyError, renderTool, toolRenderers, renderText, cards, groupTool, commandMentions, onOpenImage, onCopy, onComposerKeyDown, onFiles, fileAccept,\n slash, mentions, attachments, model, effort, artifacts, messageActions, replyFooter, afterSend, rowExtras, feedback, sendContext, onOpenFile, fileChanges, composerChips, composerHeader, composerHasContent, onPickFiles, pasteToFile } = options;\n const { labels } = useChatText();\n const textarea = useRef<HTMLTextAreaElement | null>(null);\n const offered = artifacts?.recognise && artifacts.Card ? artifacts : undefined;\n const sessionArtifacts = useMemo(() => offered?.recognise ? { recognise: offered.recognise, detect: offered.detect, open: offered.open } : undefined, [offered]);\n const session = useHeadlessChatSession<ReactNode>({ controller, activeController, mode, presentation, actions, renderEntry, hideLine, renderTool, toolRenderers,\n groupTool, subagent: cards?.subagent, onOpenImage, onFiles, mentions, slash, attachments, model, effort,\n artifacts: sessionArtifacts,\n afterSend, feedback: sendContext ?? feedback, onOpenFile, onPickFiles, commandMentions, labels, onFocus: () => textarea.current?.focus() });\n const { state, developer, natural, connected, working, sendBlocked, act } = session;\n const rowContext = useMemo<RowContext>(() => ({\n developer, natural, voiceReplay, targetLabel, sentContent, renderText, cards, groupTool, onOpenImage, onCopy, openFile: onOpenFile ?? mentions?.open, fileChanges,\n commandMentions: commandMentions ? { commands: commandMentions.commands, onActivate: session.activateMention } : undefined,\n answer: actions?.answer,\n artifacts: offered?.Card ? { open: offered.open, isOpen: offered.isOpen, kinds: offered.kinds, Card: offered.Card } : undefined,\n messageActions, historicalMessageActions, unread, replyError,\n replyFooter,\n pieces: slash?.skills?.length || mentions || attachments ? { kinds: mentions?.kinds, mentions: Boolean(mentions), uploads: Boolean(attachments), skills: (slash?.skills ?? []).map(skill => skill.name), openMention: mentions?.open, renderMention: mentions?.renderSent ?? mentions?.render, preview: mentions?.preview, audioUrl: mentions?.audioUrl } : undefined,\n ...rowExtras,\n // eslint-disable-next-line react-hooks/exhaustive-deps -- lists the fields rows read, not whole objects: activateMention changes every render and must not rebuild every row.\n }), [controller, developer, natural, voiceReplay, targetLabel, sentContent, renderText, cards, groupTool, onOpenImage, onCopy, onOpenFile, fileChanges, commandMentions, actions?.answer, slash?.skills, mentions, mentions?.open, mentions?.render, mentions?.renderSent, mentions?.preview, mentions?.audioUrl, attachments, offered?.open, offered?.isOpen, offered?.kinds, offered?.Card, messageActions, historicalMessageActions, unread, replyError, replyFooter, rowExtras]);\n\n /** The model-and-effort pill and the note that reports a refused change. */\n const pickers = () => {\n const disabled = session.changing !== null || state.disposed || !connected;\n // The pill is drawn by the component the config carries (`pi-chat-pickers`); without one there is no pill.\n const Picker = model?.Picker ?? effort?.Picker;\n const barStart = (model || effort) && Picker ? <Picker model={model} effort={effort} currentModel={session.currentModel} currentEffort={session.currentEffort} disabled={disabled} busy={session.changing}\n onModel={next => void session.changeModel(next)}\n onEffort={next => void session.changeEffort(next)} /> : undefined;\n const barNote = session.changeError ? <Button variant=\"ghost\" size=\"sm\" role=\"alert\" data-testid=\"configure-error\" title={`${session.changeError} (${labels.dismissHint})`} onClick={session.dismissChangeError}\n className=\"min-w-0 max-w-[16rem]\"><span className=\"truncate\">{session.changeError}</span></Button> : undefined;\n return { barStart, barNote };\n };\n\n const camera = attachments ? attachments.Camera : options.camera;\n /** Everything `Composer` needs from the session; a surface adds its layout and slots. */\n const composer = {\n text: state.draft.text, onText: (text: string) => controller.setText(text), onKeyDown: onComposerKeyDown, textareaRef: textarea,\n attachments: state.draft.attachments.map(item => ({ id: item.id, name: item.name, mimeType: item.content.mimeType, data: item.content.data })),\n onRemoveAttachment: (id: string) => controller.setAttachments(controller.getSnapshot().draft.attachments.filter(item => item.id !== id)),\n // A typed message answers the waiting question, so the primary button sends it.\n working: working && !session.answersQuestion, sendBlocked, disabled: state.disposed, stopRequested: state.stop === 'requested',\n onSend: session.send, onStop: () => act(controller.stop),\n hasFiles: Boolean(composerHasContent),\n uploading: session.uploading > 0, canAttach: Boolean(onPickFiles || onFiles || attachments), ...(camera ? { camera } : {}), fileAccept: attachments ? attachments.accept ?? '' : fileAccept, onPickFiles: (files: readonly File[]) => { void session.upload(files); },\n slash, onSlashError: session.report, mentions, mentionPaths: session.picked, onMentionPicked: session.pickMention,\n uploads: session.pending, onDismissUpload: session.dismissUpload,\n onPasteText: attachments ? session.pasteText : undefined, pasteToFile,\n quote: composerHeader,\n feedback, chips: composerChips,\n };\n\n return { ...session, textarea, rowContext, composer, pickers };\n}\n\nexport type ChatSession = ReturnType<typeof useChatSession>;\n\n// Compatibility exports for one release; MessageScroller remains the only scroll owner.\nexport { chunkKeys, useRowChunks, PAGE_FIRST, PAGE_MORE, CHUNK } from '@hachej/boring-ui-kit/chat-react';\nexport type { ChatPaging } from '@hachej/boring-ui-kit/chat-react';\n\n/** Thin presentation adapter to the host's shadcn MessageScroller. */\nexport function useTranscript(rows: readonly { readonly key: string }[], page = PAGE_FIRST, more = PAGE_MORE, history?: { readonly state: ChatHistoryState; readonly connected: boolean; readonly load: () => Promise<void> }, automatic = true) {\n const window = useTranscriptWindow(rows, page, more, history, automatic);\n const { scrollToEnd } = useMessageScroller();\n const scrollable = useMessageScrollerScrollable();\n const scrollToBottom = useCallback((behavior?: 'instant') => scrollToEnd({ behavior: behavior === 'instant' ? 'instant' : 'smooth' }), [scrollToEnd]);\n return { ...window, stick: { scrollRef: window.scrollRef, scrollToBottom, isAtBottom: !scrollable.end } };\n}\n",
|
|
112
112
|
"type": "registry:component",
|
|
113
113
|
"target": "components/pi-chat/session.tsx"
|
|
114
114
|
},
|
|
@@ -144,13 +144,13 @@
|
|
|
144
144
|
},
|
|
145
145
|
{
|
|
146
146
|
"path": "registry/pi-chat/labels.ts",
|
|
147
|
-
"content": "/*\n * Every word the chat shows, and the icons it draws, as plain data. A host passes a partial `labels` (and `icons`) object to `PiChat`\n * (or `AmbientChat`, `AgentWorkspace`): what it leaves out keeps the default below. Strings that need a value are functions. No i18n\n * framework: an app that translates passes its own object per language.\n */\nimport { createContext, createElement, useContext, useMemo, useRef } from 'react';\nimport type { ReactNode } from 'react';\nimport { ArrowDownIcon, ArrowUpIcon, CameraIcon, GitForkIcon, HistoryIcon, ListEndIcon, MessagesSquareIcon, PaperclipIcon, PlusIcon, RefreshCwIcon, SquareIcon } from 'lucide-react';\nimport type { BlockIcon } from '../button/actions';\nimport { withDefaults } from '../utils/utils';\n\n// An Intl.DateTimeFormat costs far more than formatting with one; every bubble's clock reuses the same few.\nconst formats = new Map<string, Intl.DateTimeFormat>();\nfunction intlFormat(locale: string, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat {\n const key = `${locale}|${JSON.stringify(options)}`;\n let found = formats.get(key);\n if (!found) { found = new Intl.DateTimeFormat(locale, options); formats.set(key, found); }\n return found;\n}\n/** « Tue 22 Oct, 14:02 ». */\nexport function formatTimestamp(at: string, locale: string) {\n const date = new Date(at);\n if (!Number.isFinite(date.getTime())) return at;\n return `${intlFormat(locale, { weekday: 'short', day: 'numeric', month: 'short' }).format(date)}, ${intlFormat(locale, { hour: '2-digit', minute: '2-digit' }).format(date)}`;\n}\n/** The time inside a bubble (« 14:02 », « 2:02 PM »). */\nexport function formatClock(at: string, locale: string) {\n const date = new Date(at);\n return Number.isFinite(date.getTime()) ? intlFormat(locale, { hour: 'numeric', minute: '2-digit' }).format(date) : '';\n}\n\nconst TOOL_VERBS: Readonly<Record<string, string>> = { read: 'Reading', write: 'Writing', edit: 'Editing', ls: 'Listing', list_files: 'Listing', find: 'Searching', grep: 'Searching', load_skill: 'Loading skill', bash: 'Running bash' };\nconst TOOL_NOUNS: Readonly<Record<string, string>> = { bash: 'command', read: 'read', write: 'write', edit: 'edit', find: 'find', grep: 'search', ls: 'list' };\n\nconst EFFORT_NAMES: Readonly<Record<string, string>> = { off: 'Off', minimal: 'Minimal', low: 'Low', medium: 'Medium', high: 'High', xhigh: 'Extra high', max: 'Max' };\nconst EFFORT_DETAILS: Readonly<Record<string, string>> = { off: 'No extra reasoning', minimal: 'Barely any reasoning', low: 'Light reasoning', medium: 'Balanced reasoning', high: 'Deep reasoning', xhigh: 'Deeper reasoning', max: 'Maximum reasoning' };\n\nexport const defaultChatLabels = {\n // Header and transcript\n /** The chat's name: the header title, the region's name and the empty state's title. */\n title: 'Conversation',\n history: 'History',\n artifacts: 'Artifacts',\n search: 'Search',\n live: 'Live',\n connecting: 'Connecting…',\n reconnecting: 'Reconnecting…',\n loadFailed: 'Conversation could not be loaded.',\n disconnected: 'Disconnected. Last observed messages remain visible.',\n reconnect: 'Reconnect',\n requiredActions: 'Required conversation actions',\n messages: 'Messages',\n showEarlier: 'Show earlier messages',\n loading: 'Loading conversation…',\n emptyDescription: 'Start a conversation.',\n jumpToLatest: 'Jump to latest',\n agentWorking: 'Agent is working',\n waitingForAnswer: 'Waiting for your answer',\n retryPending: 'Native retry pending',\n compacting: 'Native context compaction in progress',\n /** The one status of `presentation=\"natural\"` while the agent works (any tool, retry or compaction), instead of the activity wording above. */\n preparing: 'Preparing…',\n /** In `presentation=\"natural\"`, replaces the controller's own reason when a send is blocked. */\n sendUnavailable: 'Sending is not available right now.',\n /** The \"…\" menu of host actions (header, replies). */\n moreActions: 'More actions',\n // Notices\n sendUnknown: 'Submission acknowledgement is unknown. The message is kept until you check or retry it.',\n checkSubmission: 'Check original submission',\n retrySame: 'Retry same request',\n stopRequested: 'Stop requested. Waiting for native confirmation.',\n stopUnconfirmed: 'Stop is unconfirmed. Work may still be running.',\n steerRefused: 'The message could not be steered; it is back in the message box.',\n tooLate: 'Too late: that message has already started.',\n tooLateToRemove: 'Too late to remove: that message has already started.',\n queueFailed: 'The message could not be changed.',\n queueLocked: 'Queued messages cannot be changed here.',\n waitToSteer: 'Wait for the current send to finish, then steer.',\n steerFailed: 'The message could not be steered.',\n connectionFailed: 'Connection failed',\n actionFailedShort: 'Action failed',\n uploadFailedShort: 'Upload failed',\n changeRefused: 'The change was refused',\n dismissHint: 'click to dismiss',\n // Composer\n placeholder: 'Message the agent…',\n placeholderWorking: 'Queue a message…',\n messageInput: 'Message',\n send: 'Send',\n sendHint: 'Send (Enter)',\n stop: 'Stop',\n queue: 'Queue',\n add: 'Add',\n attachFiles: 'Attach files',\n takePhoto: 'Take a photo',\n cameraShoot: 'Take the photo',\n cameraClose: 'Close the camera',\n cameraUnavailable: 'The camera cannot open on this page.',\n cameraFallback: \"Use the phone's camera\",\n commands: 'Commands',\n annotate: 'Annotate',\n annotateHint: 'Annotate: click what you want to comment on (Esc to stop)',\n annotateNotes: (count: number) => `${count} ${count === 1 ? 'note' : 'notes'}`,\n annotateClear: 'Remove the notes',\n feedbackSent: (count: number) => `${count} ${count === 1 ? 'comment' : 'comments'} on the page`,\n remove: (name: string) => `Remove ${name}`,\n dismiss: (name: string) => `Dismiss ${name}`,\n uploading: (name: string) => `Uploading ${name}…`,\n uploadFailed: (name: string, error: string | undefined) => `${name}: ${error ?? 'upload failed'}`,\n preparingAttachments: 'Preparing attachments…',\n preparingAttachmentsShort: 'Preparing attachments',\n mentionedFiles: 'Mentioned files',\n uploads: 'Uploads',\n attachments: 'Attachments',\n /** The type line of a file card in a sent message. */\n fileKind: { document: 'Document', image: 'Image', pdf: 'PDF', sheet: 'Spreadsheet', slides: 'Presentation', audio: 'Audio', file: 'File' },\n openFile: (name: string) => `Open ${name}`,\n fileChange: { created: 'Created', edited: 'Edited' },\n /** The name of a pasted text file: the words and the time it was pasted. */\n pastedFileName: (stamp: string) => `Pasted text ${stamp}`,\n restorePaste: 'Put the pasted text back in the message',\n searchCommands: 'Search commands',\n filterBySource: 'Filter by source',\n allSources: 'All',\n noCommands: 'No matching commands.',\n /** A `/` menu filter chip: `skills`, `built-in` or a plugin's name. */\n slashGroup: (group: string) => group,\n skillBadge: 'skill',\n workspaceFiles: 'Workspace files',\n searchFailed: 'Search failed',\n model: 'Model',\n effort: 'Effort',\n effortDefault: 'Default',\n modelAndEffort: 'Model and effort',\n modelSummary: (model: string | undefined, effort: string | undefined) => [model && `Model: ${model}`, effort && `Effort: ${effort}`].filter(Boolean).join(', '),\n /** A thinking level's name and short description in the picker. */\n effortName: (level: string) => EFFORT_NAMES[level] ?? level,\n effortDetail: (level: string): string | undefined => EFFORT_DETAILS[level],\n // Voice input (the microphone button beside the composer)\n voiceRecord: 'Record a message',\n voiceHold: 'Hold to record, release to send',\n voicePauseRecording: 'Pause recording',\n voiceResumeRecording: 'Resume recording',\n voicePaused: 'Recording paused',\n voiceRecording: 'Recording',\n voicePreparing: 'Preparing voice message…',\n voiceAudioOnly: 'Send audio only',\n voiceDiscard: 'Discard',\n voicePlay: 'Play voice message',\n voicePause: 'Pause voice message',\n voicePlayFailed: 'The audio could not be played. Try again.',\n voiceBlocked: 'Not sent. Review the chat notice before trying again.',\n voiceUnconfirmed: 'Message admission is unconfirmed. Use the chat notice to reconcile.',\n voiceStop: 'Stop recording',\n voiceCancel: 'Cancel recording',\n voiceTranscribing: 'Transcribing…',\n voiceNothingHeard: 'Nothing was heard. Try again.',\n voiceTranscribeFailed: 'The recording could not be transcribed. Try again.',\n voiceTranscriptUnavailable: 'Voice message — transcription unavailable.',\n voiceDenied: 'Microphone blocked. Allow it for this site in the browser, then press record again.',\n voiceNoMicrophone: 'No microphone found. Connect one, then press record again.',\n voiceBusy: 'The microphone is busy or could not be opened. Close other apps using it and try again.',\n voiceInsecure: 'Recording needs a secure page (https or localhost).',\n voiceUnsupported: 'This browser cannot record audio here.',\n voiceStartFailed: 'The microphone could not be started.',\n // Queue\n queuedMessages: 'Queued messages',\n steer: 'Steer',\n steerHint: 'Send this into the running turn now',\n steering: 'Steering',\n queued: 'Queued',\n removeQueued: 'Remove queued message',\n removeHint: 'Remove',\n more: 'More',\n edit: 'Edit',\n sending: 'Sending',\n /** Under a message whose send was not confirmed; the notice below offers Check and Retry. */\n notSent: 'Not sent',\n /** The time inside a bubble in the `messaging` style (« 14:02 »), and its full date as the tooltip. */\n messageClock: (at: string) => formatClock(at, 'en'),\n messageTimestamp: (at: string) => formatTimestamp(at, 'en'),\n images: (count: number) => `[${count} image${count === 1 ? '' : 's'}]`,\n // Conversation list and history\n historyDialog: 'Conversation history',\n newConversation: 'New',\n untitled: 'New conversation',\n closeHistory: 'Close history',\n close: 'Close',\n earlierInConversation: 'Earlier messages in this conversation',\n searchConversations: 'Search conversations',\n archived: 'Archived',\n current: 'Current',\n noMatch: (query: string) => `No conversation matches “${query}”.`,\n noArchived: 'No archived conversations.',\n noConversations: 'No conversations yet.',\n loadingConversations: 'Loading conversations…',\n actionFailed: 'That did not work. Try again.',\n rename: 'Rename',\n renameItem: (title: string) => `Rename ${title}`,\n conversationName: 'Conversation name',\n save: 'Save',\n archive: 'Archive',\n restore: 'Restore',\n archiveItem: (title: string, archived: boolean) => `${archived ? 'Restore' : 'Archive'} ${title}`,\n delete: 'Delete',\n deleteItem: (title: string) => `Delete ${title}`,\n keep: 'Keep conversation',\n today: 'Today',\n yesterday: 'Yesterday',\n previousWeek: 'Previous 7 days',\n older: 'Older',\n conversations: 'Conversations',\n earlierMessages: 'Earlier messages',\n readOnly: '(read-only)',\n retry: 'Retry',\n loadingShort: 'Loading…',\n loadEarlier: 'Load earlier',\n loadEarlierMessages: 'Load earlier messages',\n earlierMessagesLoaded: (count: number) => `${count} earlier messages loaded`,\n returnToConversation: 'Return to active conversation',\n historyFailed: 'Conversation history could not be loaded. The previous page remains visible.',\n loadPage: 'Load a page to browse earlier messages.',\n noRecords: 'No records in this history page.',\n beginning: 'Start of conversation',\n justNow: 'now',\n roleSystem: 'System',\n roleToolResult: (tool: string, failed: boolean) => `Tool result: ${tool}${failed ? ' (failed)' : ''}`,\n roleUser: 'You',\n roleAssistant: (outcome: 'ok' | 'interrupted' | 'failed'): string => outcome === 'interrupted' ? 'Assistant (interrupted)' : outcome === 'failed' ? 'Assistant (failed)' : 'Assistant',\n roleEvent: 'Event',\n contextBoundary: 'Context boundary',\n // Messages\n copy: 'Copy',\n copied: 'Copied',\n copyFailed: 'Copy failed',\n failed: 'Failed',\n copyResponse: 'Copy response',\n copyCode: 'Copy code',\n fork: 'Fork from here',\n forkHint: 'Fork from here: a new conversation with the messages up to this reply',\n responseFailed: 'The response failed',\n interrupted: 'Response interrupted',\n answeredBy: (model: string) => `answered by ${model}`,\n answeredFallback: (reason: string) => `fallback: ${reason}`,\n thinking: 'Thinking…',\n openImage: 'Open image attachment',\n imageAttachment: 'Image attachment',\n imagePosition: (index: number, count: number) => `Image ${index} of ${count}`,\n previousImage: 'Previous image',\n nextImage: 'Next image',\n systemContext: 'System context',\n toolFailed: (tool: string) => `${tool} failed`,\n event: (label: string) => `Event: ${label}`,\n // Activity and tools\n reasoning: 'Reasoning',\n thinkingStep: 'Thinking',\n steps: (count: number) => `${count} ${count === 1 ? 'step' : 'steps'}`,\n toolRunning: 'Running',\n toolCompleted: 'Completed',\n toolFailedStatus: 'Failed',\n toolUnfinished: 'No result',\n arguments: 'Arguments',\n result: 'Result',\n error: 'Error',\n openImageResult: 'Open image result',\n emptyResult: 'Empty result',\n /** The live line of a running tool call; `hint` is what it acts on (a path, a command), possibly empty. */\n toolActivity: (tool: string, hint: string) => {\n const verb = TOOL_VERBS[tool] ?? `Running ${tool}`;\n return hint ? `${verb}${tool === 'bash' ? ':' : ''} ${hint}` : verb;\n },\n /** How a tool is named in a finished activity summary (\"Used read ×3 · command\"). */\n toolNoun: (tool: string) => TOOL_NOUNS[tool] ?? tool,\n // Approval and question cards\n approvalNeeded: 'Approval needed',\n needsApproval: 'Needs your approval',\n approve: 'Approve',\n deny: 'Deny',\n approved: 'Approved',\n denied: 'Denied',\n notDecided: 'Not decided: the run was stopped',\n wantsToRun: 'The agent wants to run',\n decision: 'Decision',\n stillWritingCall: 'The agent is still writing this call…',\n decidingUnavailable: 'Deciding is not available in this view.',\n decisionSent: (approved: boolean) => `${approved ? 'Approved.' : 'Denied.'} Waiting for the agent…`,\n decisionRefused: (kind: 'denied' | 'conflict' | 'unknown-question'): string => kind === 'denied' ? 'The host did not accept this answer.' : kind === 'conflict' ? 'This call was already decided.' : 'This call is no longer waiting for a decision.',\n decisionNotSent: 'The decision could not be sent.',\n waitingForAgent: 'Waiting for the agent…',\n question: 'Question',\n questionForYou: 'Question for you',\n notAnswered: 'Not answered',\n answered: 'Answered',\n askedQuestion: 'The agent asked a question.',\n answerOptions: 'Answer options',\n ownAnswer: 'Or type your own answer',\n typeAnswer: 'Type your answer',\n yourAnswerInput: 'Your answer',\n /** A question of a form left blank, in the answer sent to the agent. */\n skipped: '(no answer)',\n answer: 'Answer',\n stillWritingQuestion: 'The agent is still writing this question…',\n answeringUnavailable: 'Answering is not available in this view.',\n answerRefused: (kind: 'denied' | 'conflict' | 'unknown-question'): string => kind === 'denied' ? 'The host did not accept this answer.' : kind === 'conflict' ? 'This question was already answered.' : 'This question is no longer waiting for an answer.',\n answerNotSent: 'The answer could not be sent.',\n yourAnswer: 'Your answer:',\n // Run card (a headless run: `runs/<id>`)\n run: 'Run',\n runStatus: (status: 'queued' | 'running' | 'completed' | 'failed'): string => ({ queued: 'Queued', running: 'Running', completed: 'Completed', failed: 'Failed' })[status],\n runElapsed: (time: string) => `Running for ${time}`,\n runDuration: (time: string) => `Took ${time}`,\n runRefresh: 'Refresh',\n runNotLoaded: 'The run could not be loaded.',\n // Sub-agent card (a `delegate` call)\n subagentStatus: (status: 'running' | 'done' | 'failed' | 'cancelled'): string => ({ running: 'Running', done: 'Done', failed: 'Failed', cancelled: 'Stopped' })[status],\n subagentTime: 'Time',\n subagentToolCalls: (count: number) => `${count} tool ${count === 1 ? 'call' : 'calls'}`,\n subagentTokens: (input: number, output: number) => `${input} tokens in, ${output} tokens out`,\n subagentConversation: (id: number) => `Open conversation ${id}`,\n // Artifact cards\n artifact: 'Artifact',\n artifactTypes: { markdown: 'Document', html: 'HTML page', svg: 'SVG image', code: 'Code', canvas: 'Canvas' },\n opening: 'Opening…',\n viewing: 'Viewing',\n version: (ordinal: number) => `Version ${ordinal}`,\n runCommand: (name: string, execute: boolean) => `${execute ? 'Run' : 'Insert'} /${name} command`,\n taskStatus: 'Task status',\n activityFailed: (count: number, tools: string) => `${count > 1 ? `${count} steps failed` : 'Step failed'}: ${tools}`,\n activityUsed: (summary: string, stopped: boolean) => `${stopped ? 'Stopped · ' : 'Used '}${summary}`,\n activityThought: 'Thought it through',\n activityMany: (steps: number) => `Worked through ${steps} steps`,\n open: 'Open',\n openArtifact: (title: string, version: number | undefined) => version === undefined ? `Open ${title}` : `Open ${title}, version ${version}`,\n};\n\nexport type ChatLabels = typeof defaultChatLabels;\n\n/** The chat's icons. Any component that takes `className` (every lucide icon does). */\nexport const defaultChatIcons = {\n /** The agent's glyph in the empty state (the header shows the host's `avatar`, if any). */\n agent: MessagesSquareIcon as BlockIcon,\n send: ArrowUpIcon as BlockIcon,\n stop: SquareIcon as BlockIcon,\n history: HistoryIcon as BlockIcon,\n newChat: PlusIcon as BlockIcon,\n add: PlusIcon as BlockIcon,\n attach: PaperclipIcon as BlockIcon,\n camera: CameraIcon as BlockIcon,\n queue: ListEndIcon as BlockIcon,\n jumpToLatest: ArrowDownIcon as BlockIcon,\n reconnect: RefreshCwIcon as BlockIcon,\n fork: GitForkIcon as BlockIcon,\n};\n\nexport type ChatIcons = typeof defaultChatIcons;\n\n/** What the chat's pieces read: the merged labels and icons. */\nexport interface ChatText { readonly labels: ChatLabels; readonly icons: ChatIcons }\n/** The defaults, as `useMergedText`'s base. */\nexport const defaultChatText: ChatText = { labels: defaultChatLabels, icons: defaultChatIcons };\nconst DEFAULT_TEXT = defaultChatText;\nconst ChatTextContext = createContext<ChatText>(DEFAULT_TEXT);\n\n/** The labels and icons of the surrounding chat (the defaults outside one). */\nexport const useChatText = (): ChatText => useContext(ChatTextContext);\n\n/** Merges the host's partial labels and icons over the defaults (or over `base`), keeping one object while they do not change. */\nexport function useMergedText<L extends ChatLabels = ChatLabels, I extends ChatIcons = ChatIcons>(labels: Partial<L> | undefined, icons: Partial<I> | undefined,\n base: { readonly labels: L; readonly icons: I } = DEFAULT_TEXT as { readonly labels: L; readonly icons: I }): { readonly labels: L; readonly icons: I } {\n const stableLabels = useShallowStable(labels), stableIcons = useShallowStable(icons);\n return useMemo(() => ({ labels: withDefaults(base.labels, stableLabels), icons: withDefaults(base.icons, stableIcons) }), [stableLabels, stableIcons, base]);\n}\n\nconst shallowEqual = (a: unknown, b: unknown): boolean => Object.is(a, b) || (!!a && !!b && typeof a === 'object' && typeof b === 'object' && Array.isArray(a) === Array.isArray(b)\n && Object.keys(a).length === Object.keys(b).length && Object.keys(a).every(key => Object.is((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key])));\n/** The previous value while the new one is equal key by key (functions by identity): inline `labels={{ title }}` keeps one text context. */\nexport function useShallowStable<T>(value: T): T {\n const previous = useRef(value);\n // eslint-disable-next-line react-hooks/refs -- a render-time identity cache: it only swaps in a value equal key by key, so any render reads the same result\n if (!shallowEqual(previous.current, value)) previous.current = value;\n // eslint-disable-next-line react-hooks/refs -- returns the cached identity (see above); never setState in render\n return previous.current;\n}\n\n/** Provides the chat's labels and icons to everything inside (composer, queue, rows, history). */\nexport function ChatTextProvider({ value, children }: { readonly value: ChatText; readonly children: ReactNode }) {\n return createElement(ChatTextContext.Provider, { value }, children);\n}\n",
|
|
147
|
+
"content": "/*\n * Every word the chat shows, and the icons it draws, as plain data. A host passes a partial `labels` (and `icons`) object to `PiChat`\n * (or `AmbientChat`, `AgentWorkspace`): what it leaves out keeps the default below. Strings that need a value are functions. No i18n\n * framework: an app that translates passes its own object per language.\n */\nimport { createContext, createElement, useContext, useMemo, useRef } from 'react';\nimport type { ReactNode } from 'react';\nimport { ArrowDownIcon, ArrowUpIcon, CameraIcon, GitForkIcon, HistoryIcon, ListEndIcon, MessagesSquareIcon, PaperclipIcon, PlusIcon, RefreshCwIcon, SquareIcon } from 'lucide-react';\nimport type { BlockIcon } from '../button/actions';\nimport { withDefaults } from '../utils/utils';\n\n// An Intl.DateTimeFormat costs far more than formatting with one; every bubble's clock reuses the same few.\nconst formats = new Map<string, Intl.DateTimeFormat>();\nfunction intlFormat(locale: string, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat {\n const key = `${locale}|${JSON.stringify(options)}`;\n let found = formats.get(key);\n if (!found) { found = new Intl.DateTimeFormat(locale, options); formats.set(key, found); }\n return found;\n}\n/** « Tue 22 Oct, 14:02 ». */\nexport function formatTimestamp(at: string, locale: string) {\n const date = new Date(at);\n if (!Number.isFinite(date.getTime())) return at;\n return `${intlFormat(locale, { weekday: 'short', day: 'numeric', month: 'short' }).format(date)}, ${intlFormat(locale, { hour: '2-digit', minute: '2-digit' }).format(date)}`;\n}\n/** The time inside a bubble (« 14:02 », « 2:02 PM »). */\nexport function formatClock(at: string, locale: string) {\n const date = new Date(at);\n return Number.isFinite(date.getTime()) ? intlFormat(locale, { hour: 'numeric', minute: '2-digit' }).format(date) : '';\n}\n\nconst TOOL_VERBS: Readonly<Record<string, string>> = { read: 'Reading', write: 'Writing', edit: 'Editing', ls: 'Listing', list_files: 'Listing', find: 'Searching', grep: 'Searching', load_skill: 'Loading skill', bash: 'Running bash' };\nconst TOOL_NOUNS: Readonly<Record<string, string>> = { bash: 'command', read: 'read', write: 'write', edit: 'edit', find: 'find', grep: 'search', ls: 'list' };\n\nconst EFFORT_NAMES: Readonly<Record<string, string>> = { off: 'Off', minimal: 'Minimal', low: 'Low', medium: 'Medium', high: 'High', xhigh: 'Extra high', max: 'Max' };\nconst EFFORT_DETAILS: Readonly<Record<string, string>> = { off: 'No extra reasoning', minimal: 'Barely any reasoning', low: 'Light reasoning', medium: 'Balanced reasoning', high: 'Deep reasoning', xhigh: 'Deeper reasoning', max: 'Maximum reasoning' };\n\nexport const defaultChatLabels = {\n // Header and transcript\n /** The chat's name: the header title, the region's name and the empty state's title. */\n title: 'Conversation',\n history: 'History',\n artifacts: 'Artifacts',\n search: 'Search',\n live: 'Live',\n connecting: 'Connecting…',\n reconnecting: 'Reconnecting…',\n loadFailed: 'Conversation could not be loaded.',\n disconnected: 'Disconnected. Last observed messages remain visible.',\n reconnect: 'Reconnect',\n requiredActions: 'Required conversation actions',\n messages: 'Messages',\n showEarlier: 'Show earlier messages',\n loading: 'Loading conversation…',\n emptyDescription: 'Start a conversation.',\n jumpToLatest: 'Jump to latest',\n agentWorking: 'Agent is working',\n waitingForAnswer: 'Waiting for your answer',\n retryPending: 'Native retry pending',\n compacting: 'Native context compaction in progress',\n /** The one status of `presentation=\"natural\"` while the agent works (any tool, retry or compaction), instead of the activity wording above. */\n preparing: 'Preparing…',\n /** In `presentation=\"natural\"`, replaces the controller's own reason when a send is blocked. */\n sendUnavailable: 'Sending is not available right now.',\n /** The \"…\" menu of host actions (header, replies). */\n moreActions: 'More actions',\n // Notices\n sendUnknown: 'Submission acknowledgement is unknown. The message is kept until you check or retry it.',\n checkSubmission: 'Check original submission',\n retrySame: 'Retry same request',\n stopRequested: 'Stop requested. Waiting for native confirmation.',\n stopUnconfirmed: 'Stop is unconfirmed. Work may still be running.',\n steerRefused: 'The message could not be steered; it is back in the message box.',\n tooLate: 'Too late: that message has already started.',\n tooLateToRemove: 'Too late to remove: that message has already started.',\n queueFailed: 'The message could not be changed.',\n queueLocked: 'Queued messages cannot be changed here.',\n waitToSteer: 'Wait for the current send to finish, then steer.',\n steerFailed: 'The message could not be steered.',\n connectionFailed: 'Connection failed',\n actionFailedShort: 'Action failed',\n uploadFailedShort: 'Upload failed',\n changeRefused: 'The change was refused',\n dismissHint: 'click to dismiss',\n // Composer\n placeholder: 'Message the agent…',\n placeholderWorking: 'Queue a message…',\n messageInput: 'Message',\n send: 'Send',\n sendHint: 'Send (Enter)',\n stop: 'Stop',\n queue: 'Queue',\n add: 'Add',\n attachFiles: 'Attach files',\n takePhoto: 'Take a photo',\n cameraShoot: 'Take the photo',\n cameraClose: 'Close the camera',\n cameraUnavailable: 'The camera cannot open on this page.',\n cameraFallback: \"Use the phone's camera\",\n commands: 'Commands',\n annotate: 'Annotate',\n annotateHint: 'Annotate: click what you want to comment on (Esc to stop)',\n annotateNotes: (count: number) => `${count} ${count === 1 ? 'note' : 'notes'}`,\n annotateClear: 'Remove the notes',\n feedbackSent: (count: number) => `${count} ${count === 1 ? 'comment' : 'comments'} on the page`,\n remove: (name: string) => `Remove ${name}`,\n dismiss: (name: string) => `Dismiss ${name}`,\n uploading: (name: string) => `Uploading ${name}…`,\n uploadFailed: (name: string, error: string | undefined) => `${name}: ${error ?? 'upload failed'}`,\n preparingAttachments: 'Preparing attachments…',\n preparingAttachmentsShort: 'Preparing attachments',\n mentionedFiles: 'Mentioned files',\n uploads: 'Uploads',\n attachments: 'Attachments',\n /** The type line of a file card in a sent message. */\n fileKind: { document: 'Document', image: 'Image', pdf: 'PDF', sheet: 'Spreadsheet', slides: 'Presentation', audio: 'Audio', file: 'File' },\n openFile: (name: string) => `Open ${name}`,\n fileChange: { created: 'Created', edited: 'Edited' },\n /** The name of a pasted text file: the words and the time it was pasted. */\n pastedFileName: (stamp: string) => `Pasted text ${stamp}`,\n restorePaste: 'Put the pasted text back in the message',\n searchCommands: 'Search commands',\n filterBySource: 'Filter by source',\n allSources: 'All',\n noCommands: 'No matching commands.',\n /** A `/` menu filter chip: `skills`, `built-in` or a plugin's name. */\n slashGroup: (group: string) => group,\n skillBadge: 'skill',\n workspaceFiles: 'Workspace files',\n searchFailed: 'Search failed',\n model: 'Model',\n effort: 'Effort',\n effortDefault: 'Default',\n modelAndEffort: 'Model and effort',\n modelSummary: (model: string | undefined, effort: string | undefined) => [model && `Model: ${model}`, effort && `Effort: ${effort}`].filter(Boolean).join(', '),\n /** A thinking level's name and short description in the picker. */\n effortName: (level: string) => EFFORT_NAMES[level] ?? level,\n effortDetail: (level: string): string | undefined => EFFORT_DETAILS[level],\n // Voice input (the microphone button beside the composer)\n voiceRecord: 'Record a message',\n voiceHold: 'Hold to record, release to send',\n voicePauseRecording: 'Pause recording',\n voiceResumeRecording: 'Resume recording',\n voicePaused: 'Recording paused',\n voiceRecording: 'Recording',\n voicePreparing: 'Preparing voice message…',\n voiceAudioOnly: 'Send audio only',\n voiceDiscard: 'Discard',\n voicePlay: 'Play voice message',\n voicePause: 'Pause voice message',\n voicePlayFailed: 'The audio could not be played. Try again.',\n voiceBlocked: 'Not sent. Review the chat notice before trying again.',\n voiceUnconfirmed: 'Message admission is unconfirmed. Use the chat notice to reconcile.',\n voiceStop: 'Stop recording',\n voiceCancel: 'Cancel recording',\n voiceTranscribing: 'Transcribing…',\n voiceNothingHeard: 'Nothing was heard. Try again.',\n voiceTranscribeFailed: 'The recording could not be transcribed. Try again.',\n voiceTranscriptUnavailable: 'Voice message — transcription unavailable.',\n voiceDenied: 'Microphone blocked. Allow it for this site in the browser, then press record again.',\n voiceNoMicrophone: 'No microphone found. Connect one, then press record again.',\n voiceBusy: 'The microphone is busy or could not be opened. Close other apps using it and try again.',\n voiceInsecure: 'Recording needs a secure page (https or localhost).',\n voiceUnsupported: 'This browser cannot record audio here.',\n voiceStartFailed: 'The microphone could not be started.',\n // Queue\n queuedMessages: 'Queued messages',\n steer: 'Steer',\n steerHint: 'Send this into the running turn now',\n steering: 'Steering',\n queued: 'Queued',\n removeQueued: 'Remove queued message',\n removeHint: 'Remove',\n more: 'More',\n edit: 'Edit',\n sending: 'Sending',\n /** Under a message whose send was not confirmed; the notice below offers Check and Retry. */\n notSent: 'Not sent',\n /** The time inside a bubble in the `messaging` style (« 14:02 »), and its full date as the tooltip. */\n messageClock: (at: string) => formatClock(at, 'en'),\n messageTimestamp: (at: string) => formatTimestamp(at, 'en'),\n images: (count: number) => `[${count} image${count === 1 ? '' : 's'}]`,\n // Conversation list and history\n historyDialog: 'Conversation history',\n newConversation: 'New',\n untitled: 'New conversation',\n closeHistory: 'Close history',\n close: 'Close',\n earlierInConversation: 'Earlier messages in this conversation',\n searchConversations: 'Search conversations',\n archived: 'Archived',\n current: 'Current',\n noMatch: (query: string) => `No conversation matches “${query}”.`,\n noArchived: 'No archived conversations.',\n noConversations: 'No conversations yet.',\n loadingConversations: 'Loading conversations…',\n actionFailed: 'That did not work. Try again.',\n rename: 'Rename',\n renameItem: (title: string) => `Rename ${title}`,\n conversationName: 'Conversation name',\n save: 'Save',\n archive: 'Archive',\n restore: 'Restore',\n archiveItem: (title: string, archived: boolean) => `${archived ? 'Restore' : 'Archive'} ${title}`,\n delete: 'Delete',\n deleteItem: (title: string) => `Delete ${title}`,\n keep: 'Keep conversation',\n today: 'Today',\n yesterday: 'Yesterday',\n previousWeek: 'Previous 7 days',\n older: 'Older',\n conversations: 'Conversations',\n earlierMessages: 'Earlier messages',\n readOnly: '(read-only)',\n retry: 'Retry',\n loadingShort: 'Loading…',\n loadEarlier: 'Load earlier',\n loadEarlierMessages: 'Load earlier messages',\n earlierMessagesLoaded: (count: number) => `${count} earlier messages loaded`,\n returnToConversation: 'Return to active conversation',\n historyFailed: 'Conversation history could not be loaded. The previous page remains visible.',\n loadPage: 'Load a page to browse earlier messages.',\n noRecords: 'No records in this history page.',\n beginning: 'Start of conversation',\n justNow: 'now',\n roleSystem: 'System',\n roleToolResult: (tool: string, failed: boolean) => `Tool result: ${tool}${failed ? ' (failed)' : ''}`,\n roleUser: 'You',\n roleAssistant: (outcome: 'ok' | 'interrupted' | 'failed'): string => outcome === 'interrupted' ? 'Assistant (interrupted)' : outcome === 'failed' ? 'Assistant (failed)' : 'Assistant',\n roleEvent: 'Event',\n contextBoundary: 'Context boundary',\n // Messages\n copy: 'Copy',\n copied: 'Copied',\n copyFailed: 'Copy failed',\n failed: 'Failed',\n copyResponse: 'Copy response',\n copyCode: 'Copy code',\n fork: 'Fork from here',\n forkHint: 'Fork from here: a new conversation with the messages up to this reply',\n responseFailed: 'The response failed',\n interrupted: 'Response interrupted',\n answeredBy: (model: string) => `answered by ${model}`,\n answeredFallback: (reason: string) => `fallback: ${reason}`,\n thinking: 'Thinking…',\n openImage: 'Open image attachment',\n imageAttachment: 'Image attachment',\n imagePosition: (index: number, count: number) => `Image ${index} of ${count}`,\n previousImage: 'Previous image',\n nextImage: 'Next image',\n systemContext: 'System context',\n toolFailed: (tool: string) => `${tool} failed`,\n event: (label: string) => `Event: ${label}`,\n // Activity and tools\n reasoning: 'Reasoning',\n thinkingStep: 'Thinking',\n steps: (count: number) => `${count} ${count === 1 ? 'step' : 'steps'}`,\n toolRunning: 'Running',\n toolCompleted: 'Completed',\n toolFailedStatus: 'Failed',\n toolUnfinished: 'No result',\n arguments: 'Arguments',\n result: 'Result',\n error: 'Error',\n openImageResult: 'Open image result',\n emptyResult: 'Empty result',\n /** The live line of a running tool call; `hint` is what it acts on (a path, a command), possibly empty. */\n toolActivity: (tool: string, hint: string) => {\n const verb = TOOL_VERBS[tool] ?? `Running ${tool}`;\n return hint ? `${verb}${tool === 'bash' ? ':' : ''} ${hint}` : verb;\n },\n /** How a tool is named in a finished activity summary (\"Used read ×3 · command\"). */\n toolNoun: (tool: string) => TOOL_NOUNS[tool] ?? tool,\n // Approval and question cards\n approvalNeeded: 'Approval needed',\n needsApproval: 'Needs your approval',\n approve: 'Approve',\n deny: 'Deny',\n approved: 'Approved',\n denied: 'Denied',\n notDecided: 'Not decided: the run was stopped',\n wantsToRun: 'The agent wants to run',\n decision: 'Decision',\n stillWritingCall: 'The agent is still writing this call…',\n decidingUnavailable: 'Deciding is not available in this view.',\n decisionSent: (approved: boolean) => `${approved ? 'Approved.' : 'Denied.'} Waiting for the agent…`,\n decisionRefused: (kind: 'denied' | 'conflict' | 'unknown-question'): string => kind === 'denied' ? 'The host did not accept this answer.' : kind === 'conflict' ? 'This call was already decided.' : 'This call is no longer waiting for a decision.',\n decisionNotSent: 'The decision could not be sent.',\n waitingForAgent: 'Waiting for the agent…',\n question: 'Question',\n questionForYou: 'Question for you',\n notAnswered: 'Not answered',\n answered: 'Answered',\n askedQuestion: 'The agent asked a question.',\n answerOptions: 'Answer options',\n ownAnswer: 'Or type your own answer',\n typeAnswer: 'Type your answer',\n yourAnswerInput: 'Your answer',\n /** A question of a form left blank, in the answer sent to the agent. */\n skipped: '(no answer)',\n answer: 'Answer',\n stillWritingQuestion: 'The agent is still writing this question…',\n answeringUnavailable: 'Answering is not available in this view.',\n answerRefused: (kind: 'denied' | 'conflict' | 'unknown-question'): string => kind === 'denied' ? 'The host did not accept this answer.' : kind === 'conflict' ? 'This question was already answered.' : 'This question is no longer waiting for an answer.',\n answerNotSent: 'The answer could not be sent.',\n questionNotAnswered: 'The question was not answered.',\n yourAnswer: 'Your answer:',\n // Run card (a headless run: `runs/<id>`)\n run: 'Run',\n runStatus: (status: 'queued' | 'running' | 'completed' | 'failed'): string => ({ queued: 'Queued', running: 'Running', completed: 'Completed', failed: 'Failed' })[status],\n runElapsed: (time: string) => `Running for ${time}`,\n runDuration: (time: string) => `Took ${time}`,\n runRefresh: 'Refresh',\n runNotLoaded: 'The run could not be loaded.',\n // Sub-agent card (a `delegate` call)\n subagentStatus: (status: 'running' | 'done' | 'failed' | 'cancelled'): string => ({ running: 'Running', done: 'Done', failed: 'Failed', cancelled: 'Stopped' })[status],\n subagentTime: 'Time',\n subagentToolCalls: (count: number) => `${count} tool ${count === 1 ? 'call' : 'calls'}`,\n subagentTokens: (input: number, output: number) => `${input} tokens in, ${output} tokens out`,\n subagentConversation: (id: number) => `Open conversation ${id}`,\n // Artifact cards\n artifact: 'Artifact',\n artifactTypes: { markdown: 'Document', html: 'HTML page', svg: 'SVG image', code: 'Code', canvas: 'Canvas' },\n opening: 'Opening…',\n viewing: 'Viewing',\n version: (ordinal: number) => `Version ${ordinal}`,\n runCommand: (name: string, execute: boolean) => `${execute ? 'Run' : 'Insert'} /${name} command`,\n taskStatus: 'Task status',\n activityFailed: (count: number, tools: string) => `${count > 1 ? `${count} steps failed` : 'Step failed'}: ${tools}`,\n activityUsed: (summary: string, stopped: boolean) => `${stopped ? 'Stopped · ' : 'Used '}${summary}`,\n activityThought: 'Thought it through',\n activityMany: (steps: number) => `Worked through ${steps} steps`,\n open: 'Open',\n openArtifact: (title: string, version: number | undefined) => version === undefined ? `Open ${title}` : `Open ${title}, version ${version}`,\n};\n\nexport type ChatLabels = typeof defaultChatLabels;\n\n/** The chat's icons. Any component that takes `className` (every lucide icon does). */\nexport const defaultChatIcons = {\n /** The agent's glyph in the empty state (the header shows the host's `avatar`, if any). */\n agent: MessagesSquareIcon as BlockIcon,\n send: ArrowUpIcon as BlockIcon,\n stop: SquareIcon as BlockIcon,\n history: HistoryIcon as BlockIcon,\n newChat: PlusIcon as BlockIcon,\n add: PlusIcon as BlockIcon,\n attach: PaperclipIcon as BlockIcon,\n camera: CameraIcon as BlockIcon,\n queue: ListEndIcon as BlockIcon,\n jumpToLatest: ArrowDownIcon as BlockIcon,\n reconnect: RefreshCwIcon as BlockIcon,\n fork: GitForkIcon as BlockIcon,\n};\n\nexport type ChatIcons = typeof defaultChatIcons;\n\n/** What the chat's pieces read: the merged labels and icons. */\nexport interface ChatText { readonly labels: ChatLabels; readonly icons: ChatIcons }\n/** The defaults, as `useMergedText`'s base. */\nexport const defaultChatText: ChatText = { labels: defaultChatLabels, icons: defaultChatIcons };\nconst DEFAULT_TEXT = defaultChatText;\nconst ChatTextContext = createContext<ChatText>(DEFAULT_TEXT);\n\n/** The labels and icons of the surrounding chat (the defaults outside one). */\nexport const useChatText = (): ChatText => useContext(ChatTextContext);\n\n/** Merges the host's partial labels and icons over the defaults (or over `base`), keeping one object while they do not change. */\nexport function useMergedText<L extends ChatLabels = ChatLabels, I extends ChatIcons = ChatIcons>(labels: Partial<L> | undefined, icons: Partial<I> | undefined,\n base: { readonly labels: L; readonly icons: I } = DEFAULT_TEXT as { readonly labels: L; readonly icons: I }): { readonly labels: L; readonly icons: I } {\n const stableLabels = useShallowStable(labels), stableIcons = useShallowStable(icons);\n return useMemo(() => ({ labels: withDefaults(base.labels, stableLabels), icons: withDefaults(base.icons, stableIcons) }), [stableLabels, stableIcons, base]);\n}\n\nconst shallowEqual = (a: unknown, b: unknown): boolean => Object.is(a, b) || (!!a && !!b && typeof a === 'object' && typeof b === 'object' && Array.isArray(a) === Array.isArray(b)\n && Object.keys(a).length === Object.keys(b).length && Object.keys(a).every(key => Object.is((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key])));\n/** The previous value while the new one is equal key by key (functions by identity): inline `labels={{ title }}` keeps one text context. */\nexport function useShallowStable<T>(value: T): T {\n const previous = useRef(value);\n // eslint-disable-next-line react-hooks/refs -- a render-time identity cache: it only swaps in a value equal key by key, so any render reads the same result\n if (!shallowEqual(previous.current, value)) previous.current = value;\n // eslint-disable-next-line react-hooks/refs -- returns the cached identity (see above); never setState in render\n return previous.current;\n}\n\n/** Provides the chat's labels and icons to everything inside (composer, queue, rows, history). */\nexport function ChatTextProvider({ value, children }: { readonly value: ChatText; readonly children: ReactNode }) {\n return createElement(ChatTextContext.Provider, { value }, children);\n}\n",
|
|
148
148
|
"type": "registry:lib",
|
|
149
149
|
"target": "components/pi-chat/labels.ts"
|
|
150
150
|
},
|
|
151
151
|
{
|
|
152
152
|
"path": "registry/pi-chat/labels-fr.ts",
|
|
153
|
-
"content": "/* The chat's words in French, the `locale=\"fr\"` base of `PiChat`, `AmbientChat` and `AgentWorkspace`. Typed as `ChatLabels`, so a key added in English must be added here too. */\nimport { formatClock, formatTimestamp } from './labels';\nimport type { ChatLabels } from './labels';\n\nconst TOOL_VERBS: Readonly<Record<string, string>> = { read: 'Lecture', write: 'Écriture', edit: 'Modification', ls: 'Liste', list_files: 'Liste', find: 'Recherche', grep: 'Recherche', load_skill: 'Chargement de la compétence', bash: 'Exécution bash' };\nconst TOOL_NOUNS: Readonly<Record<string, string>> = { bash: 'commande', read: 'lecture', write: 'écriture', edit: 'modification', find: 'recherche', grep: 'recherche', ls: 'liste' };\nconst EFFORT_NAMES: Readonly<Record<string, string>> = { off: 'Désactivé', minimal: 'Minimal', low: 'Faible', medium: 'Moyen', high: 'Élevé', xhigh: 'Très élevé', max: 'Maximum' };\nconst EFFORT_DETAILS: Readonly<Record<string, string>> = { off: 'Sans réflexion supplémentaire', minimal: 'Presque aucune réflexion', low: 'Réflexion légère', medium: 'Réflexion équilibrée', high: 'Réflexion approfondie', xhigh: 'Réflexion très approfondie', max: 'Réflexion maximale' };\n\nexport const frenchChatLabels: ChatLabels = {\n title: 'Conversation',\n history: 'Historique',\n artifacts: 'Fichiers',\n search: 'Rechercher',\n live: 'En direct',\n connecting: 'Connexion…',\n reconnecting: 'Reconnexion…',\n loadFailed: 'La conversation n’a pas pu être chargée.',\n disconnected: 'Déconnecté. Les derniers messages reçus restent affichés.',\n reconnect: 'Se reconnecter',\n requiredActions: 'Actions requises dans la conversation',\n messages: 'Messages',\n showEarlier: 'Afficher les messages précédents',\n loading: 'Chargement de la conversation…',\n emptyDescription: 'Commencez une conversation.',\n jumpToLatest: 'Aller au dernier message',\n agentWorking: 'L’agent travaille',\n waitingForAnswer: 'En attente de votre réponse',\n retryPending: 'Nouvelle tentative en attente',\n compacting: 'Résumé du contexte en cours',\n preparing: 'En préparation…',\n sendUnavailable: 'L’envoi n’est pas disponible pour le moment.',\n moreActions: 'Plus d’actions',\n sendUnknown: 'L’envoi n’est pas confirmé. Le message est conservé jusqu’à ce que vous le vérifiiez ou le renvoyiez.',\n checkSubmission: 'Vérifier l’envoi',\n retrySame: 'Renvoyer la même demande',\n stopRequested: 'Arrêt demandé. En attente de confirmation.',\n stopUnconfirmed: 'Arrêt non confirmé. Le travail continue peut-être.',\n steerRefused: 'Le message n’a pas pu être ajouté en cours de réponse ; il est revenu dans la zone de saisie.',\n tooLate: 'Trop tard : ce message a déjà commencé.',\n tooLateToRemove: 'Trop tard pour le retirer : ce message a déjà commencé.',\n queueFailed: 'Le message n’a pas pu être modifié.',\n queueLocked: 'Les messages en attente ne peuvent pas être modifiés ici.',\n waitToSteer: 'Attendez la fin de l’envoi en cours, puis réessayez.',\n steerFailed: 'Le message n’a pas pu être ajouté en cours de réponse.',\n connectionFailed: 'Échec de la connexion',\n actionFailedShort: 'Échec de l’action',\n uploadFailedShort: 'Échec de l’envoi',\n changeRefused: 'La modification a été refusée',\n dismissHint: 'cliquez pour fermer',\n placeholder: 'Écrivez à l’agent…',\n placeholderWorking: 'Mettre un message en attente…',\n messageInput: 'Message',\n send: 'Envoyer',\n sendHint: 'Envoyer (Entrée)',\n stop: 'Arrêter',\n queue: 'Mettre en attente',\n add: 'Ajouter',\n attachFiles: 'Joindre des fichiers',\n takePhoto: 'Prendre une photo',\n cameraShoot: 'Prendre la photo',\n cameraClose: 'Fermer l’appareil photo',\n cameraUnavailable: 'L’appareil photo ne peut pas s’ouvrir sur cette page.',\n cameraFallback: 'Utiliser l’appareil photo du téléphone',\n // Voice input (the microphone button beside the composer)\n voiceRecord: 'Enregistrer un message',\n voiceHold: 'Maintenir pour enregistrer, relâcher pour envoyer',\n voicePauseRecording: 'Mettre l’enregistrement en pause',\n voiceResumeRecording: 'Reprendre l’enregistrement',\n voicePaused: 'Enregistrement en pause',\n voiceRecording: 'Enregistrement',\n voicePreparing: 'Préparation du message vocal…',\n voiceAudioOnly: 'Envoyer l’audio seul',\n voiceDiscard: 'Abandonner',\n voicePlay: 'Lire',\n voicePause: 'Mettre en pause',\n voicePlayFailed: 'La lecture de l’audio a échoué. Réessayez.',\n voiceBlocked: 'Non envoyé. Vérifiez l’avis de la conversation avant de réessayer.',\n voiceUnconfirmed: 'L’envoi n’est pas confirmé. Vérifiez l’avis de la conversation.',\n voiceStop: 'Arrêter l’enregistrement',\n voiceCancel: 'Annuler l’enregistrement',\n voiceTranscribing: 'Transcription…',\n voiceNothingHeard: 'Rien n’a été entendu. Réessayez.',\n voiceTranscribeFailed: 'L’enregistrement n’a pas pu être transcrit. Réessayez.',\n voiceTranscriptUnavailable: 'Message vocal — transcription indisponible.',\n voiceDenied: 'Microphone bloqué. Autorisez-le pour ce site dans le navigateur, puis appuyez de nouveau sur enregistrer.',\n voiceNoMicrophone: 'Aucun microphone trouvé. Branchez-en un, puis appuyez de nouveau sur enregistrer.',\n voiceBusy: 'Le microphone est occupé ou n’a pas pu être ouvert. Fermez les autres applications qui l’utilisent et réessayez.',\n voiceInsecure: 'L’enregistrement exige une page sécurisée (https ou localhost).',\n voiceUnsupported: 'Ce navigateur ne peut pas enregistrer le son ici.',\n voiceStartFailed: 'Le microphone n’a pas pu être démarré.',\n commands: 'Commandes',\n annotate: 'Annoter',\n annotateHint: 'Annoter : cliquez sur ce que vous voulez commenter (Échap pour arrêter)',\n annotateNotes: count => `${count} ${count === 1 ? 'note' : 'notes'}`,\n annotateClear: 'Retirer les notes',\n feedbackSent: count => `${count} ${count === 1 ? 'commentaire' : 'commentaires'} sur la page`,\n remove: name => `Retirer ${name}`,\n dismiss: name => `Fermer ${name}`,\n uploading: name => `Envoi de ${name}…`,\n uploadFailed: (name, error) => `${name} : ${error ?? 'échec de l’envoi'}`,\n preparingAttachments: 'Préparation des pièces jointes…',\n preparingAttachmentsShort: 'Préparation des pièces jointes',\n mentionedFiles: 'Fichiers mentionnés',\n uploads: 'Envois',\n attachments: 'Pièces jointes',\n fileKind: { document: 'Document', image: 'Image', pdf: 'PDF', sheet: 'Tableur', slides: 'Présentation', audio: 'Audio', file: 'Fichier' },\n openFile: name => `Ouvrir ${name}`,\n fileChange: { created: 'Créé', edited: 'Modifié' },\n pastedFileName: stamp => `Texte collé ${stamp}`,\n restorePaste: 'Remettre le texte collé dans le message',\n searchCommands: 'Rechercher une commande',\n filterBySource: 'Filtrer par source',\n allSources: 'Toutes',\n noCommands: 'Aucune commande ne correspond.',\n slashGroup: group => group === 'skills' ? 'compétences' : group === 'built-in' ? 'intégrées' : group,\n skillBadge: 'compétence',\n workspaceFiles: 'Fichiers de l’espace',\n searchFailed: 'La recherche a échoué',\n model: 'Modèle',\n effort: 'Effort',\n effortDefault: 'Par défaut',\n modelAndEffort: 'Modèle et effort',\n modelSummary: (model, effort) => [model && `Modèle : ${model}`, effort && `Effort : ${effort}`].filter(Boolean).join(', '),\n effortName: level => EFFORT_NAMES[level] ?? level,\n effortDetail: level => EFFORT_DETAILS[level],\n queuedMessages: 'Messages en attente',\n steer: 'Envoyer maintenant',\n steerHint: 'Ajouter ce message à la réponse en cours',\n steering: 'Envoi en cours de réponse',\n queued: 'En attente',\n removeQueued: 'Retirer le message en attente',\n removeHint: 'Retirer',\n more: 'Plus',\n edit: 'Modifier',\n sending: 'Envoi',\n notSent: 'Non envoyé',\n messageClock: at => formatClock(at, 'fr'),\n messageTimestamp: at => formatTimestamp(at, 'fr'),\n images: count => `[${count} image${count === 1 ? '' : 's'}]`,\n historyDialog: 'Historique des conversations',\n newConversation: 'Nouvelle',\n untitled: 'Nouvelle conversation',\n closeHistory: 'Fermer l’historique',\n close: 'Fermer',\n earlierInConversation: 'Messages précédents de cette conversation',\n searchConversations: 'Rechercher une conversation',\n archived: 'Archivées',\n current: 'En cours',\n noMatch: query => `Aucune conversation ne correspond à « ${query} ».`,\n noArchived: 'Aucune conversation archivée.',\n noConversations: 'Aucune conversation pour le moment.',\n loadingConversations: 'Chargement des conversations…',\n actionFailed: 'Cela n’a pas fonctionné. Réessayez.',\n rename: 'Renommer',\n renameItem: title => `Renommer ${title}`,\n conversationName: 'Nom de la conversation',\n save: 'Enregistrer',\n archive: 'Archiver',\n restore: 'Restaurer',\n archiveItem: (title, archived) => `${archived ? 'Restaurer' : 'Archiver'} ${title}`,\n delete: 'Supprimer',\n deleteItem: title => `Supprimer ${title}`,\n keep: 'Garder la conversation',\n today: 'Aujourd’hui',\n yesterday: 'Hier',\n previousWeek: 'Les 7 derniers jours',\n older: 'Plus anciennes',\n conversations: 'Conversations',\n earlierMessages: 'Messages précédents',\n readOnly: '(lecture seule)',\n retry: 'Réessayer',\n loadingShort: 'Chargement…',\n loadEarlier: 'Charger les précédents',\n loadEarlierMessages: 'Charger les messages précédents',\n earlierMessagesLoaded: count => `${count} messages précédents chargés`,\n returnToConversation: 'Revenir à la conversation en cours',\n historyFailed: 'L’historique n’a pas pu être chargé. La page précédente reste affichée.',\n loadPage: 'Chargez une page pour parcourir les messages précédents.',\n noRecords: 'Aucun élément sur cette page d’historique.',\n beginning: 'Début de la conversation.',\n justNow: 'à l’instant',\n roleSystem: 'Système',\n roleToolResult: (tool, failed) => `Résultat de l’outil : ${tool}${failed ? ' (échec)' : ''}`,\n roleUser: 'Vous',\n roleAssistant: outcome => outcome === 'interrupted' ? 'Assistant (interrompu)' : outcome === 'failed' ? 'Assistant (échec)' : 'Assistant',\n roleEvent: 'Événement',\n contextBoundary: 'Limite du contexte',\n copy: 'Copier',\n copied: 'Copié',\n copyFailed: 'Échec de la copie',\n failed: 'Échec',\n copyResponse: 'Copier la réponse',\n copyCode: 'Copier le code',\n fork: 'Repartir d’ici',\n forkHint: 'Repartir d’ici : une nouvelle conversation avec les messages jusqu’à cette réponse',\n responseFailed: 'La réponse a échoué',\n interrupted: 'Réponse interrompue',\n answeredBy: model => `répondu par ${model}`,\n answeredFallback: reason => `repli : ${reason}`,\n thinking: 'Réflexion…',\n openImage: 'Ouvrir l’image jointe',\n imageAttachment: 'Image jointe',\n imagePosition: (index, count) => `Image ${index} sur ${count}`,\n previousImage: 'Image précédente',\n nextImage: 'Image suivante',\n systemContext: 'Contexte système',\n toolFailed: tool => `Échec de ${tool}`,\n event: label => `Événement : ${label}`,\n reasoning: 'Réflexion',\n thinkingStep: 'Réflexion',\n steps: count => `${count} ${count === 1 ? 'étape' : 'étapes'}`,\n toolRunning: 'En cours',\n toolCompleted: 'Terminé',\n toolFailedStatus: 'Échec',\n toolUnfinished: 'Sans résultat',\n arguments: 'Arguments',\n result: 'Résultat',\n error: 'Erreur',\n openImageResult: 'Ouvrir l’image du résultat',\n emptyResult: 'Résultat vide',\n toolActivity: (tool, hint) => {\n const verb = TOOL_VERBS[tool] ?? `Exécution de ${tool}`;\n return hint ? `${verb}${tool === 'bash' ? ' :' : ''} ${hint}` : verb;\n },\n toolNoun: tool => TOOL_NOUNS[tool] ?? tool,\n approvalNeeded: 'Approbation requise',\n needsApproval: 'Votre approbation est nécessaire',\n approve: 'Approuver',\n deny: 'Refuser',\n approved: 'Approuvé',\n denied: 'Refusé',\n notDecided: 'Non décidé : l’exécution a été arrêtée',\n wantsToRun: 'L’agent souhaite exécuter',\n decision: 'Décision',\n stillWritingCall: 'L’agent est encore en train d’écrire cet appel…',\n decidingUnavailable: 'La décision n’est pas disponible dans cette vue.',\n decisionSent: approved => `${approved ? 'Approuvé.' : 'Refusé.'} En attente de l’agent…`,\n decisionRefused: kind => kind === 'denied' ? 'Cette réponse n’a pas été acceptée.' : kind === 'conflict' ? 'Cet appel a déjà été décidé.' : 'Cet appel n’attend plus de décision.',\n decisionNotSent: 'La décision n’a pas pu être envoyée.',\n waitingForAgent: 'En attente de l’agent…',\n question: 'Question',\n questionForYou: 'Une question pour vous',\n notAnswered: 'Sans réponse',\n answered: 'Répondu',\n askedQuestion: 'L’agent a posé une question.',\n answerOptions: 'Réponses possibles',\n ownAnswer: 'Ou écrivez votre propre réponse',\n typeAnswer: 'Écrivez votre réponse',\n yourAnswerInput: 'Votre réponse',\n run: 'Exécution',\n runStatus: status => ({ queued: 'En attente', running: 'En cours', completed: 'Terminée', failed: 'Échouée' })[status],\n runElapsed: time => `En cours depuis ${time}`,\n runDuration: time => `Durée : ${time}`,\n runRefresh: 'Actualiser',\n runNotLoaded: 'L’exécution n’a pas pu être chargée.',\n subagentStatus: status => ({ running: 'En cours', done: 'Terminé', failed: 'Échoué', cancelled: 'Arrêté' })[status],\n subagentTime: 'Durée',\n subagentToolCalls: count => `${count} ${count === 1 ? 'appel' : 'appels'} d’outil`,\n subagentTokens: (input, output) => `${input} jetons en entrée, ${output} jetons en sortie`,\n subagentConversation: id => `Ouvrir la conversation ${id}`,\n skipped: '(pas de réponse)',\n answer: 'Répondre',\n stillWritingQuestion: 'L’agent est encore en train d’écrire cette question…',\n answeringUnavailable: 'La réponse n’est pas disponible dans cette vue.',\n answerRefused: kind => kind === 'denied' ? 'Cette réponse n’a pas été acceptée.' : kind === 'conflict' ? 'Cette question a déjà reçu une réponse.' : 'Cette question n’attend plus de réponse.',\n answerNotSent: 'La réponse n’a pas pu être envoyée.',\n yourAnswer: 'Votre réponse :',\n artifact: 'Document',\n artifactTypes: { markdown: 'Document', html: 'Page HTML', svg: 'Image SVG', code: 'Code', canvas: 'Canevas' },\n opening: 'Ouverture…',\n viewing: 'Affiché',\n version: ordinal => `Version ${ordinal}`,\n runCommand: (name, execute) => `${execute ? 'Exécuter' : 'Insérer'} la commande /${name}`,\n taskStatus: 'État de la tâche',\n activityFailed: (count, tools) => `${count > 1 ? `${count} étapes ont échoué` : 'Une étape a échoué'} : ${tools}`,\n activityUsed: (summary, stopped) => `${stopped ? 'Arrêté · ' : 'Utilisé : '}${summary}`,\n activityThought: 'Réflexion terminée',\n activityMany: steps => `${steps} étapes réalisées`,\n open: 'Ouvrir',\n openArtifact: (title, version) => version === undefined ? `Ouvrir ${title}` : `Ouvrir ${title}, version ${version}`,\n};\n",
|
|
153
|
+
"content": "/* The chat's words in French, the `locale=\"fr\"` base of `PiChat`, `AmbientChat` and `AgentWorkspace`. Typed as `ChatLabels`, so a key added in English must be added here too. */\nimport { formatClock, formatTimestamp } from './labels';\nimport type { ChatLabels } from './labels';\n\nconst TOOL_VERBS: Readonly<Record<string, string>> = { read: 'Lecture', write: 'Écriture', edit: 'Modification', ls: 'Liste', list_files: 'Liste', find: 'Recherche', grep: 'Recherche', load_skill: 'Chargement de la compétence', bash: 'Exécution bash' };\nconst TOOL_NOUNS: Readonly<Record<string, string>> = { bash: 'commande', read: 'lecture', write: 'écriture', edit: 'modification', find: 'recherche', grep: 'recherche', ls: 'liste' };\nconst EFFORT_NAMES: Readonly<Record<string, string>> = { off: 'Désactivé', minimal: 'Minimal', low: 'Faible', medium: 'Moyen', high: 'Élevé', xhigh: 'Très élevé', max: 'Maximum' };\nconst EFFORT_DETAILS: Readonly<Record<string, string>> = { off: 'Sans réflexion supplémentaire', minimal: 'Presque aucune réflexion', low: 'Réflexion légère', medium: 'Réflexion équilibrée', high: 'Réflexion approfondie', xhigh: 'Réflexion très approfondie', max: 'Réflexion maximale' };\n\nexport const frenchChatLabels: ChatLabels = {\n title: 'Conversation',\n history: 'Historique',\n artifacts: 'Fichiers',\n search: 'Rechercher',\n live: 'En direct',\n connecting: 'Connexion…',\n reconnecting: 'Reconnexion…',\n loadFailed: 'La conversation n’a pas pu être chargée.',\n disconnected: 'Déconnecté. Les derniers messages reçus restent affichés.',\n reconnect: 'Se reconnecter',\n requiredActions: 'Actions requises dans la conversation',\n messages: 'Messages',\n showEarlier: 'Afficher les messages précédents',\n loading: 'Chargement de la conversation…',\n emptyDescription: 'Commencez une conversation.',\n jumpToLatest: 'Aller au dernier message',\n agentWorking: 'L’agent travaille',\n waitingForAnswer: 'En attente de votre réponse',\n retryPending: 'Nouvelle tentative en attente',\n compacting: 'Résumé du contexte en cours',\n preparing: 'En préparation…',\n sendUnavailable: 'L’envoi n’est pas disponible pour le moment.',\n moreActions: 'Plus d’actions',\n sendUnknown: 'L’envoi n’est pas confirmé. Le message est conservé jusqu’à ce que vous le vérifiiez ou le renvoyiez.',\n checkSubmission: 'Vérifier l’envoi',\n retrySame: 'Renvoyer la même demande',\n stopRequested: 'Arrêt demandé. En attente de confirmation.',\n stopUnconfirmed: 'Arrêt non confirmé. Le travail continue peut-être.',\n steerRefused: 'Le message n’a pas pu être ajouté en cours de réponse ; il est revenu dans la zone de saisie.',\n tooLate: 'Trop tard : ce message a déjà commencé.',\n tooLateToRemove: 'Trop tard pour le retirer : ce message a déjà commencé.',\n queueFailed: 'Le message n’a pas pu être modifié.',\n queueLocked: 'Les messages en attente ne peuvent pas être modifiés ici.',\n waitToSteer: 'Attendez la fin de l’envoi en cours, puis réessayez.',\n steerFailed: 'Le message n’a pas pu être ajouté en cours de réponse.',\n connectionFailed: 'Échec de la connexion',\n actionFailedShort: 'Échec de l’action',\n uploadFailedShort: 'Échec de l’envoi',\n changeRefused: 'La modification a été refusée',\n dismissHint: 'cliquez pour fermer',\n placeholder: 'Écrivez à l’agent…',\n placeholderWorking: 'Mettre un message en attente…',\n messageInput: 'Message',\n send: 'Envoyer',\n sendHint: 'Envoyer (Entrée)',\n stop: 'Arrêter',\n queue: 'Mettre en attente',\n add: 'Ajouter',\n attachFiles: 'Joindre des fichiers',\n takePhoto: 'Prendre une photo',\n cameraShoot: 'Prendre la photo',\n cameraClose: 'Fermer l’appareil photo',\n cameraUnavailable: 'L’appareil photo ne peut pas s’ouvrir sur cette page.',\n cameraFallback: 'Utiliser l’appareil photo du téléphone',\n // Voice input (the microphone button beside the composer)\n voiceRecord: 'Enregistrer un message',\n voiceHold: 'Maintenir pour enregistrer, relâcher pour envoyer',\n voicePauseRecording: 'Mettre l’enregistrement en pause',\n voiceResumeRecording: 'Reprendre l’enregistrement',\n voicePaused: 'Enregistrement en pause',\n voiceRecording: 'Enregistrement',\n voicePreparing: 'Préparation du message vocal…',\n voiceAudioOnly: 'Envoyer l’audio seul',\n voiceDiscard: 'Abandonner',\n voicePlay: 'Lire',\n voicePause: 'Mettre en pause',\n voicePlayFailed: 'La lecture de l’audio a échoué. Réessayez.',\n voiceBlocked: 'Non envoyé. Vérifiez l’avis de la conversation avant de réessayer.',\n voiceUnconfirmed: 'L’envoi n’est pas confirmé. Vérifiez l’avis de la conversation.',\n voiceStop: 'Arrêter l’enregistrement',\n voiceCancel: 'Annuler l’enregistrement',\n voiceTranscribing: 'Transcription…',\n voiceNothingHeard: 'Rien n’a été entendu. Réessayez.',\n voiceTranscribeFailed: 'L’enregistrement n’a pas pu être transcrit. Réessayez.',\n voiceTranscriptUnavailable: 'Message vocal — transcription indisponible.',\n voiceDenied: 'Microphone bloqué. Autorisez-le pour ce site dans le navigateur, puis appuyez de nouveau sur enregistrer.',\n voiceNoMicrophone: 'Aucun microphone trouvé. Branchez-en un, puis appuyez de nouveau sur enregistrer.',\n voiceBusy: 'Le microphone est occupé ou n’a pas pu être ouvert. Fermez les autres applications qui l’utilisent et réessayez.',\n voiceInsecure: 'L’enregistrement exige une page sécurisée (https ou localhost).',\n voiceUnsupported: 'Ce navigateur ne peut pas enregistrer le son ici.',\n voiceStartFailed: 'Le microphone n’a pas pu être démarré.',\n commands: 'Commandes',\n annotate: 'Annoter',\n annotateHint: 'Annoter : cliquez sur ce que vous voulez commenter (Échap pour arrêter)',\n annotateNotes: count => `${count} ${count === 1 ? 'note' : 'notes'}`,\n annotateClear: 'Retirer les notes',\n feedbackSent: count => `${count} ${count === 1 ? 'commentaire' : 'commentaires'} sur la page`,\n remove: name => `Retirer ${name}`,\n dismiss: name => `Fermer ${name}`,\n uploading: name => `Envoi de ${name}…`,\n uploadFailed: (name, error) => `${name} : ${error ?? 'échec de l’envoi'}`,\n preparingAttachments: 'Préparation des pièces jointes…',\n preparingAttachmentsShort: 'Préparation des pièces jointes',\n mentionedFiles: 'Fichiers mentionnés',\n uploads: 'Envois',\n attachments: 'Pièces jointes',\n fileKind: { document: 'Document', image: 'Image', pdf: 'PDF', sheet: 'Tableur', slides: 'Présentation', audio: 'Audio', file: 'Fichier' },\n openFile: name => `Ouvrir ${name}`,\n fileChange: { created: 'Créé', edited: 'Modifié' },\n pastedFileName: stamp => `Texte collé ${stamp}`,\n restorePaste: 'Remettre le texte collé dans le message',\n searchCommands: 'Rechercher une commande',\n filterBySource: 'Filtrer par source',\n allSources: 'Toutes',\n noCommands: 'Aucune commande ne correspond.',\n slashGroup: group => group === 'skills' ? 'compétences' : group === 'built-in' ? 'intégrées' : group,\n skillBadge: 'compétence',\n workspaceFiles: 'Fichiers de l’espace',\n searchFailed: 'La recherche a échoué',\n model: 'Modèle',\n effort: 'Effort',\n effortDefault: 'Par défaut',\n modelAndEffort: 'Modèle et effort',\n modelSummary: (model, effort) => [model && `Modèle : ${model}`, effort && `Effort : ${effort}`].filter(Boolean).join(', '),\n effortName: level => EFFORT_NAMES[level] ?? level,\n effortDetail: level => EFFORT_DETAILS[level],\n queuedMessages: 'Messages en attente',\n steer: 'Envoyer maintenant',\n steerHint: 'Ajouter ce message à la réponse en cours',\n steering: 'Envoi en cours de réponse',\n queued: 'En attente',\n removeQueued: 'Retirer le message en attente',\n removeHint: 'Retirer',\n more: 'Plus',\n edit: 'Modifier',\n sending: 'Envoi',\n notSent: 'Non envoyé',\n messageClock: at => formatClock(at, 'fr'),\n messageTimestamp: at => formatTimestamp(at, 'fr'),\n images: count => `[${count} image${count === 1 ? '' : 's'}]`,\n historyDialog: 'Historique des conversations',\n newConversation: 'Nouvelle',\n untitled: 'Nouvelle conversation',\n closeHistory: 'Fermer l’historique',\n close: 'Fermer',\n earlierInConversation: 'Messages précédents de cette conversation',\n searchConversations: 'Rechercher une conversation',\n archived: 'Archivées',\n current: 'En cours',\n noMatch: query => `Aucune conversation ne correspond à « ${query} ».`,\n noArchived: 'Aucune conversation archivée.',\n noConversations: 'Aucune conversation pour le moment.',\n loadingConversations: 'Chargement des conversations…',\n actionFailed: 'Cela n’a pas fonctionné. Réessayez.',\n rename: 'Renommer',\n renameItem: title => `Renommer ${title}`,\n conversationName: 'Nom de la conversation',\n save: 'Enregistrer',\n archive: 'Archiver',\n restore: 'Restaurer',\n archiveItem: (title, archived) => `${archived ? 'Restaurer' : 'Archiver'} ${title}`,\n delete: 'Supprimer',\n deleteItem: title => `Supprimer ${title}`,\n keep: 'Garder la conversation',\n today: 'Aujourd’hui',\n yesterday: 'Hier',\n previousWeek: 'Les 7 derniers jours',\n older: 'Plus anciennes',\n conversations: 'Conversations',\n earlierMessages: 'Messages précédents',\n readOnly: '(lecture seule)',\n retry: 'Réessayer',\n loadingShort: 'Chargement…',\n loadEarlier: 'Charger les précédents',\n loadEarlierMessages: 'Charger les messages précédents',\n earlierMessagesLoaded: count => `${count} messages précédents chargés`,\n returnToConversation: 'Revenir à la conversation en cours',\n historyFailed: 'L’historique n’a pas pu être chargé. La page précédente reste affichée.',\n loadPage: 'Chargez une page pour parcourir les messages précédents.',\n noRecords: 'Aucun élément sur cette page d’historique.',\n beginning: 'Début de la conversation.',\n justNow: 'à l’instant',\n roleSystem: 'Système',\n roleToolResult: (tool, failed) => `Résultat de l’outil : ${tool}${failed ? ' (échec)' : ''}`,\n roleUser: 'Vous',\n roleAssistant: outcome => outcome === 'interrupted' ? 'Assistant (interrompu)' : outcome === 'failed' ? 'Assistant (échec)' : 'Assistant',\n roleEvent: 'Événement',\n contextBoundary: 'Limite du contexte',\n copy: 'Copier',\n copied: 'Copié',\n copyFailed: 'Échec de la copie',\n failed: 'Échec',\n copyResponse: 'Copier la réponse',\n copyCode: 'Copier le code',\n fork: 'Repartir d’ici',\n forkHint: 'Repartir d’ici : une nouvelle conversation avec les messages jusqu’à cette réponse',\n responseFailed: 'La réponse a échoué',\n interrupted: 'Réponse interrompue',\n answeredBy: model => `répondu par ${model}`,\n answeredFallback: reason => `repli : ${reason}`,\n thinking: 'Réflexion…',\n openImage: 'Ouvrir l’image jointe',\n imageAttachment: 'Image jointe',\n imagePosition: (index, count) => `Image ${index} sur ${count}`,\n previousImage: 'Image précédente',\n nextImage: 'Image suivante',\n systemContext: 'Contexte système',\n toolFailed: tool => `Échec de ${tool}`,\n event: label => `Événement : ${label}`,\n reasoning: 'Réflexion',\n thinkingStep: 'Réflexion',\n steps: count => `${count} ${count === 1 ? 'étape' : 'étapes'}`,\n toolRunning: 'En cours',\n toolCompleted: 'Terminé',\n toolFailedStatus: 'Échec',\n toolUnfinished: 'Sans résultat',\n arguments: 'Arguments',\n result: 'Résultat',\n error: 'Erreur',\n openImageResult: 'Ouvrir l’image du résultat',\n emptyResult: 'Résultat vide',\n toolActivity: (tool, hint) => {\n const verb = TOOL_VERBS[tool] ?? `Exécution de ${tool}`;\n return hint ? `${verb}${tool === 'bash' ? ' :' : ''} ${hint}` : verb;\n },\n toolNoun: tool => TOOL_NOUNS[tool] ?? tool,\n approvalNeeded: 'Approbation requise',\n needsApproval: 'Votre approbation est nécessaire',\n approve: 'Approuver',\n deny: 'Refuser',\n approved: 'Approuvé',\n denied: 'Refusé',\n notDecided: 'Non décidé : l’exécution a été arrêtée',\n wantsToRun: 'L’agent souhaite exécuter',\n decision: 'Décision',\n stillWritingCall: 'L’agent est encore en train d’écrire cet appel…',\n decidingUnavailable: 'La décision n’est pas disponible dans cette vue.',\n decisionSent: approved => `${approved ? 'Approuvé.' : 'Refusé.'} En attente de l’agent…`,\n decisionRefused: kind => kind === 'denied' ? 'Cette réponse n’a pas été acceptée.' : kind === 'conflict' ? 'Cet appel a déjà été décidé.' : 'Cet appel n’attend plus de décision.',\n decisionNotSent: 'La décision n’a pas pu être envoyée.',\n waitingForAgent: 'En attente de l’agent…',\n question: 'Question',\n questionForYou: 'Une question pour vous',\n notAnswered: 'Sans réponse',\n answered: 'Répondu',\n askedQuestion: 'L’agent a posé une question.',\n answerOptions: 'Réponses possibles',\n ownAnswer: 'Ou écrivez votre propre réponse',\n typeAnswer: 'Écrivez votre réponse',\n yourAnswerInput: 'Votre réponse',\n run: 'Exécution',\n runStatus: status => ({ queued: 'En attente', running: 'En cours', completed: 'Terminée', failed: 'Échouée' })[status],\n runElapsed: time => `En cours depuis ${time}`,\n runDuration: time => `Durée : ${time}`,\n runRefresh: 'Actualiser',\n runNotLoaded: 'L’exécution n’a pas pu être chargée.',\n subagentStatus: status => ({ running: 'En cours', done: 'Terminé', failed: 'Échoué', cancelled: 'Arrêté' })[status],\n subagentTime: 'Durée',\n subagentToolCalls: count => `${count} ${count === 1 ? 'appel' : 'appels'} d’outil`,\n subagentTokens: (input, output) => `${input} jetons en entrée, ${output} jetons en sortie`,\n subagentConversation: id => `Ouvrir la conversation ${id}`,\n skipped: '(pas de réponse)',\n answer: 'Répondre',\n stillWritingQuestion: 'L’agent est encore en train d’écrire cette question…',\n answeringUnavailable: 'La réponse n’est pas disponible dans cette vue.',\n answerRefused: kind => kind === 'denied' ? 'Cette réponse n’a pas été acceptée.' : kind === 'conflict' ? 'Cette question a déjà reçu une réponse.' : 'Cette question n’attend plus de réponse.',\n answerNotSent: 'La réponse n’a pas pu être envoyée.',\n questionNotAnswered: 'La question n’a pas reçu de réponse.',\n yourAnswer: 'Votre réponse :',\n artifact: 'Document',\n artifactTypes: { markdown: 'Document', html: 'Page HTML', svg: 'Image SVG', code: 'Code', canvas: 'Canevas' },\n opening: 'Ouverture…',\n viewing: 'Affiché',\n version: ordinal => `Version ${ordinal}`,\n runCommand: (name, execute) => `${execute ? 'Exécuter' : 'Insérer'} la commande /${name}`,\n taskStatus: 'État de la tâche',\n activityFailed: (count, tools) => `${count > 1 ? `${count} étapes ont échoué` : 'Une étape a échoué'} : ${tools}`,\n activityUsed: (summary, stopped) => `${stopped ? 'Arrêté · ' : 'Utilisé : '}${summary}`,\n activityThought: 'Réflexion terminée',\n activityMany: steps => `${steps} étapes réalisées`,\n open: 'Ouvrir',\n openArtifact: (title, version) => version === undefined ? `Ouvrir ${title}` : `Ouvrir ${title}, version ${version}`,\n};\n",
|
|
154
154
|
"type": "registry:lib",
|
|
155
155
|
"target": "components/pi-chat/labels-fr.ts"
|
|
156
156
|
},
|
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.54/r/pi-chat.json",
|
|
12
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.54/r/utils.json",
|
|
13
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.54/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.54/r/pi-app.json",
|
|
13
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.54/r/pi-ambient.json",
|
|
14
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.54/r/app-shell.json",
|
|
15
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.54/r/utils.json"
|
|
16
16
|
],
|
|
17
17
|
"files": [
|
|
18
18
|
{
|
package/r/provider-setup.json
CHANGED