@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
@@ -0,0 +1,145 @@
1
+ import { presentationLabel } from './locales.mjs';
2
+ import React, { useRef, useState } from 'react';
3
+ const h = React.createElement;
4
+ const text = (key, value) => h('div', { className: 'text', key }, value ?? '');
5
+
6
+ export function MethodDock({ useMethods, onDetails, onDecide, onAcknowledge, onRefresh, onPage, t }) {
7
+ const state = useMethods(value => value);
8
+ const [expanded, setExpanded] = useState(false);
9
+ const [detail, setDetail] = useState(null);
10
+ const [detailLoading, setDetailLoading] = useState(false);
11
+ const [busy, setBusy] = useState(false);
12
+ const [error, setError] = useState(null);
13
+ const [notice, setNotice] = useState(null);
14
+ const actionPending = useRef(false), detailGeneration = useRef(0), retryIds = useRef(new Map());
15
+
16
+ async function show(methodId, keepError = false) {
17
+ const generation = ++detailGeneration.current;
18
+ setDetailLoading(true); setExpanded(true);
19
+ if (!keepError) { setError(null); setNotice(null); }
20
+ try {
21
+ const value = await onDetails(methodId);
22
+ if (generation === detailGeneration.current) setDetail(value);
23
+ } catch (failure) {
24
+ if (generation === detailGeneration.current) { setDetail(null); setError(failure.message || t('failed')); }
25
+ } finally {
26
+ if (generation === detailGeneration.current) setDetailLoading(false);
27
+ }
28
+ }
29
+ function closeDetail() {
30
+ detailGeneration.current++; setDetail(null); setDetailLoading(false);
31
+ }
32
+ async function historyPage(offset) {
33
+ if (!detail) return;
34
+ const methodId = detail.method.id, generation = ++detailGeneration.current;
35
+ setDetailLoading(true); setError(null);
36
+ try {
37
+ const value = await onDetails(methodId, offset);
38
+ if (generation === detailGeneration.current) setDetail(current => current?.method.id === methodId
39
+ ? { ...current, notifications: value.notifications } : current);
40
+ // History reads do not silently replace the full method/version the user
41
+ // is deciding on. A deliberate reload or conflict refresh does that.
42
+ } catch (failure) {
43
+ if (generation === detailGeneration.current) setError(failure.message || t('failed'));
44
+ } finally { if (generation === detailGeneration.current) setDetailLoading(false); }
45
+ }
46
+ async function decide(action) {
47
+ if (actionPending.current || detailLoading || !detail) return;
48
+ actionPending.current = true; setBusy(true); setError(null); setNotice(null);
49
+ // The request comes from the displayed detail, never a newer unseen list item.
50
+ const request = { methodId: detail.method.id, expectedRevision: detail.method.revision,
51
+ contentHash: detail.contentHash, action };
52
+ const key = JSON.stringify(request);
53
+ if (!retryIds.current.has(key)) retryIds.current.set(key, globalThis.crypto.randomUUID());
54
+ request.requestId = retryIds.current.get(key);
55
+ try {
56
+ await onDecide(request);
57
+ retryIds.current.delete(key);
58
+ setNotice(t('saved'));
59
+ await show(request.methodId, true);
60
+ } catch (failure) {
61
+ if (failure.remote) retryIds.current.delete(key);
62
+ setError(failure.remote ? failure.message : `${failure.message || t('failed')} ${t('unknown')}`);
63
+ if (failure.code === 'revision_conflict' || failure.code === 'method_superseded' || failure.code === 'method_source_invalid') {
64
+ setError(`${failure.message || ''} ${t('conflict')}`);
65
+ await show(request.methodId, true);
66
+ }
67
+ } finally { actionPending.current = false; setBusy(false); }
68
+ }
69
+ async function acknowledge(id) {
70
+ if (actionPending.current) return;
71
+ actionPending.current = true; setBusy(true); setError(null);
72
+ try { await onAcknowledge(id); await historyPage(detail?.notifications?.offset ?? 0); }
73
+ catch (failure) { setError(failure.message || t('failed')); }
74
+ finally { actionPending.current = false; setBusy(false); }
75
+ }
76
+ const button = (label, action, disabled = false, extra = {}) => h('button', { type: 'button', disabled,
77
+ onClick: () => { void action(); }, ...extra }, label);
78
+ const method = detail?.method;
79
+ const history = detail?.notifications;
80
+ return h('section', { className: 'arcfold-methods', 'aria-label': t('title') },
81
+ h('header', null,
82
+ h('strong', null, `${t('title')} · ${t('pending')} ${state.decisionCount} · ${t('unread')} ${state.unreadCount}`),
83
+ button(expanded ? t('close') : t('open'), () => setExpanded(!expanded), false, { 'aria-expanded': expanded }),
84
+ button(t('refresh'), onRefresh)),
85
+ state.loading && h('p', { role: 'status' }, t('loading')),
86
+ state.error && h('p', { role: 'alert' }, state.error),
87
+ expanded && h('div', { className: 'detail' },
88
+ error && h('p', { role: 'alert' }, error),
89
+ notice && h('p', { role: 'status' }, notice),
90
+ !method && !detailLoading && h('ul', null, state.items.map(item => h('li', { key: item.method.id },
91
+ h('strong', null, item.method.title),
92
+ h('p', { className: 'meta' }, `${presentationLabel('method', item.method.status, t)} · ${t('version')} ${item.method.version ?? 1}`),
93
+ text('current-reason', item.reason),
94
+ item.needsDecision && h('p', null, t('decisionPrompt')),
95
+ item.unreadCount > 0 && h('p', { className: 'meta' }, `${t('unread')} ${item.unreadCount}`),
96
+ button(t(item.needsDecision ? 'reviewDecision' : 'viewDetails'), () => show(item.method.id), busy)))),
97
+ !method && !detailLoading && !state.loading && !state.error && !state.items.length && h('p', null, t('empty')),
98
+ !method && !detailLoading && (state.offset > 0 || state.nextOffset != null) && h('nav', { 'aria-label': t('title') },
99
+ button(t('previous'), () => onPage(Math.max(0, state.offset - 20)), state.offset === 0),
100
+ button(t('next'), () => onPage(state.nextOffset), state.nextOffset === null || state.nextOffset === undefined)),
101
+ detailLoading && h('p', { role: 'status' }, t('loading')),
102
+ method && h('ul', null, h('li', null, h('article', { 'aria-label': method.title },
103
+ button(t('backToMethods'), closeDetail, busy),
104
+ h('h3', null, method.title),
105
+ h('p', { className: 'meta' }, [`${t('version')} ${method.version ?? 1}`,
106
+ method.revision != null ? `${t('revision')} ${method.revision}` : null,
107
+ `${t('status')} ${presentationLabel('method', method.status, t)}`].filter(Boolean).join(' · ')),
108
+ h('h4', null, t('reason')), text('reason', detail.reason),
109
+ detail.needsDecision && h('p', null, t('decisionPrompt')),
110
+ detail.usage && h('p', null, `${t('usage')} ${detail.usage.trialUses}/${detail.usage.maxTrialUses} · ${t('remaining')} ${detail.usage.remaining}`),
111
+ detail.pendingAdoptionCount > 0 && h('div', null,
112
+ h('h4', null, `${t('pendingAdoptions')} ${detail.pendingAdoptionCount}`),
113
+ (detail.pendingAdoptions || []).map(adoption => h('p', { className: 'meta', key: adoption.id },
114
+ `${adoption.id} · ${t('conversation')} ${adoption.sessionId} · ${t('turn')} ${adoption.turn} · ${presentationLabel('adoption', adoption.state, t)}`))),
115
+ method.guidance !== undefined && h('div', null,
116
+ h('h4', null, t('guidance')), text('guidance', method.guidance),
117
+ h('h4', null, t('applies')), text('applies', method.appliesTo),
118
+ h('h4', null, t('excludes')), text('excludes', method.doesNotApplyTo)),
119
+ h('h4', null, t('sources')),
120
+ detail.sources.length ? detail.sources.map(source => h('div', { className: 'source', key: source.id },
121
+ h('p', { className: 'meta' }, `${t('sourceIdentity')}: ${source.id} · ${t('revision')} ${source.revision} · ${t('author')}: ${presentationLabel('role', source.role, t)} · ${t('sourceTime')}: ${source.occurredAt ?? ''}`),
122
+ text('content', source.content),
123
+ h('p', { className: 'meta' }, `${t('provenance')}: ${JSON.stringify(source.provenance ?? null)}`))) : h('p', null, t('noSources')),
124
+ detail.successors.length > 0 && h('div', null, h('h4', null, t('successors')),
125
+ detail.successors.map(next => h('p', { key: next.id },
126
+ `${next.title} · ${t('version')} ${next.version} · ${presentationLabel('method', next.status, t)} `,
127
+ button(t('open'), () => show(next.id), busy)))),
128
+ !detail.activation.allowed && method.status !== 'active' && h('p', { role: 'alert' }, detail.activation.message),
129
+ h('p', { className: 'meta' }, t('boundary')),
130
+ h('div', { className: 'actions' },
131
+ button(busy ? t('busy') : t('continue'), () => decide('continue'), busy || detailLoading || !detail.activation.allowed),
132
+ button(t(method.status === 'paused' ? 'keepPaused' : 'pause'), () => decide('pause'),
133
+ busy || detailLoading || !(['trial', 'active'].includes(method.status) || method.status === 'paused' && detail.needsDecision)),
134
+ button(t('retry'), () => show(method.id), busy)),
135
+ h('details', { key: method.id, 'aria-label': t('history') },
136
+ h('summary', null, `${t('history')} ${history?.total ?? 0} · ${t('unread')} ${history?.unreadCount ?? 0}`),
137
+ h('ul', null, (history?.items || []).map(item => h('li', { key: item.id },
138
+ h('strong', null, item.title), text('message', item.displayMessage ?? item.message),
139
+ h('p', { className: 'meta' }, `${t('version')} ${item.methodVersion} · ${item.createdAt ?? ''}`),
140
+ item.status === 'unread' ? button(t('acknowledge'), () => acknowledge(item.id), busy || detailLoading) : h('span', null, t('read'))))),
141
+ !history?.total && h('p', null, t('noNotifications')),
142
+ (history?.offset > 0 || history?.nextOffset != null) && h('nav', { 'aria-label': t('history') },
143
+ button(t('previous'), () => historyPage(Math.max(0, history.offset - 20)), busy || detailLoading || history.offset === 0),
144
+ button(t('next'), () => historyPage(history.nextOffset), busy || detailLoading || history.nextOffset == null))))))));
145
+ }
@@ -0,0 +1,144 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { deployedRoutes, validLearningChoice } from './model-settings-source.mjs';
3
+ const h = React.createElement;
4
+ const PHASE_LABELS = { ready: 'modelsPhaseReady', starting: 'modelsPhaseStarting',
5
+ 'requires-restart': 'modelsPhaseRestart', failed: 'modelsPhaseFailed', closed: 'modelsPhaseClosed' };
6
+ const REASON_LABELS = {
7
+ 'main-not-configured': 'modelsReasonNoModel', 'learn-not-configured': 'modelsReasonNoModel',
8
+ 'native-credential-not-configured': 'modelsReasonCredential',
9
+ 'transport-not-configured': 'modelsReasonUnsupported', 'selected-route-not-supported': 'modelsReasonUnsupported',
10
+ 'selected-model-controls-unavailable': 'modelsReasonUnsupported', 'main-output-limit-unavailable': 'modelsReasonUnsupported',
11
+ 'selection-outside-model-policy': 'modelsReasonUnsupported', 'model-catalog-controls-invalid': 'modelsReasonUnsupported',
12
+ 'model-catalog-has-no-common-controls': 'modelsReasonUnsupported',
13
+ 'native-selection-changed-during-status': 'modelsReasonChanged', 'learning-selection-changed-during-status': 'modelsReasonChanged',
14
+ 'runtime-not-ready': 'modelsReasonNotReady', 'runtime-readiness-unavailable': 'modelsReasonNotReady',
15
+ 'guard-unavailable': 'modelsReasonNotReady', 'startup-failed': 'modelsReasonNotReady', 'runtime-closed': 'modelsReasonNotReady',
16
+ 'runtime-restart-required': 'modelsReasonRestart', 'critical-component-changed': 'modelsReasonRestart',
17
+ 'critical-configuration-changed': 'modelsReasonRestart', 'model-topology-changed': 'modelsReasonRestart',
18
+ };
19
+ const EFFORT_LABELS = { off: 'modelsEffortOff', low: 'modelsEffortLow', medium: 'modelsEffortMedium',
20
+ high: 'modelsEffortHigh', xhigh: 'modelsEffortXHigh', max: 'modelsEffortMax' };
21
+ const reasonText = (reason, t) => t(REASON_LABELS[reason] || 'modelsReasonUnknown');
22
+ const effortText = (effort, t) => t(EFFORT_LABELS[effort] || 'modelsEffortOther');
23
+ const selectionText = (value, t) => value ? [value.provider, value.model,
24
+ value.reasoningEffort ? effortText(value.reasoningEffort, t) : null].filter(Boolean).join(' · ') : '';
25
+ function outcomeText(result, t, credential = false) {
26
+ if (result?.status === 'saved') return t(result.refreshed ? 'modelsSaved' : 'modelsSavedUnread');
27
+ if (credential) return t(result?.status === 'unknown' ? 'modelsCredentialUnknown' : 'modelsCredentialFailed');
28
+ return t(result?.status === 'unknown' ? 'modelsUnknown' : result?.code === 'conflict' ? 'modelsConflict' : 'modelsRefused');
29
+ }
30
+ function CredentialCard({ route, info, disabled, readFailed, onStore, onRemove, t }) {
31
+ const [draft, setDraft] = useState(''), [busy, setBusy] = useState(false), [notice, setNotice] = useState('');
32
+ const pending = useRef(false), mounted = useRef(true);
33
+ useEffect(() => { mounted.current = true; return () => { mounted.current = false; }; }, []);
34
+ async function write(remove) {
35
+ if (pending.current || disabled || !info?.writable) return;
36
+ pending.current = true; setBusy(true); setNotice('');
37
+ const value = draft;
38
+ setDraft('');
39
+ try {
40
+ const result = remove ? await onRemove(route.provider) : await onStore(route.provider, value);
41
+ if (mounted.current) setNotice(outcomeText(result, t, true));
42
+ } catch {
43
+ if (mounted.current) setNotice(t('modelsCredentialUnknown'));
44
+ } finally {
45
+ pending.current = false;
46
+ if (mounted.current) { setDraft(''); setBusy(false); }
47
+ }
48
+ }
49
+ return h('article', { className: 'model-card' }, h('h3', null, route.label),
50
+ h('p', null, readFailed ? t('modelsCredentialReadFailed') : t(info?.configured ? 'modelsConfigured' : 'modelsNotConfigured')),
51
+ info?.source && h('p', { className: 'meta' }, t('modelsSource') + ': ' + info.source),
52
+ !readFailed && !info?.writable && h('p', null, t('modelsReadOnly')),
53
+ h('label', null, route.label + ' · ' + t('modelsCredential'),
54
+ h('input', { type: 'password', value: draft, autoComplete: 'new-password', spellCheck: false,
55
+ disabled: disabled || busy || !info?.writable,
56
+ onChange: event => setDraft(event.target.value) })),
57
+ h('div', { className: 'actions' },
58
+ h('button', { type: 'button', disabled: disabled || busy || !info?.writable || !draft.trim(), onClick: () => { void write(false); } }, t('modelsCredentialSave')),
59
+ h('button', { type: 'button', disabled: disabled || busy || !info?.writable || !info?.configured, onClick: () => { void write(true); } }, t('modelsCredentialRemove')),
60
+ h('button', { type: 'button', onClick: () => { setDraft(''); setNotice(''); } }, t('modelsCredentialCancel'))),
61
+ notice && h('p', { role: 'status' }, notice));
62
+ }
63
+ function Usage({ route, t }) {
64
+ const usage = route.usage;
65
+ const scope = { limited: 'modelsScopeLimited', 'web-unlimited': 'modelsScopeWeb', 'web-openai': 'modelsScopeOpenai' };
66
+ const row = (label, value) => h('div', { key: label }, h('dt', null, t(label)), h('dd', null, value));
67
+ return h('article', { className: 'model-card' }, h('h3', null, route.label),
68
+ !usage ? h('p', null, t('modelsUsageMissing')) : h(React.Fragment, null,
69
+ h('p', { className: 'meta' }, scope[usage.scope] ? t(scope[usage.scope]) : usage.scope),
70
+ h('dl', null, row('modelsRequests', usage.requests), row('modelsUnknownCount', usage.unknownCount),
71
+ usage.accounting === 'cny' ? [
72
+ row('modelsSpent', '¥ ' + usage.spentCny), row('modelsReserved', '¥ ' + usage.reservedCny),
73
+ row('modelsCeiling', usage.ceilingCny === null ? t('modelsNoCeiling') : '¥ ' + usage.ceilingCny),
74
+ ] : [
75
+ row('modelsPrompt', usage.promptTokens), row('modelsCompletion', usage.completionTokens),
76
+ row('modelsCached', usage.cachedTokens), row('modelsPending', usage.pendingCount),
77
+ ]),
78
+ usage.accounting === 'tokens_only' && h('p', null, t('modelsTokensOnly'))));
79
+ }
80
+ function LearningForm({ data, disabled, onSave, t }) {
81
+ const defaults = data.learning?.defaults;
82
+ const [draft, setDraft] = useState(() => ({ ...defaults }));
83
+ const [busy, setBusy] = useState(false), [notice, setNotice] = useState('');
84
+ const pending = useRef(false), mounted = useRef(true);
85
+ useEffect(() => { mounted.current = true; return () => { mounted.current = false; }; }, []);
86
+ useEffect(() => { setDraft({ ...defaults }); }, [defaults?.revision, defaults?.provider, defaults?.model, defaults?.reasoningEffort]);
87
+ const routes = deployedRoutes(data), route = routes.find(row => row.provider === draft.provider);
88
+ const model = route?.models?.find(row => row.id === draft.model);
89
+ const blocked = disabled || busy || data.gate?.phase !== 'ready' || !data.learning?.enabled ||
90
+ !defaults;
91
+ const select = (label, value, options, update) => h('label', { key: label }, t(label),
92
+ h('select', { value: options.some(item => item.id === value) ? value : '', disabled: blocked,
93
+ onChange: event => update(event.target.value) },
94
+ h('option', { value: '', disabled: true }, t('modelsChoose')),
95
+ options.map(item => h('option', { key: item.id, value: item.id }, item.label || item.id))));
96
+ async function save() {
97
+ if (pending.current || blocked || !validLearningChoice(data, draft)) return;
98
+ pending.current = true; setBusy(true); setNotice('');
99
+ try {
100
+ const result = await onSave({ expectedRevision: defaults.revision, provider: draft.provider,
101
+ model: draft.model, reasoningEffort: draft.reasoningEffort });
102
+ if (mounted.current) setNotice(outcomeText(result, t));
103
+ } catch { if (mounted.current) setNotice(t('modelsUnknown')); }
104
+ finally { pending.current = false; if (mounted.current) setBusy(false); }
105
+ }
106
+ return h('div', null,
107
+ !data.learning?.enabled && h('p', null, t('modelsLearnDisabled')),
108
+ !defaults && h('p', null, t('modelsNoDefault')),
109
+ !data.learning?.availability?.available && h('p', null, reasonText(data.learning?.availability?.reason, t)),
110
+ defaults && h('p', { className: 'meta' }, t('modelsRevision') + ': ' + defaults.revision + ' · ' + selectionText(defaults, t)),
111
+ h('div', { className: 'model-fields' },
112
+ select('modelsProvider', draft.provider, routes.map(row => ({ id: row.provider, label: row.label })),
113
+ provider => { setNotice(''); setDraft({ provider, model: '', reasoningEffort: '' }); }),
114
+ select('modelsModel', draft.model, route?.models || [],
115
+ model => { setNotice(''); setDraft({ ...draft, model, reasoningEffort: '' }); }),
116
+ select('modelsReasoning', draft.reasoningEffort, (model?.reasoningEfforts || []).map(id => ({ id, label: effortText(id, t) })),
117
+ reasoningEffort => { setNotice(''); setDraft({ ...draft, reasoningEffort }); })),
118
+ h('button', { type: 'button', disabled: blocked || !validLearningChoice(data, draft), onClick: () => { void save(); } },
119
+ t(busy ? 'modelsBusy' : 'modelsSave')),
120
+ notice && h('p', { role: 'status' }, notice));
121
+ }
122
+ export function ModelSettings({ useModels, onRefresh, onSaveLearning, onStoreCredential, onRemoveCredential, t }) {
123
+ const state = useModels(value => value), data = state.data;
124
+ return h('section', { className: 'arcfold-methods arcfold-model-settings', 'aria-label': t('modelsTitle') },
125
+ h('header', null, h('h2', null, t('modelsTitle')),
126
+ h('button', { type: 'button', disabled: state.loading, onClick: () => { void onRefresh(); } }, t('modelsRefresh'))),
127
+ state.loading && h('p', { role: 'status' }, t('modelsLoading')),
128
+ state.error && h('p', { role: 'alert' }, t('modelsReadFailed')),
129
+ data && h(React.Fragment, null,
130
+ h('p', { className: 'meta' }, t('modelsGate') + ': ' + t(PHASE_LABELS[data.gate.phase] || 'modelsReasonUnknown') + (data.gate.reason ? ' · ' + reasonText(data.gate.reason, t) : '')),
131
+ h('h3', null, t('modelsMain')), h('p', null, t('modelsMainScope')),
132
+ h('p', null, t('modelsDefault') + ': ' + (selectionText(data.main?.defaultSelection, t) || t('modelsUnavailable'))),
133
+ !data.main?.availability?.available && h('p', null, reasonText(data.main?.availability?.reason, t)),
134
+ h('h3', null, t('modelsLearn')), h('p', null, t('modelsLearnScope')),
135
+ h(LearningForm, { data,
136
+ disabled: Boolean(state.error || state.loading), onSave: onSaveLearning, t }),
137
+ h('h3', null, t('modelsCredentials')), h('p', null, t('modelsCredentialScope')),
138
+ !deployedRoutes(data).length && h('p', null, t('modelsNoRoutes')),
139
+ deployedRoutes(data).map(route => h(CredentialCard, { key: route.provider + ':' + route.apiKeyRef, route,
140
+ info: state.credentials[route.apiKeyRef], readFailed: state.credentialError,
141
+ disabled: Boolean(state.error || state.loading), onStore: onStoreCredential, onRemove: onRemoveCredential, t })),
142
+ h('h3', null, t('modelsUsage')), h('p', null, t('modelsUsageBoundary')),
143
+ deployedRoutes(data).map(route => h(Usage, { key: route.provider, route, t }))));
144
+ }
@@ -0,0 +1,66 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ const h = React.createElement;
3
+
4
+ function status(item, t) {
5
+ const key = item.control?.dismissed ? 'opportunityDismissed' : item.stopped ? 'opportunityStopped'
6
+ : !item.run ? (item.rejection ? 'opportunityRejected' : 'opportunityPending')
7
+ : ({ prepared: 'opportunityPrepared', submitted: 'opportunitySubmitted', unknown: 'opportunityUncertain',
8
+ stopped: 'opportunityStopped', settled: 'opportunitySettled' }[item.run.state] ?? 'presentationUnknown');
9
+ return t(key);
10
+ }
11
+
12
+ function OpportunityCard({ item, readDetails, sessionId, t }) {
13
+ const [detail, setDetail] = useState(null), [error, setError] = useState(null);
14
+ useEffect(() => {
15
+ let active = true;
16
+ // Browsing never grants permission, marks a notice as read, starts
17
+ // preparation, or submits a new instruction.
18
+ Promise.resolve().then(() => readDetails(item.opportunity.id)).then(value => {
19
+ if (active) setDetail(value);
20
+ }, failure => { if (active) setError(failure.message); });
21
+ return () => { active = false; };
22
+ }, [item.opportunity.id, readDetails]);
23
+ const current = detail ?? item, row = current.opportunity;
24
+ return h('article', { className: 'opportunity-card', 'aria-label': row.description },
25
+ h('div', { className: 'opportunity-status' },
26
+ h('span', { className: 'status-badge' }, status(current, t)),
27
+ current.notice && h('span', { className: 'meta' }, t(current.notice.status === 'unread' ? 'opportunityUnread' : 'opportunityAlreadyRead'))),
28
+ h('h3', { className: 'text' }, row.description),
29
+ sessionId && row.conversationId !== sessionId && h('p', { className: 'meta' }, t('browseOtherSession')),
30
+ row.rationale && h('div', { className: 'opportunity-field' }, h('h4', null, t('opportunityRationale')), h('p', { className: 'text' }, row.rationale)),
31
+ row.suggestedPreparation && h('div', { className: 'opportunity-field' }, h('h4', null, t('opportunitySuggestion')), h('p', { className: 'text' }, row.suggestedPreparation)),
32
+ error && h('p', { role: 'alert' }, error),
33
+ current.reason && (current.run || current.control || current.rejection) && h('p', { className: 'meta' }, current.reason),
34
+ current.rejection && !current.run && h('p', { role: 'alert' }, current.rejection.error.message),
35
+ current.run?.error && h('p', { role: 'alert' }, current.run.error.message),
36
+ current.artifact && h('div', { className: 'opportunity-draft' }, h('h4', null, t('opportunityDraft')),
37
+ !current.available && h('p', { role: 'alert' }, t('opportunityStopped')),
38
+ h('p', { className: 'meta' }, current.artifact.evidence.path), h('pre', { className: 'text' }, current.artifact.evidence.text),
39
+ h('p', { className: 'meta' }, t('opportunityNotUsefulYet'))),
40
+ detail && h('details', { className: 'opportunity-evidence' }, h('summary', null, t('opportunityEvidence')),
41
+ h('pre', { className: 'text' }, JSON.stringify(detail.run?.input ?? detail.originals, null, 2))));
42
+ }
43
+
44
+ export function OpportunityDock({ useOpportunities, onOpportunityDetails, onOpportunityRefresh, onOpportunityPage, opportunitySessionId, viewScope, t }) {
45
+ const state = useOpportunities(value => value);
46
+ const [open, setOpen] = useState(false), [refreshVersion, setRefreshVersion] = useState(0);
47
+ const button = (label, action, disabled = false, extra = {}) => h('button', { type: 'button', disabled, onClick: action, ...extra }, label);
48
+ return h('section', { className: 'arcfold-methods arcfold-opportunities', 'aria-label': t('opportunityTitle') },
49
+ h('header', null, h('strong', null, `${t('opportunityTitle')} · ${state.total}`),
50
+ button(t(open ? 'opportunityHide' : 'opportunityOpen'), () => setOpen(!open), false, { 'aria-expanded': open }),
51
+ button(t('refresh'), () => { setRefreshVersion(value => value + 1); void onOpportunityRefresh(); })),
52
+ state.error && h('p', { role: 'alert' }, state.error),
53
+ open && h('div', { className: 'detail opportunity-content' },
54
+ h('div', { className: 'opportunity-overview' },
55
+ h('p', null, t('opportunityReadOnly')),
56
+ viewScope !== 'all' && h('div', { className: 'opportunity-status' }, h('span', { className: 'status-badge' }, t(state.permission?.enabled ? 'opportunityOn' : 'opportunityOff'))),
57
+ state.workspace && h('p', { className: 'meta' }, `${t('opportunityWorkspace')} · ${state.workspace}`)),
58
+ h('ul', { className: 'opportunity-list' }, state.items.map(item => h('li', {
59
+ // Versioned snapshots discard late reads across reconnects and refresh.
60
+ key: JSON.stringify([item, state.permission?.revision, state.resetVersion, refreshVersion]),
61
+ }, h(OpportunityCard, { item, readDetails: onOpportunityDetails, sessionId: opportunitySessionId, t })))),
62
+ !state.loading && !state.items.length && h('p', { className: 'meta' }, t('opportunityEmpty'))),
63
+ open && (state.offset > 0 || state.nextOffset != null) && h('nav', { className: 'opportunity-pagination', 'aria-label': t('opportunityTitle') },
64
+ button(t('previous'), () => onOpportunityPage(Math.max(0, state.offset - 20)), !state.offset),
65
+ button(t('next'), () => onOpportunityPage(state.nextOffset), state.nextOffset == null)));
66
+ }
@@ -0,0 +1,117 @@
1
+ import { presentationLabel } from './locales.mjs';
2
+ import React, { useEffect, useRef, useState } from 'react';
3
+ const h = React.createElement;
4
+ const literal = value => typeof value === 'string' ? value : JSON.stringify(value ?? '');
5
+ export function UnderstandingDock({ useUnderstandings, onUnderstandingDetails, onUnderstandingDecide,
6
+ onUnderstandingCorrect, onUnderstandingRefresh, onUnderstandingPage, t }) {
7
+ const state = useUnderstandings(value => value);
8
+ const [expanded, expand] = useState(false), [detail, setDetail] = useState(null), [edit, setEdit] = useState(null);
9
+ const [busy, setBusy] = useState(false), [loading, setLoading] = useState(false), [notice, setNotice] = useState(''), [error, setError] = useState('');
10
+ const generation = useRef(0), pending = useRef(false), identities = useRef(new Map()), mounted = useRef(true);
11
+ useEffect(() => { mounted.current = true; return () => { mounted.current = false; generation.current++; }; }, []);
12
+ useEffect(() => {
13
+ generation.current++; setDetail(null); setEdit(null); setLoading(false); setError(''); setNotice('');
14
+ }, [state.readEpoch]);
15
+ async function show(id, retainNotice = false, retainedEdit = null) {
16
+ const current = ++generation.current; setLoading(true); setEdit(null); setDetail(null);
17
+ if (!retainNotice) { setError(''); setNotice(''); }
18
+ try {
19
+ const value = await onUnderstandingDetails(id);
20
+ if (mounted.current && current === generation.current) {
21
+ setDetail(value);
22
+ if (retainedEdit?.id === value.understanding.id && retainedEdit.revision === value.understanding.revision) setEdit(retainedEdit);
23
+ }
24
+ }
25
+ catch (failure) { if (mounted.current && current === generation.current) setError(failure.message || t('failed')); }
26
+ finally { if (mounted.current && current === generation.current) setLoading(false); }
27
+ }
28
+ async function refresh() {
29
+ const current = generation.current, id = detail?.understanding.id;
30
+ try {
31
+ await onUnderstandingRefresh();
32
+ if (mounted.current && current === generation.current && id) await show(id);
33
+ } catch (failure) { if (mounted.current && current === generation.current) setError(failure.message || t('failed')); }
34
+ }
35
+ async function act(action, source) {
36
+ if (pending.current || loading || !detail || detail.browse?.related === false) return;
37
+ const current = detail.understanding, selectedGeneration = generation.current;
38
+ if (action === 'correct' && (!edit || edit.id !== current.id || edit.revision !== current.revision || !edit.text.trim())) return;
39
+ const request = { understandingId: current.id, expectedRevision: current.revision,
40
+ ...(action === 'correct' ? { text: edit.text } : { action }),
41
+ ...(source ? { sourceRef: { id: source.id, revision: source.revision, expectedControlRevision: source.expectedControlRevision } } : {}) };
42
+ const key = JSON.stringify(request);
43
+ if (!identities.current.has(key)) identities.current.set(key, globalThis.crypto.randomUUID());
44
+ request.requestId = identities.current.get(key);
45
+ pending.current = true; setBusy(true); setNotice(''); setError('');
46
+ try {
47
+ const value = await (action === 'correct' ? onUnderstandingCorrect(request) : onUnderstandingDecide(request));
48
+ if (!mounted.current || selectedGeneration !== generation.current) return;
49
+ const command = value.command;
50
+ setNotice(t(command?.result?.state === 'applied' ? 'understandingApplied' : command?.result?.state === 'failed' ? 'understandingFailed'
51
+ : action !== 'correct' ? 'understandingSaved' : command?.admission?.state === 'accepted' ? 'understandingPending' : 'understandingUnknown'));
52
+ await onUnderstandingRefresh();
53
+ if (mounted.current && selectedGeneration === generation.current)
54
+ await show(current.id, true, action === 'correct' && !command?.result && command?.admission?.state !== 'accepted' ? edit : null);
55
+ } catch (failure) {
56
+ if (mounted.current && selectedGeneration === generation.current) {
57
+ if (failure.code === 'revision_conflict') {
58
+ setError(failure.message || t('failed'));
59
+ await show(current.id, true);
60
+ } else setError((failure.message || t('failed')) + ' ' + t('understandingUnknown'));
61
+ }
62
+ } finally { pending.current = false; if (mounted.current) setBusy(false); }
63
+ }
64
+ const button = (label, action, disabled = false, props = {}) => h('button', { type: 'button', disabled,
65
+ onClick: () => { void action(); }, ...props }, t(label));
66
+ const body = value => h('div', { className: 'text' }, literal(value));
67
+ const item = detail?.understanding;
68
+ const claim = value => presentationLabel('claim', value, t);
69
+ return h('section', { className: 'arcfold-methods arcfold-understandings', 'aria-label': t('understandingTitle') },
70
+ h('header', null, h('strong', null, `${t('understandingTitle')} · ${state.total}`),
71
+ button(expanded ? 'understandingHide' : 'understandingOpen', () => expand(!expanded), false, { 'aria-expanded': expanded }),
72
+ button('refresh', refresh, busy || loading)),
73
+ expanded && h('div', { className: 'detail' },
74
+ h('p', { className: 'meta' }, t('understandingBoundary')),
75
+ (state.loading || loading) && h('p', { role: 'status' }, t('understandingLoading')),
76
+ (error || state.error) && h('p', { role: 'alert' }, error || state.error), notice && h('p', { role: 'status' }, notice),
77
+ h('ul', null, state.items.map(row => h('li', { key: row.understanding.id },
78
+ body(row.understanding.content), h('p', { className: 'meta' }, claim(row.understanding.claimType)),
79
+ button('understandingOpen', () => show(row.understanding.id), busy || loading)))),
80
+ !state.loading && !state.error && !state.items.length && h('p', null, t('understandingEmpty')),
81
+ h('nav', null, button('previous', () => onUnderstandingPage(Math.max(0, state.offset - 20)), busy || loading || !state.offset),
82
+ button('next', () => onUnderstandingPage(state.nextOffset), busy || loading || state.nextOffset == null)),
83
+ item && h('article', { 'aria-label': t('understandingTitle') + ' ' + item.id },
84
+ h('h3', null, claim(item.claimType)), body(item.content),
85
+ detail.browse?.related === false && h('p', { className: 'meta' }, t('browseReturnToOrigin')),
86
+ h('p', { className: 'meta' }, `${t('revision')} ${item.revision} · ${t('understandingVersion')} ${item.version ?? '—'} · ${presentationLabel('understanding', item.status, t)} · ${t(detail.availability.allowed ? 'understandingAvailable' : 'understandingUnavailable')}`),
87
+ !detail.availability.allowed && h('p', null, detail.availability.message),
88
+ h('p', { className: 'meta' }, t('understandingWithdrawalHelp')),
89
+ h('div', { className: 'actions' },
90
+ button('understandingStop', () => act('stop-understanding'), busy || loading || !detail.controls?.stopAllowed),
91
+ button('understandingCorrect', () => setEdit({ id: item.id, revision: item.revision, text: item.content }), busy || loading || !detail.controls?.correctionAllowed)),
92
+ edit && h('div', { className: 'understanding-correction' },
93
+ h('p', null, t('understandingCorrectionHelp')),
94
+ h('label', null, t('understandingReplacement'), h('textarea', { value: edit.text, maxLength: 16000, disabled: busy || loading,
95
+ onChange: event => setEdit({ ...edit, text: event.target.value }) })),
96
+ button('understandingSubmit', () => act('correct'), busy || loading || !edit.text.trim()),
97
+ button('understandingCancel', () => setEdit(null), busy)),
98
+ h('h4', null, t('understandingSource')),
99
+ (detail.sources || []).map(source => h('div', { className: 'source', key: source.id },
100
+ h('p', { className: 'meta' }, `${source.id} · ${source.author && source.author !== source.role ? source.author : presentationLabel('role', source.role, t)} · ${source.occurredAt || ''} · ${t('revision')} ${source.revision ?? '—'} · ${t(source.available ? 'understandingAvailable' : 'understandingUnavailable')}`),
101
+ body(source.missing ? t('understandingMissing') : source.content),
102
+ h('details', null, h('summary', null, t('understandingProvenance')), body(source.provenance), source.control && body(source.control)),
103
+ button('understandingWithdraw', () => act('withdraw-source', source), busy || loading || detail.controls?.sourceControlAllowed === false || !source.available || !Number.isSafeInteger(source.revision)))),
104
+ h('h4', null, t('understandingSuccessors')),
105
+ (detail.successors || []).map(next => h('div', { key: next.id }, h('span', null, `${next.id} · ${t('revision')} ${next.revision} `),
106
+ button('understandingOpen', () => show(next.id), busy || loading))),
107
+ h('details', null, h('summary', null, `${t('understandingHistory')} · ${detail.history?.length || 0}/${detail.historyTotal || 0}`),
108
+ (detail.history || []).map(row => h('div', { className: 'source', key: row.understanding.id },
109
+ h('p', { className: 'meta' }, `${row.understanding.id} · ${t('revision')} ${row.understanding.revision} · ${t('understandingVersion')} ${row.understanding.version ?? '—'}`), body(row.understanding.content)))),
110
+ h('h4', null, `${t('understandingCommands')} · ${detail.commands?.length || 0}/${detail.commandsTotal || 0}`),
111
+ (detail.commands || []).map(command => h('div', { className: 'source', key: command.id },
112
+ h('p', null, `${presentationLabel('understandingAction', command.action, t)} · ${command.requestId} · ${t(command.result?.state === 'applied' ? 'understandingApplied' : command.result?.state === 'failed' ? 'understandingFailed' : 'understandingAwaiting')}`),
113
+ command.admission && h('p', null, t('understandingAdmission') + ': ' + presentationLabel('admission', command.admission.state, t)),
114
+ command.request?.text !== undefined && body(command.request.text), command.result?.error && body(command.result.error.message),
115
+ command.result?.changes?.length > 0 && h('details', null, h('summary', null, t('understandingEffects')), body(command.result.changes)),
116
+ command.result?.goalImpacts?.length > 0 && h('details', null, h('summary', null, t('understandingGoalEffects')), body(command.result.goalImpacts)))))));
117
+ }
@@ -0,0 +1,6 @@
1
+ // The UI host is packaged separately from core; keep its wire validation local.
2
+ export function browseScope(value, fallback = 'current') {
3
+ if (value === undefined) return fallback;
4
+ if (value === 'all' || value === 'current') return value;
5
+ throw Object.assign(new Error('请选择全部积累或与当前聊天相关的内容。'), { code: 'invalid_browse_scope' });
6
+ }
@@ -0,0 +1,31 @@
1
+ // Browsing changes a read projection, never a record's scope or an action's authority.
2
+ export function viewScopeFields(viewScope) {
3
+ if (viewScope !== undefined && !['all', 'current'].includes(viewScope))
4
+ throw new TypeError('Unknown Arcfold view scope');
5
+ return viewScope === undefined ? {} : { viewScope };
6
+ }
7
+
8
+ export function requireViewScope(viewScope) {
9
+ if (viewScope === undefined) throw new TypeError('A view scope is required');
10
+ viewScopeFields(viewScope);
11
+ }
12
+
13
+ export function createBrowseScopeSource(sources) {
14
+ let snapshot = { viewScope: 'all', epoch: 0 }, disposed = false;
15
+ const listeners = new Set();
16
+ return {
17
+ getSnapshot: () => snapshot,
18
+ subscribe(listener) {
19
+ if (disposed) return () => {};
20
+ listeners.add(listener); return () => listeners.delete(listener);
21
+ },
22
+ setViewScope(viewScope) {
23
+ requireViewScope(viewScope);
24
+ if (disposed || snapshot.viewScope === viewScope) return;
25
+ for (const source of sources) source.setViewScope?.(viewScope);
26
+ snapshot = { viewScope, epoch: snapshot.epoch + 1 };
27
+ for (const listener of listeners) listener();
28
+ },
29
+ dispose() { disposed = true; listeners.clear(); },
30
+ };
31
+ }