@arcfold/dsh-method-ui 0.0.0-stage → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2 -2
  3. package/THIRD_PARTY_NOTICES.md +11 -0
  4. package/lib/client.js +4237 -0
  5. package/package.json +35 -4
  6. package/src/Brand.mjs +17 -0
  7. package/src/CollaborationView.mjs +56 -0
  8. package/src/DesktopModels.mjs +40 -0
  9. package/src/DesktopSetup.mjs +48 -0
  10. package/src/GoalDock.mjs +130 -0
  11. package/src/GoalWorkList.mjs +52 -0
  12. package/src/ImportDock.mjs +172 -0
  13. package/src/MethodDock.mjs +145 -0
  14. package/src/ModelSettings.mjs +144 -0
  15. package/src/OpportunityDock.mjs +66 -0
  16. package/src/UnderstandingDock.mjs +117 -0
  17. package/src/browse-scope.mjs +6 -0
  18. package/src/browse-source.mjs +31 -0
  19. package/src/client.mjs +138 -0
  20. package/src/desktop-setup-host.mjs +30 -0
  21. package/src/desktop-workspace.mjs +8 -0
  22. package/src/goal-host.mjs +133 -0
  23. package/src/goal-locales.mjs +78 -0
  24. package/src/goal-source.mjs +72 -0
  25. package/src/import-host.mjs +98 -0
  26. package/src/import-locales.mjs +126 -0
  27. package/src/import-source.mjs +83 -0
  28. package/src/index.mjs +127 -0
  29. package/src/locales.mjs +153 -0
  30. package/src/main-session.mjs +7 -0
  31. package/src/method-source.mjs +82 -0
  32. package/src/model-host.mjs +56 -0
  33. package/src/model-settings-locales.mjs +91 -0
  34. package/src/model-settings-source.mjs +119 -0
  35. package/src/opportunity-host.mjs +118 -0
  36. package/src/opportunity-locales.mjs +34 -0
  37. package/src/opportunity-source.mjs +56 -0
  38. package/src/style.css +99 -0
  39. package/src/tree-client.mjs +15 -0
  40. package/src/tree-host.mjs +39 -0
  41. package/src/tree-source.mjs +65 -0
  42. package/src/tree-style.css +19 -0
  43. package/src/tree-view.mjs +278 -0
  44. package/src/understanding-host.mjs +123 -0
  45. package/src/understanding-locales.mjs +38 -0
  46. package/src/understanding-source.mjs +64 -0
package/package.json CHANGED
@@ -1,6 +1,37 @@
1
1
  {
2
2
  "name": "@arcfold/dsh-method-ui",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.4.0",
4
+ "type": "module",
5
+ "main": "./src/index.mjs",
6
+ "exports": {
7
+ ".": "./src/index.mjs",
8
+ "./client": "./lib/client.js",
9
+ "./package.json": "./package.json"
10
+ },
11
+ "dsh": {
12
+ "client": {
13
+ "platform": "web",
14
+ "inject": [
15
+ "@deepseek-ai/dsh-client-connection",
16
+ "@deepseek-ai/dsh-client-locale",
17
+ "@deepseek-ai/dsh-client-ui-renderer",
18
+ "@deepseek-ai/dsh-client-ui-conversation",
19
+ "@deepseek-ai/dsh-client-file-upload",
20
+ "@deepseek-ai/dsh-client-ui-sidebar"
21
+ ]
22
+ }
23
+ },
24
+ "peerDependencies": {
25
+ "@deepseek-ai/cordis": "4.1.0",
26
+ "@deepseek-ai/dsh-typert-protocol": "0.2.2-rc.1",
27
+ "react": "18.3.1"
28
+ },
29
+ "files": [
30
+ "src",
31
+ "lib",
32
+ "README.md",
33
+ "LICENSE",
34
+ "THIRD_PARTY_NOTICES.md"
35
+ ],
36
+ "license": "MIT"
37
+ }
package/src/Brand.mjs ADDED
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+
3
+ // Visuals only: the native parent owns navigation, permissions and focus.
4
+ export function BrandMark({ size = 24, className }) {
5
+ return React.createElement('svg', { width: size, height: size, viewBox: '0 0 32 32',
6
+ className, fill: 'none', 'aria-hidden': true, focusable: false,
7
+ style: { flexShrink: 0, color: 'inherit' } },
8
+ React.createElement('path', { d: 'M5 26 16 5l11 21M10 19h12M16 5v21',
9
+ stroke: 'currentColor', strokeWidth: 2.4, strokeLinecap: 'round', strokeLinejoin: 'round' }));
10
+ }
11
+ export function BrandName() { return React.createElement('span', null, 'Arcfold'); }
12
+ export function installBrand(ctx) {
13
+ for (const [name, component] of [
14
+ ['sidebar.brand.mark', BrandMark], ['sidebar.brand.name', BrandName],
15
+ ['conversation.hero.brand.mark', BrandMark],
16
+ ]) ctx.slots.inject(name, () => ctx.slots.register({ name }, component));
17
+ }
@@ -0,0 +1,56 @@
1
+ import React, { useEffect, useId, useRef, useState } from 'react';
2
+ import { OpportunityDock } from './OpportunityDock.mjs';
3
+ import { MethodDock } from './MethodDock.mjs';
4
+ import { GoalDock } from './GoalDock.mjs';
5
+ import { UnderstandingDock } from './UnderstandingDock.mjs';
6
+ import { ImportDock } from './ImportDock.mjs';
7
+ import { ModelSettings } from './ModelSettings.mjs';
8
+
9
+ const h = React.createElement;
10
+ function Panels(props) {
11
+ return h(React.Fragment, null, h(OpportunityDock, props), h(MethodDock, props),
12
+ h(GoalDock, props), h(UnderstandingDock, props), h(ImportDock, props));
13
+ }
14
+ function Models(props) {
15
+ return h(ModelSettings, { ...props, onRefresh: props.onModelsRefresh });
16
+ }
17
+ function Dialog({ title, onClose, children, t }) {
18
+ const ref = useRef(null), titleId = useId();
19
+ useEffect(() => {
20
+ const dialog = ref.current;
21
+ dialog.showModal();
22
+ return () => { dialog.close(); };
23
+ }, []);
24
+ return h('dialog', { ref, className: 'arcfold-dialog', 'aria-labelledby': titleId,
25
+ onCancel: event => { event.preventDefault(); onClose(); } },
26
+ h('header', { className: 'arcfold-toolbar' },
27
+ h('h2', { id: titleId }, title),
28
+ h('button', { type: 'button', onClick: onClose, autoFocus: true }, t('collaborationClose'))),
29
+ children);
30
+ }
31
+ export function CollaborationView(props) {
32
+ const scope = props.useBrowseScope ? props.useBrowseScope(value => value) : { viewScope: 'all', epoch: 0 };
33
+ const [modelsOpen, setModelsOpen] = useState(false);
34
+ useEffect(() => { setModelsOpen(false); }, [props.opportunitySessionId]);
35
+ return h('section', { className: 'arcfold-view', 'aria-label': props.t('collaborationTitle') },
36
+ h('header', { className: 'arcfold-toolbar' },
37
+ h('h2', null, props.t('collaborationTitle')),
38
+ h('button', { type: 'button', onClick: () => props.onOpenModels ? props.onOpenModels() : setModelsOpen(true) }, props.t('modelsTitle'))),
39
+ h('div', { className: 'arcfold-view-scope', role: 'group', 'aria-label': props.t('collaborationViewScope') },
40
+ ['all', 'current'].map(viewScope => h('button', { key: viewScope, type: 'button',
41
+ 'aria-pressed': scope.viewScope === viewScope,
42
+ onClick: () => props.onViewScopeChange?.(viewScope) }, props.t(viewScope === 'all' ? 'collaborationAll' : 'collaborationCurrent')))),
43
+ h(Panels, { ...props, viewScope: scope.viewScope, key: `${props.opportunitySessionId}:${scope.epoch}` }),
44
+ modelsOpen && h(Dialog, { title: props.t('modelsTitle'), t: props.t, onClose: () => setModelsOpen(false) }, h(Models, props)));
45
+ }
46
+ export function CollaborationHero(props) {
47
+ const [panel, setPanel] = useState(null);
48
+ // A session switch must not carry an open dialog or unfinished draft forward.
49
+ useEffect(() => { setPanel(null); }, [props.opportunitySessionId]);
50
+ return h('div', { className: 'arcfold-hero-entry' },
51
+ h('button', { type: 'button', onClick: () => setPanel('collaboration') }, props.t('collaborationTitle')),
52
+ h('button', { type: 'button', onClick: () => setPanel('models') }, props.t('modelsTitle')),
53
+ panel && h(Dialog, { title: props.t(panel === 'models' ? 'modelsTitle' : 'collaborationTitle'),
54
+ t: props.t, onClose: () => setPanel(null) },
55
+ panel === 'models' ? h(Models, props) : h(CollaborationView, { ...props, onOpenModels: () => setPanel('models') })));
56
+ }
@@ -0,0 +1,40 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ const h = React.createElement, REF = 'DEEPSEEK_API_KEY';
3
+
4
+ /** Secrets stay in this local input until submitted to DSH's credential API. */
5
+ export function DesktopModels({ rpc }) {
6
+ const [info, setInfo] = useState(null), [secret, setSecret] = useState('');
7
+ const [busy, setBusy] = useState(false), [message, setMessage] = useState('');
8
+ async function refresh() {
9
+ const result = await rpc.call('/api', 'credentials/describe', { args: { refs: [REF] } });
10
+ if (result?.ok !== true) throw new Error('无法读取密钥状态。');
11
+ const value = result.value?.[REF];
12
+ setInfo({ configured: value?.configured === true, writable: value?.writable === true });
13
+ }
14
+ useEffect(() => { let active = true;
15
+ void rpc.call('/api', 'credentials/describe', { args: { refs: [REF] } }).then(result => {
16
+ if (!active) return;
17
+ if (result?.ok !== true) throw new Error('unavailable');
18
+ const value = result.value?.[REF]; setInfo({ configured: value?.configured === true, writable: value?.writable === true });
19
+ }).catch(() => { if (active) setMessage('无法读取密钥状态,请稍后重试。'); });
20
+ return () => { active = false; };
21
+ }, [rpc]);
22
+ async function save() {
23
+ if (!info?.writable || !secret.trim() || busy) return;
24
+ setBusy(true); setMessage('正在保存…');
25
+ try {
26
+ const result = await rpc.call('/api', 'credentials/set', { args: { ref: REF, value: secret } });
27
+ if (result?.ok !== true) throw new Error('unconfirmed');
28
+ await refresh(); setMessage('已保存到 Desktop 的凭据服务。');
29
+ } catch { setMessage('保存结果尚未确认,请刷新状态后再决定。'); }
30
+ finally { setSecret(''); setBusy(false); }
31
+ }
32
+ return h('section', { className: 'arcfold-desktop-setup' }, h('h2', null, 'DeepSeek'),
33
+ h('p', null, '密钥由 DSH 统一保存和使用;Arcfold 不把密钥写进自身数据或费用记录。'),
34
+ h('p', null, !info ? '正在读取密钥状态…' : info.configured ? 'DeepSeek 密钥已配置。' : '尚未配置 DeepSeek 密钥。'),
35
+ h('label', null, 'DeepSeek API 密钥', h('input', { type: 'password', autoComplete: 'off', value: secret,
36
+ disabled: !info?.writable || busy, onChange: event => setSecret(event.target.value) })),
37
+ h('button', { type: 'button', disabled: !info?.writable || !secret.trim() || busy, onClick: () => void save() }, '保存密钥'),
38
+ h('button', { type: 'button', disabled: busy, onClick: () => void refresh().catch(() => setMessage('无法读取密钥状态。')) }, '刷新状态'),
39
+ message && h('p', { role: 'status' }, message));
40
+ }
@@ -0,0 +1,48 @@
1
+ import React, { useState } from 'react';
2
+
3
+ const h = React.createElement;
4
+ export function DesktopSetup({ initial, rpc, openWorkspace }) {
5
+ const [info, setInfo] = useState(initial), [checked, setChecked] = useState(false);
6
+ const [busy, setBusy] = useState(false), [message, setMessage] = useState('');
7
+ async function refresh() {
8
+ const reply = await rpc.call('/api', 'arcfoldDesktop/status', { args: { request: {} } });
9
+ if (reply?.ok !== true) throw new Error('无法读取设置状态。');
10
+ setInfo(reply.value); return reply.value;
11
+ }
12
+ async function open(path) {
13
+ if (typeof openWorkspace !== 'function') throw new Error('DSH 工作区服务尚未就绪。');
14
+ await openWorkspace(path); globalThis.location.reload();
15
+ }
16
+ async function start() {
17
+ if (!checked || busy) return;
18
+ setBusy(true); setMessage('正在创建独立数据并启动 Arcfold…');
19
+ let initialized = false;
20
+ try {
21
+ const result = await rpc.call('/api', 'arcfoldDesktop/start', { args: { request: { confirmed: true } } });
22
+ if (result?.ok !== true) throw new Error('设置结果尚未确认。');
23
+ setInfo(result.value);
24
+ if (result.value?.state === 'ready') { initialized = true; setMessage('设置完成,正在打开独立工作区。'); await open(result.value.workspace); return; }
25
+ setMessage('设置尚未完成,请查看当前状态。');
26
+ } catch { setMessage(initialized ? '设置已完成,但工作区未能打开。请重试打开工作区。' : '设置结果尚未确认,请刷新状态,不要重复提交。'); try { await refresh(); } catch {} }
27
+ finally { setBusy(false); }
28
+ }
29
+ const refreshButton = h('button', { type: 'button', onClick: () => void refresh().catch(() => setMessage('无法读取设置状态。')) }, '刷新状态');
30
+ if (info.state !== 'needs_setup') return h('section', { className: 'arcfold-desktop-setup' },
31
+ h('h2', null, 'Arcfold 设置'), h('p', null, info.state === 'configured' ? '正在读取已保存的设置。' :
32
+ info.state === 'ready' ? 'Arcfold 已就绪。' : 'Arcfold 尚未完成启动,请保留已有数据并检查设置。'),
33
+ info.activationError && h('p', { role: 'alert' }, info.activationError), message && h('p', { role: 'status' }, message),
34
+ info.state === 'ready' && h('button', { type: 'button', disabled: busy, onClick: () => { setBusy(true); void open(info.workspace).catch(() => setMessage('无法打开工作区,请稍后重试。')).finally(() => setBusy(false)); } }, '打开 Arcfold 工作区'), refreshButton);
35
+ return h('section', { className: 'arcfold-desktop-setup' },
36
+ h('h2', null, '开始使用 Arcfold'),
37
+ h('p', null, 'Arcfold 会在 Desktop 中保存独立的目标、经历和费用记录。其他版本已有的数据不会自动导入。'),
38
+ h('p', null, '工作区:', h('code', null, info.workspace)),
39
+ h('p', null, '数据位置:', h('code', null, info.dataRoot)),
40
+ h('p', null, '主对话和与其相关的后台处理使用当前对话选择的模型;不同后台任务保留各自的思考设置。完成后可在“设置 → 模型”中配置 DeepSeek 密钥。'),
41
+ h('p', null, '跨多段对话自动整理时,使用最新相关对话的模型;没有关联对话时使用默认模型。'),
42
+ h('p', null, '模型服务会产生费用。这里记录请求和估算费用,但不设本地消费上限;实际扣费以服务商为准。'),
43
+ h('p', null, '当前估算价格资料核对日期:', info.priceReviewedAt, '。'),
44
+ h('button', { type: 'button', 'aria-pressed': checked, onClick: () => setChecked(value => !value) },
45
+ checked ? '已确认:模型调用可能产生费用,本机不设消费上限' : '确认:模型调用可能产生费用,本机不设消费上限'),
46
+ h('div', null, h('button', { type: 'button', disabled: !checked || busy, onClick: () => void start() }, busy ? '正在设置…' : '创建并开始'), ' ', refreshButton),
47
+ message && h('p', { role: 'status' }, message));
48
+ }
@@ -0,0 +1,130 @@
1
+ import { presentationLabel } from './locales.mjs';
2
+ import { GoalWorkList } from './GoalWorkList.mjs';
3
+ import React, { useRef, useState } from 'react';
4
+ const h = React.createElement;
5
+ const content = value => typeof value === 'string' ? value : value?.content ?? '';
6
+ const version = goal => JSON.stringify([goal.id, goal.revision, goal.definitionVersion]);
7
+
8
+ function GoalCard({ detail, works, canDiscuss, onGoalDiscuss, onGoalRefresh, onAction, workBusy, t }) {
9
+ const goal = detail.goal, allowed = detail.availability?.allowed === true;
10
+ const [draft, setDraft] = useState(''), [draftVersion, setDraftVersion] = useState(null);
11
+ const [busy, setBusy] = useState(false), [error, setError] = useState(null), [notice, setNotice] = useState(null);
12
+ const pending = useRef(false), retryIds = useRef(new Map());
13
+ const staleDraft = Boolean(draft && draftVersion !== version(goal));
14
+ async function discuss() {
15
+ if (pending.current || !draft.trim() || staleDraft || !canDiscuss || !(allowed || detail.repairAllowed)) return;
16
+ const request = { goalId: goal.id, expectedRevision: goal.revision, definitionVersion: goal.definitionVersion,
17
+ mode: allowed ? 'discuss' : 'repair', text: draft };
18
+ const key = JSON.stringify(request);
19
+ if (!retryIds.current.has(key)) retryIds.current.set(key, globalThis.crypto.randomUUID());
20
+ request.requestId = retryIds.current.get(key);
21
+ pending.current = true; setBusy(true); setError(null); setNotice(null);
22
+ try {
23
+ const result = await onGoalDiscuss(request), admission = result.binding?.admission?.state;
24
+ setNotice(t(admission === 'accepted' ? 'goalAccepted' : admission === 'failed' ? 'goalFailed' : 'goalUnknown'));
25
+ await onGoalRefresh();
26
+ } catch (failure) {
27
+ setError(failure.remote ? failure.message : `${failure.message || t('failed')} ${t('goalUnknown')}`);
28
+ if (['revision_conflict', 'goal_source_invalid', 'goal_unavailable'].includes(failure.code)) await onGoalRefresh();
29
+ } finally { pending.current = false; setBusy(false); }
30
+ }
31
+ const text = (key, value) => h('div', { key, className: 'text' }, content(value));
32
+ const label = (kind, value) => presentationLabel(kind, value, t);
33
+ return h('article', { className: 'goal-card', id: `arcfold-goal-${goal.id}`, 'aria-label': goal.title || t('goalUnavailable') },
34
+ h('header', { className: 'goal-card-heading' }, h('h3', null, goal.title || t('goalUnavailable')),
35
+ h('span', { className: 'goal-status-badge' }, label('goal', goal.status))),
36
+ detail.browse?.related === false && h('p', { className: 'meta' }, t('browseReturnToOrigin')),
37
+ !allowed && h('p', { role: 'alert' }, detail.availability?.message || t('goalUnavailable')),
38
+ allowed && h('div', { className: 'goal-outcome text' }, content(goal.outcome)),
39
+ h('div', { className: 'goal-card-section' },
40
+ works.length ? h(GoalWorkList, { works, title: t('goalCurrentWork'), onAction, busy: workBusy, t })
41
+ : h(React.Fragment, null, h('h4', null, t('goalCurrentWork')), h('p', { className: 'meta' }, t('goalNoCurrentWork')))),
42
+ allowed && h('div', { className: 'goal-card-section goal-progress' },
43
+ h('h4', null, t('goalProgress')),
44
+ text('progress', detail.coverage?.progressUnavailable > 0 ? t('goalProgressUnavailable') : content(goal.progress) || t('goalNoProgress'))),
45
+ h('details', { className: 'goal-card-evidence' }, h('summary', null, t('goalEvidence')),
46
+ h('p', { className: 'meta' }, `${t('goalDefinition')} ${goal.definitionVersion} · ${t('revision')} ${goal.revision}`),
47
+ allowed && h(React.Fragment, null,
48
+ h('h4', null, t('goalRelations')),
49
+ !(detail.relations || []).length && h('p', { className: 'meta' }, t('goalNoRelations')),
50
+ (detail.relations || []).map((item, index) => h('p', { className: 'meta', key: index },
51
+ `${label('relation', item.type)} · ${item.targetId} · ${item.state === 'needs_review' ? t('goalRelationReview') : item.state === 'unavailable' ? t('goalRelationUnavailable') : ''}`)),
52
+ h('h4', null, t('sources')),
53
+ (detail.sources || []).map(source => h('div', { className: 'source', key: source.id },
54
+ h('p', { className: 'meta' }, `${source.id} · ${label('role', source.role)} · ${t('revision')} ${source.revision} · ${source.occurredAt || ''}`),
55
+ text('content', source.content), h('p', { className: 'meta' }, JSON.stringify(source.provenance || null))))),
56
+ (detail.links || []).map(link => h('div', { className: 'source', key: link.id },
57
+ h('p', { className: 'meta' }, `${link.sessionId} · ${t('turn')} ${link.turn} · ${t('goalDefinition')} ${link.definitionVersion} · ${label('outcome', link.outcome?.outcome ?? 'pending')} · ${t('goalUnassessed')}`),
58
+ h('p', { className: 'meta' }, t('goalContribution')),
59
+ text('contribution', link.contribution?.available === true ? link.contribution.reason : t('goalContributionUnavailable')))),
60
+ !(detail.links || []).length && !works.length && h('p', { className: 'meta' }, t('goalNoLinks')),
61
+ (detail.discussions || []).length > 0 && h('div', null,
62
+ h('h4', null, `${t('goalHistory')} · ${detail.discussions.length}/${detail.discussionsTotal ?? detail.discussions.length}`),
63
+ detail.discussions.map(item => h('div', { className: 'source', key: item.id },
64
+ h('p', { className: 'meta' }, `${item.requestId} · ${t('goalAdmission')}: ${label('admission', item.admission?.state ?? 'unknown')}`),
65
+ text('submitted', item.content?.map(part => part.text || '').join('') || ''),
66
+ item.admission?.state !== 'accepted' && h('p', null, t('goalUnknown')))))),
67
+ (allowed || detail.repairAllowed) && h('details', { className: 'goal-discussion' },
68
+ h('summary', null, t('goalCorrection')),
69
+ h('p', { className: 'meta' }, t(allowed ? 'goalDiscussHelp' : 'goalRepairHelp')),
70
+ !canDiscuss && h('p', { className: 'meta' }, t('goalMainRequired')),
71
+ staleDraft && h('p', { role: 'alert' }, t('goalDraftChanged')),
72
+ error && h('p', { role: 'alert' }, error), notice && h('p', { role: 'status' }, notice),
73
+ h('label', null, t('goalDiscussLabel'), h('textarea', { value: draft, maxLength: 24000, disabled: busy,
74
+ onChange: event => { setDraft(event.target.value); setDraftVersion(version(goal)); } })),
75
+ h('button', { type: 'button', onClick: () => { void discuss(); }, disabled: busy || staleDraft || !draft.trim() || !canDiscuss },
76
+ t(allowed ? 'goalDiscuss' : 'goalRepair'))));
77
+ }
78
+
79
+ export function GoalDock({ useGoals, onGoalDiscuss, onGoalStopWork, onGoalResumeWork, onGoalRefresh, onGoalPage, t }) {
80
+ const state = useGoals(value => value);
81
+ const [expanded, setExpanded] = useState(false), [busy, setBusy] = useState(false);
82
+ const [error, setError] = useState(null), [notice, setNotice] = useState(null);
83
+ const pending = useRef(false), retryIds = useRef(new Map());
84
+ async function controlWork(action, work) {
85
+ const control = action === 'resume' ? onGoalResumeWork : onGoalStopWork;
86
+ if (pending.current || !control) return;
87
+ const request = { workId: work.id, nativeGoalId: work.nativeGoalId, nativeRevision: work.nativeRevision };
88
+ const key = JSON.stringify({ action, ...request });
89
+ if (!retryIds.current.has(key)) retryIds.current.set(key, globalThis.crypto.randomUUID());
90
+ request.requestId = retryIds.current.get(key);
91
+ pending.current = true; setBusy(true); setError(null); setNotice(null);
92
+ try {
93
+ const result = await control(request);
94
+ if (result?.state === 'applied') {
95
+ retryIds.current.delete(key); setNotice(t(action === 'resume' ? 'goalWorkResumed' : 'goalWorkStopped'));
96
+ } else setNotice(t('goalWorkUnknown'));
97
+ } catch (failure) {
98
+ if (failure.remote) retryIds.current.delete(key);
99
+ setError(`${failure.message || t('failed')}${failure.remote ? '' : ` ${t('goalWorkUnknown')}`}`);
100
+ await onGoalRefresh();
101
+ } finally { pending.current = false; setBusy(false); }
102
+ }
103
+ // The existing list read already includes full goal details and related work.
104
+ // Keep one card per goal; no second detail fetch or duplicate goal navigation.
105
+ const works = [...new Map([...state.items.flatMap(item => item.works || []), ...(state.works || [])]
106
+ .map(work => [work.id, work])).values()];
107
+ const belongs = (work, id) => work.relation?.state === 'current' && work.relation.goalId === id;
108
+ const others = works.filter(work => !state.items.some(item => belongs(work, item.goal.id)));
109
+ const button = (label, action, disabled = false, props = {}) => h('button', { type: 'button', disabled,
110
+ onClick: () => { void action(); }, ...props }, label);
111
+ return h('section', { className: 'arcfold-methods arcfold-goals', 'aria-label': t('goalTitle') },
112
+ h('header', null, h('strong', null, `${t('goalTitle')} · ${state.total}`),
113
+ button(expanded ? t('goalHide') : t('goalOpen'), () => setExpanded(!expanded), false, { 'aria-expanded': expanded }),
114
+ button(t('refresh'), onGoalRefresh)),
115
+ state.loading && h('p', { role: 'status' }, t('goalLoading')),
116
+ state.error && h('p', { role: 'alert' }, state.error),
117
+ // Collapsing the panel preserves unsent drafts and each card's request identity.
118
+ h('div', { className: 'detail', hidden: !expanded },
119
+ h('p', { className: 'meta' }, t('goalBoundary')),
120
+ error && h('p', { role: 'alert' }, error), notice && h('p', { role: 'status' }, notice),
121
+ h('div', { className: 'goal-cards' }, state.items.map(item => h(GoalCard, { key: item.goal.id, detail: item,
122
+ works: works.filter(work => belongs(work, item.goal.id)), canDiscuss: state.canDiscuss === true && item.browse?.related !== false,
123
+ onGoalDiscuss, onGoalRefresh, onAction: controlWork, workBusy: busy, t }))),
124
+ !state.loading && !state.error && !state.items.length && h('p', null, t('goalEmpty')),
125
+ others.length > 0 && h('details', { className: 'goal-other-work' },
126
+ h('summary', null, `${t('goalOtherWork')} · ${others.length}`), h(GoalWorkList, { works: others, onAction: controlWork, busy, t })),
127
+ (state.offset > 0 || state.nextOffset != null) && h('nav', null,
128
+ button(t('previous'), () => onGoalPage(Math.max(0, state.offset - 20)), state.offset === 0),
129
+ button(t('next'), () => onGoalPage(state.nextOffset), state.nextOffset == null))));
130
+ }
@@ -0,0 +1,52 @@
1
+ import React from 'react';
2
+ const h = React.createElement;
3
+
4
+ function workStatus(work, t) {
5
+ if (work.stage) {
6
+ const keys = { waiting: 'goalStageWaiting', completed: 'goalStageComplete', paused: 'goalWorkPaused', needs_review: 'goalStageReview' };
7
+ if (Object.hasOwn(keys, work.stage.status)) return t(keys[work.stage.status]);
8
+ } else if (work.phase === 'active') return t('goalStageReview');
9
+ if (work.phase === 'active') return t(work.activation === 'armed' ? 'goalWorkEnabled' :
10
+ work.activation === 'disarmed' ? 'goalWorkDisarmed' : 'presentationUnknown');
11
+ const keys = { paused: 'goalWorkPaused', blocked: 'goalWorkBlocked', complete: 'goalWorkComplete', cleared: 'goalWorkCleared' };
12
+ return t(Object.hasOwn(keys, work.phase) ? keys[work.phase] : 'presentationUnknown');
13
+ }
14
+
15
+ // Arcfold stage decisions and the corresponding native execution are one view.
16
+ export function GoalWorkList({ works = [], title, onAction, busy, t }) {
17
+ if (!works.length) return null;
18
+ return h('section', { className: 'goal-work-list', 'aria-label': t('goalWorkTitle') },
19
+ title && h('h4', null, title),
20
+ works.map(work => {
21
+ const relation = work.relation;
22
+ const linked = relation?.state === 'current';
23
+ const blocker = work.blockedReason;
24
+ const reasonKey = ({ 'round-limit': 'goalBlockRoundLimit', 'queue-failed': 'goalBlockQueueFailed',
25
+ 'prompt-rejected': 'goalBlockPromptRejected' })[blocker?.code];
26
+ return h('div', { className: 'goal-work', key: work.id },
27
+ h('span', { className: 'goal-status-badge' }, workStatus(work, t)),
28
+ work.otherSession && h('p', { className: 'meta' }, t('browseReturnToOrigin')),
29
+ h('p', { className: 'text' }, work.stage?.available === false ? t('goalStageReview') :
30
+ work.stage?.objective || (linked ? relation.reason : work.objective)),
31
+ work.stage?.reason && h('p', { className: 'meta' }, work.stage.reason),
32
+ !relation && h('p', { className: 'meta' }, t('goalWorkUnlinked')),
33
+ relation && !linked && h('p', { className: 'meta' },
34
+ t(relation.state === 'needs_review' ? 'goalWorkRelationReview' : 'goalRelationUnavailable')),
35
+ work.phase === 'blocked' && h('div', { className: 'goal-work-blocker' }, h('h5', null, t('goalBlockReason')),
36
+ h('p', { className: 'text' }, reasonKey ? t(reasonKey) : blocker?.message || t('goalBlockUnknown'))),
37
+ work.canStop && onAction && h('button', { type: 'button', disabled: busy,
38
+ onClick: () => { void onAction('stop', work); } }, t('goalWorkStop')),
39
+ work.canResume && onAction && h('button', { type: 'button', disabled: busy,
40
+ onClick: () => { void onAction('resume', work); } }, t('goalWorkResume')),
41
+ h('details', { className: 'goal-work-details' }, h('summary', null, t('goalWorkOriginal')),
42
+ h('p', { className: 'text' }, work.stage?.available === false ? t('goalStageReview') : work.stage?.objective || work.objective),
43
+ work.stage?.doneWhen && h('p', { className: 'text' }, `${t('goalStageDoneWhen')} ${work.stage.doneWhen}`),
44
+ work.stage?.boundaries && h('p', { className: 'text' }, `${t('goalStageBoundaries')} ${work.stage.boundaries}`),
45
+ work.phase === 'blocked' && blocker?.message && h('p', { className: 'text' }, blocker.message),
46
+ h('p', { className: 'meta' }, `${t('goalWorkRounds')} ${work.roundsStarted} / ${work.maxGoalRounds}`),
47
+ h('p', { className: 'meta' }, t(work.provenance === 'verified-native-delegation' ? 'goalWorkVerified' :
48
+ work.provenance === 'source-unavailable' ? 'goalWorkSourceUnavailable' : 'goalWorkLegacy')),
49
+ h('p', { className: 'meta' }, `${work.sessionId} · ${work.nativeGoalId} · ${t('revision')} ${work.nativeRevision}`),
50
+ relation && h('p', { className: 'meta' }, `${relation.goalId} · ${t('goalDefinition')} ${relation.definitionVersion}`)));
51
+ }));
52
+ }
@@ -0,0 +1,172 @@
1
+ import { importReasonKeys } from './import-locales.mjs';
2
+ import React, { useEffect, useRef, useState } from 'react';
3
+ import { IMPORT_MAX_FILE_BYTES } from './import-source.mjs';
4
+ const h = React.createElement;
5
+ const statusKeys = { preparing:'importPreparingStatus', ready:'importReady', failed:'importFailed', confirmed:'importConfirmed', withdrawn:'importWithdrawn' };
6
+ const eligibilityKeys = { enabled:'importEnabled', trial:'importTrial', paused:'importPaused', history:'importHistory', needs_review:'importNeedsReview' };
7
+ const scopeKeys = { global:'importGlobal', mapped:'importMapped', historical:'importHistoricalScope' };
8
+ export function ImportDock({ useImports, importSessionId, onImportUpload, onImportPrepare, onImportDetails,
9
+ onImportConfirm, onImportWithdraw, onImportRefresh, onImportPage, t }) {
10
+ const state = useImports(value => value);
11
+ const [expanded, expand] = useState(false), [file, setFile] = useState(null), [receipt, setReceipt] = useState(null);
12
+ const [detail, setDetail] = useState(null), [scope, setScope] = useState(''), [text, setText] = useState(null);
13
+ const [busy, setBusy] = useState(false), [loading, setLoading] = useState(false), [progress, setProgress] = useState(null);
14
+ const [error, setError] = useState(''), [notice, setNotice] = useState('');
15
+ const mounted = useRef(true), generation = useRef(0), pending = useRef(false), identities = useRef(new Map());
16
+ useEffect(() => { mounted.current = true; return () => { mounted.current = false; generation.current++; }; }, []);
17
+ useEffect(() => {
18
+ generation.current++; setDetail(null); setText(null); setReceipt(null); setScope(''); setLoading(false); setProgress(null);
19
+ setError(''); if (pending.current) setNotice(t('importUnknown'));
20
+ }, [state.readEpoch]);
21
+ const current = value => mounted.current && value === generation.current;
22
+ async function show(batchId, view = 'records', offset = 0, retainNotice = false) {
23
+ const version = ++generation.current; setLoading(true); setText(null); setDetail(null);
24
+ if (!retainNotice) { setError(''); setNotice(''); }
25
+ try {
26
+ const result = await onImportDetails({ batchId, view, offset, limit: 20 });
27
+ if (current(version)) { setDetail({ ...result, offset }); setScope(result.batch.scopeMap[0]?.sourceConversationId || ''); }
28
+ } catch (failure) { if (current(version)) setError(failure.message || t('failed')); }
29
+ finally { if (current(version)) setLoading(false); }
30
+ }
31
+ async function readText(textKey, offset = 0) {
32
+ if (!detail || pending.current) return;
33
+ const version = generation.current, batchId = detail.batch.id; setLoading(true); setText(null);
34
+ try {
35
+ const result = await onImportDetails({ batchId, view:'text', textKey, offset, limit:16000 });
36
+ if (current(version)) setText(result);
37
+ } catch (failure) { if (current(version)) setError(failure.message || t('failed')); }
38
+ finally { if (current(version)) setLoading(false); }
39
+ }
40
+ async function refresh() {
41
+ const version = generation.current, selected = detail;
42
+ await onImportRefresh();
43
+ if (current(version) && selected) await show(selected.batch.id, selected.view, selected.offset, true);
44
+ }
45
+ async function prepare(reuse = false) {
46
+ if (state.canImport === false || pending.current || loading || (!reuse && !file) || (reuse && !receipt)) return;
47
+ const version = ++generation.current;
48
+ pending.current = true; setBusy(true); setError(''); setNotice(''); setText(null); setProgress(null);
49
+ try {
50
+ let staged = receipt;
51
+ if (!reuse) {
52
+ staged = await onImportUpload(file, value => { if (current(version)) setProgress(value); });
53
+ if (!current(version)) return;
54
+ setReceipt(staged);
55
+ }
56
+ const value = await onImportPrepare({ receiptId: staged.receiptId, requestId:globalThis.crypto.randomUUID(),
57
+ scopeMap:reuse && scope ? [{ sourceConversationId:scope, targetSessionId:importSessionId }] : [] });
58
+ if (!current(version)) return;
59
+ setReceipt({ ...staged, batchId:value.id });
60
+ await onImportRefresh();
61
+ if (current(version)) await show(value.id, 'records', 0, true);
62
+ } catch (failure) {
63
+ if (current(version)) setError((failure.message || t('failed')) + (failure.remote ? '' : ' ' + t('importUnknown')));
64
+ } finally { pending.current = false; if (mounted.current) { setBusy(false); setProgress(null); } }
65
+ }
66
+ async function decide(action) {
67
+ const batch = detail?.batch;
68
+ if (state.canImport === false || detail?.canImport === false || !batch || pending.current || loading) return;
69
+ const changed = scope !== (batch.scopeMap[0]?.sourceConversationId || '');
70
+ if (action === 'confirm' && (batch.status !== 'ready' || batch.sessionId !== importSessionId || changed)) return;
71
+ if (action === 'withdraw' && batch.status !== 'confirmed') return;
72
+ const version = generation.current;
73
+ const request = { batchId:batch.id, expectedRevision:batch.revision, previewHash:batch.previewHash };
74
+ const key = JSON.stringify([action,request]);
75
+ if (!identities.current.has(key)) identities.current.set(key,globalThis.crypto.randomUUID());
76
+ request.requestId = identities.current.get(key);
77
+ pending.current = true; setBusy(true); setError(''); setNotice('');
78
+ try {
79
+ const value = await (action === 'confirm' ? onImportConfirm(request) : onImportWithdraw(request));
80
+ if (!current(version)) return;
81
+ setNotice(t('importSaved'));
82
+ await onImportRefresh();
83
+ if (current(version)) await show(value.id, 'records', 0, true);
84
+ } catch (failure) {
85
+ if (current(version)) {
86
+ setError((failure.message || t('failed')) + (failure.remote ? '' : ' ' + t('importUnknown')));
87
+ // Only read back. A lost response must never trigger a second mutation.
88
+ await onImportRefresh();
89
+ if (current(version)) await show(batch.id, 'records', 0, true);
90
+ }
91
+ } finally { pending.current = false; if (mounted.current) setBusy(false); }
92
+ }
93
+ const button = (label, action, disabled = false, props = {}) => h('button', { type:'button', disabled,
94
+ onClick:() => { void action(); }, ...props }, t(label));
95
+ const body = value => h('div', { className:'text' }, String(value ?? ''));
96
+ const reason = code => t(importReasonKeys.includes(code) ? 'importReason_' + code : 'importReasonUnrecognized');
97
+ const role = value => t(['user','assistant','tool','system','material','program'].includes(value) ? 'importRole_' + value : 'importRoleUnknown');
98
+ const batch = detail?.batch, locked = busy || loading;
99
+ const actionLocked = locked || state.canImport === false || detail?.canImport === false;
100
+ const scopeChanged = batch && scope !== (batch.scopeMap[0]?.sourceConversationId || '');
101
+ const receiptMatches = receipt && batch && receipt.batchId === batch.id;
102
+ return h('section', { className:'arcfold-methods arcfold-imports', 'aria-label':t('importTitle') },
103
+ h('header', null, h('strong', null, `${t('importTitle')} · ${state.total}`),
104
+ button(expanded ? 'importHide' : 'importOpen', () => expand(!expanded), busy, { 'aria-expanded':expanded }),
105
+ button('refresh', refresh, locked)),
106
+ expanded && h('div', { className:'detail' },
107
+ h('p', null, t('importBoundary')),
108
+ h('label', null, t('importFile'), h('input', { type:'file', accept:'.json,.jsonl,.ndjson,application/json', disabled:actionLocked,
109
+ onChange:event => {
110
+ const chosen = event.target.files?.[0] || null; generation.current++; setDetail(null); setText(null); setReceipt(null); setScope(''); setNotice('');
111
+ const valid = chosen && chosen.size > 0 && chosen.size <= IMPORT_MAX_FILE_BYTES;
112
+ setFile(valid ? chosen : null); setError(chosen && !valid ? t('importFileTooLarge') : '');
113
+ } })),
114
+ button('importPrepare', () => prepare(), actionLocked || !file),
115
+ busy && h('p', { role:'status' }, t('importWorking'), progress && ` ${progress.loaded.toLocaleString()} / ${file?.size.toLocaleString()} bytes`),
116
+ h('p', { className:'meta' }, t('importStorage')),
117
+ (state.loading || loading) && h('p', { role:'status' }, t('importLoading')),
118
+ (error || state.error) && h('p', { role:'alert' }, error || state.error), notice && h('p', { role:'status' }, notice),
119
+ h('ul', null, state.items.map(row => h('li', { key:row.id },
120
+ body(row.file.name), h('p', { className:'meta' }, `${t(statusKeys[row.status])} · ${row.createdAt}`),
121
+ button('importPreview', () => show(row.id), locked)))),
122
+ !state.loading && !state.error && !state.items.length && h('p', null, t('importEmpty')),
123
+ h('nav', null, button('previous', () => onImportPage(Math.max(0,state.offset-20)), locked || !state.offset),
124
+ button('next', () => onImportPage(state.nextOffset), locked || state.nextOffset == null)),
125
+ batch && h('article', { 'aria-label':t('importTitle') + ' ' + batch.id },
126
+ h('h3', null, batch.file.name), h('p', null, t(statusKeys[batch.status])),
127
+ h('p', { className:'meta' }, t('importSnapshot')),
128
+ batch.status === 'withdrawn' && h('p', { role:'status' }, t('importWithdrawnNotice')),
129
+ h('p', { className:'meta' }, `${t('importRevision')} ${batch.revision} · ${batch.file.bytes.toLocaleString()} bytes`),
130
+ h('details', null, h('summary', null, t('importFileHash')), body(batch.file.sha256)),
131
+ batch.error && h('p', { role:'alert' }, batch.error.message),
132
+ h('h4', null, t('importCountsAtPreview')),
133
+ h('dl', { className:'import-counts' }, Object.entries({ sources:'importCountSources',records:'importCountRecords',enabled:'importEnabled',trial:'importTrial',paused:'importPaused',history:'importHistory',needsReview:'importNeedsReview' }).map(([name,key]) =>
134
+ h('div', { key:name }, h('dt', null, t(key)), h('dd', null, String(batch.counts[name] ?? 0))))),
135
+ h('h4', null, t('importScopeTitle')), h('p', { className:'meta' }, t('importScopeHelp')),
136
+ h('label', null, t('importMapScope'), h('select', { value:scope, disabled:actionLocked || batch.status !== 'ready' || !receiptMatches || batch.sessionId !== importSessionId,
137
+ onChange:event => setScope(event.target.value) },
138
+ h('option', { value:'' }, t('importKeepScope')),
139
+ batch.sourceScopes.map(source => h('option', { key:source.sourceConversationId, value:source.sourceConversationId }, `${source.title || source.sourceConversationId} · ${source.messageCount}`)))),
140
+ scope && h('p', { className:'text' }, batch.sourceScopes.find(source => source.sourceConversationId === scope)?.preview),
141
+ scopeChanged && h('p', { role:'status' }, t('importScopeChanged')),
142
+ batch.status === 'ready' && (receiptMatches ? button('importReprepare', () => prepare(true), actionLocked || !scopeChanged)
143
+ : h('p', { className:'meta' }, t('importNoReceipt'))),
144
+ h('nav', null, button('importRecords', () => show(batch.id,'records'), locked), button('importSources', () => show(batch.id,'sources'), locked)),
145
+ h('p', { className:'meta' }, t('importExcerpt')),
146
+ h('ul', null, detail.items.map(row => h('li', { key:row.id, className:'source' },
147
+ row.title && h('h4', null, row.title), body(row.textPreview),
148
+ row.eligibility && h('p', null, `${t('importEligibilityAtPreview')}:${t(eligibilityKeys[row.eligibility])}`),
149
+ row.claimType && h('p', null, t({ user_statement:'importClaimLiteral', inference:'importClaimInference', open_question:'importClaimQuestion' }[row.claimType] || 'importClaimInference')),
150
+ row.kind === 'methods' && h(React.Fragment, null,
151
+ h('h4', null, t('importAppliesTo')), body(row.appliesTo ?? t('importAttributeUnavailable')),
152
+ h('h4', null, t('importDoesNotApplyTo')), body(row.doesNotApplyTo ?? t('importAttributeUnavailable'))),
153
+ row.scope && h('p', { className:'meta' }, t(scopeKeys[row.scope.kind])),
154
+ row.scope?.topics?.length > 0 && h('p', { className:'meta' }, t('importTopics') + ': ' + row.scope.topics.join(' · ')),
155
+ row.role && h('p', { className:'meta' }, `${role(row.role)} · ${row.occurredAt || ''} · ${t('importSourceAtPreview')}:${t(row.available ? 'importAvailable' : 'importUnavailable')}`),
156
+ row.methodUsage && h('p', null, `${t('importUsageAtPreview')}:${t('importUsed')} ${row.methodUsage.trialUses ?? '—'} / ${t('importMaximum')} ${row.methodUsage.maxTrialUses ?? '—'} · ${t('importRemaining')} ${row.methodUsage.remaining ?? '—'}`),
157
+ row.reasonCodes?.length > 0 && h('details', null, h('summary', null, t('importReason')), body(row.reasonCodes.map(reason).join('\n'))),
158
+ row.existingRecordId && h('details', null, h('summary', null, t('importExistingRecord')), body(row.existingRecordId)),
159
+ row.originalTextKey && button('importOriginalRecord', () => readText(row.originalTextKey), locked),
160
+ row.textKey && button(detail.view === 'sources' ? 'importText' : 'importContent', () => readText(row.textKey), locked)))),
161
+ h('nav', null, button('previous', () => show(batch.id,detail.view,Math.max(0,detail.offset-20)), locked || !detail.offset),
162
+ button('next', () => show(batch.id,detail.view,detail.nextOffset), locked || detail.nextOffset == null)),
163
+ text && h('section', { 'aria-label':t('importTextPage') },
164
+ h('h4', null, t('importTextPage')), h('p', { className:'meta' }, `${text.offset}–${text.offset+text.text.length} / ${text.total}`), body(text.text),
165
+ button('importReadPrevious', () => readText(text.textKey,Math.max(0,text.offset-16000)), locked || !text.offset),
166
+ button('importReadMore', () => readText(text.textKey,text.nextOffset), locked || text.nextOffset == null)),
167
+ batch.status === 'ready' && h('div', { className:'import-decision' }, h('p', null, t('importConfirmHelp')),
168
+ batch.sessionId !== importSessionId && h('p', null, t('importOtherSession')),
169
+ button('importConfirm', () => decide('confirm'), actionLocked || scopeChanged || batch.sessionId !== importSessionId)),
170
+ batch.status === 'confirmed' && h('div', { className:'import-decision' }, h('p', null, t('importWithdrawHelp')),
171
+ button('importWithdraw', () => decide('withdraw'), actionLocked)))));
172
+ }