@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,56 @@
1
+ import { TypertRemoteService, RemoteError } from '@deepseek-ai/dsh-typert-protocol';
2
+ const SERVICE = 'arcfoldModelWeb', NAMESPACE = 'arcfoldModels';
3
+ const fail = (code, message) => { throw new RemoteError(code, message, {}); };
4
+ async function runModelRequest({ models, connection }, method, request, signal, peer) {
5
+ if (!peer || peer !== connection.browserPeer || peer.principal?.kind !== 'owner' ||
6
+ peer.interactions !== 'answer' || peer.signal?.aborted !== false || signal?.aborted !== false)
7
+ fail('trusted_browser_required', '此操作只接受当前已认证网页的请求。');
8
+ if (!models) fail('model_settings_unavailable', '当前部署尚未接通模型设置。');
9
+ if (!request || typeof request !== 'object' || Array.isArray(request)) fail('invalid_model_request', '设置请求格式无效。');
10
+ if (method === 'read') {
11
+ if (Object.keys(request).length) fail('invalid_model_request', '读取模型设置不接受会话或其他参数。');
12
+ const result = await models.read(); signal.throwIfAborted(); return result;
13
+ }
14
+ if (method === 'saveLearning') {
15
+ const fields = ['expectedRevision', 'provider', 'model', 'reasoningEffort'];
16
+ if (Object.keys(request).sort().join() !== fields.sort().join() || !Number.isSafeInteger(request.expectedRevision) || request.expectedRevision < 1 ||
17
+ ['provider', 'model', 'reasoningEffort'].some(key => typeof request[key] !== 'string' || !request[key] || request[key].length > 256))
18
+ fail('invalid_model_request', '只接受当前版本和完整的学习模型选择。');
19
+ // A disconnected browser must not leave an awaited catalog read able to
20
+ // commit its setting. The service invokes this again just before saving.
21
+ return models.saveLearning(request, () => {
22
+ signal.throwIfAborted();
23
+ if (peer !== connection.browserPeer || peer.signal.aborted || peer.principal?.kind !== 'owner' || peer.interactions !== 'answer') fail('trusted_browser_required', '网页连接已变化,请刷新设置。');
24
+ });
25
+ }
26
+ fail('model_endpoint_not_found', '不存在的模型设置操作。');
27
+ }
28
+ const knownErrors = {
29
+ revision_conflict: '学习设置已在其他页面修改,请刷新后重新选择。',
30
+ learning_configuration_unavailable: '当前部署无法使用所选的学习模型,请检查设置。',
31
+ invalid_learning_defaults: '请提交完整的模型选择和当前设置版本。',
32
+ ARCFOLD_RUNTIME_RESTART_REQUIRED: '运行配置已变化,请通过启动器重新启动后再保存。',
33
+ ARCFOLD_RUNTIME_NOT_READY: '运行环境尚未就绪,请稍后刷新。',
34
+ };
35
+ export async function dispatchModelRequest(...args) {
36
+ try { return await runModelRequest(...args); }
37
+ catch (error) {
38
+ if (error instanceof RemoteError) throw error;
39
+ const code = Object.hasOwn(knownErrors, error?.code) ? error.code : 'model_settings_failed';
40
+ throw new RemoteError(code, knownErrors[code] ?? '模型设置未完成,请刷新查看实际保存结果。', {});
41
+ }
42
+ }
43
+ class ModelWeb extends TypertRemoteService {
44
+ constructor(ctx) { super(ctx, SERVICE, { namespace: NAMESPACE }); }
45
+ dispatch(method, request) {
46
+ const { signal, peer } = this.ctx.invocation ?? {};
47
+ return dispatchModelRequest({ models: this.ctx.get('arcfoldRuntimeModels'), connection: this.ctx.connection }, method, request, signal, peer);
48
+ }
49
+ read(request) { return this.dispatch('read', request); }
50
+ saveLearning(request) { return this.dispatch('saveLearning', request); }
51
+ }
52
+ export const modelInvocations = ['read', 'saveLearning'].map(method => ({
53
+ id: `@arcfold/dsh-method-ui#${NAMESPACE}/${method}`, service: SERVICE, namespace: NAMESPACE, method,
54
+ invocation: { kind: 'direct' },
55
+ parameters: [{ name: 'request', wire: 'request', source: 'json', codec: { mode: 'src-json' } }], result: { mode: 'src-json' } }));
56
+ export function installModelWeb(ctx) { return new ModelWeb(ctx); }
@@ -0,0 +1,91 @@
1
+ export const modelsZh = {
2
+ modelsPhaseReady: "已就绪",
3
+ modelsPhaseStarting: "启动中",
4
+ modelsPhaseRestart: "需重启",
5
+ modelsPhaseFailed: "启动失败",
6
+ modelsPhaseClosed: "已停止",
7
+ modelsReasonNoModel: "尚未选择模型。",
8
+ modelsReasonCredential: "尚未填写所选提供方的凭据。",
9
+ modelsReasonUnsupported: "所选模型或思考强度不受当前部署支持。",
10
+ modelsReasonChanged: "设置刚刚变化,请刷新查看。",
11
+ modelsReasonNotReady: "当前尚未就绪,请稍后刷新。",
12
+ modelsReasonRestart: "运行配置已变化,请重启后再使用。",
13
+ modelsReasonUnknown: "暂时无法使用,请刷新查看。",
14
+ modelsEffortOff: "关闭",
15
+ modelsEffortLow: "低",
16
+ modelsEffortMedium: "中",
17
+ modelsEffortHigh: "高",
18
+ modelsEffortXHigh: "很高",
19
+ modelsEffortMax: "最高",
20
+ modelsEffortOther: "其他",
21
+ modelsTitle: '模型与学习', modelsRefresh: '刷新状态', modelsLoading: '正在读取设置…',
22
+ modelsReadFailed: '未能读取当前设置;暂不可保存,请刷新后再试。', modelsUnavailable: '当前不可用',
23
+ modelsMain: '主会话模型', modelsMainScope: '在会话输入区使用原生模型按钮。更改影响该会话下一次回答,并保存为新会话默认;其他已有会话保留自己的选择。',
24
+ modelsDefault: '新会话默认', modelsLearn: '回答后的学习', modelsLearnScope: '这里独立选择学习模型。已开始的学习保留原配置;修改只影响之后开始的学习,不重发未知请求。',
25
+ modelsLearnDisabled: '此部署未启用学习。', modelsNoDefault: '尚无可用的学习默认配置。', modelsProvider: '提供方',
26
+ modelsModel: '模型', modelsReasoning: '思考强度', modelsChoose: '请选择', modelsSave: '保存学习设置',
27
+ modelsSaved: '已保存并读回。', modelsSavedUnread: '保存已返回成功,但最新状态未能读回;请刷新确认。',
28
+ modelsConflict: '设置已变化,未覆盖。已尝试刷新,请查看当前版本后重新选择。',
29
+ modelsRefused: '保存被拒绝,设置未获确认。请刷新状态后检查可用选项。',
30
+ modelsUnknown: '未收到明确结果;没有自动重发。请刷新核对后再决定是否重新操作。',
31
+ modelsBusy: '正在处理…', modelsCredentials: '连接凭据', modelsCredentialScope: '仅写入官方凭据存储,不显示旧密钥。“已配置”不代表已通过服务商验证。这里不会发送模型测试请求。',
32
+ modelsConfigured: '已配置', modelsNotConfigured: '未配置', modelsCredentialReadFailed: '凭据状态暂不可读。',
33
+ modelsReadOnly: '当前凭据来源不可写。', modelsCredential: '新密钥', modelsCredentialSave: '保存密钥',
34
+ modelsCredentialRemove: '移除已存密钥', modelsCredentialCancel: '清空输入', modelsCredentialFailed: '凭据操作未获确认;输入已清空。请刷新状态后重试。',
35
+ modelsCredentialUnknown: '凭据操作结果未知,输入已清空且未自动重发。请先刷新核对状态。',
36
+ modelsSource: '来源', modelsNoRoutes: '没有已部署的可用路线。', modelsUsage: '用量与费用',
37
+ modelsUsageMissing: '当前用量不可读;不按零费用显示。', modelsRequests: '请求数', modelsUnknownCount: '结果未知',
38
+ modelsSpent: '已结算', modelsReserved: '预留', modelsCeiling: '上限', modelsNoCeiling: '此范围无金额上限',
39
+ modelsPrompt: '输入 token', modelsCompletion: '输出 token', modelsCached: '缓存输入 token', modelsPending: '待结算',
40
+ modelsTokensOnly: '这里只记录 token 用量,未提供人民币费用估算。',
41
+ modelsUsageBoundary: '各范围独立显示;网页用量不与开发预算相加。改变模型设置不修改费用授权。',
42
+ modelsScopeLimited: '开发研究预算(独立范围)', modelsScopeWeb: 'DeepSeek 网页', modelsScopeOpenai: 'OpenAI 网页',
43
+ modelsGate: '运行状态', modelsRevision: '设置版本',
44
+ };
45
+ export const modelsEn = {
46
+ modelsPhaseReady: "Ready",
47
+ modelsPhaseStarting: "Starting",
48
+ modelsPhaseRestart: "Restart required",
49
+ modelsPhaseFailed: "Startup failed",
50
+ modelsPhaseClosed: "Stopped",
51
+ modelsReasonNoModel: "No model is selected.",
52
+ modelsReasonCredential: "Credentials for the selected provider have not been entered.",
53
+ modelsReasonUnsupported: "The selected model or reasoning effort is not supported by this deployment.",
54
+ modelsReasonChanged: "Settings just changed. Refresh to see the current selection.",
55
+ modelsReasonNotReady: "The runtime is not ready. Refresh shortly.",
56
+ modelsReasonRestart: "Runtime configuration changed. Restart before continuing.",
57
+ modelsReasonUnknown: "Temporarily unavailable. Refresh to check again.",
58
+ modelsEffortOff: "Off",
59
+ modelsEffortLow: "Low",
60
+ modelsEffortMedium: "Medium",
61
+ modelsEffortHigh: "High",
62
+ modelsEffortXHigh: "Very high",
63
+ modelsEffortMax: "Maximum",
64
+ modelsEffortOther: "Other",
65
+ modelsTitle: 'Models and learning', modelsRefresh: 'Refresh status', modelsLoading: 'Loading settings…',
66
+ modelsReadFailed: 'Settings could not be read. Refresh before saving.', modelsUnavailable: 'Unavailable',
67
+ modelsMain: 'Conversation model', modelsMainScope: 'Use the native model picker in the conversation input. A change applies to that conversation’s next answer and the default for new conversations; other existing conversations keep their selection.',
68
+ modelsDefault: 'New conversation default', modelsLearn: 'Learning after an answer', modelsLearnScope: 'Learning has its own model selection. Started learning keeps its original configuration; changes apply only to learning started later and never resend unknown requests.',
69
+ modelsLearnDisabled: 'Learning is not enabled for this deployment.', modelsNoDefault: 'No learning default is available.',
70
+ modelsProvider: 'Provider', modelsModel: 'Model', modelsReasoning: 'Reasoning effort', modelsChoose: 'Select',
71
+ modelsSave: 'Save learning settings', modelsSaved: 'Saved and read back.',
72
+ modelsSavedUnread: 'Save returned success, but the latest state could not be read. Refresh to confirm.',
73
+ modelsConflict: 'Settings changed and were not overwritten. A refresh was attempted; review the current version before selecting again.',
74
+ modelsRefused: 'Save was refused. Refresh and check the available choices.',
75
+ modelsUnknown: 'No definite result was received. Nothing was retried automatically. Refresh before deciding whether to submit again.',
76
+ modelsBusy: 'Working…', modelsCredentials: 'Credentials',
77
+ modelsCredentialScope: 'Write only to the native credential store. Existing keys are never displayed. Configured does not mean verified with the provider; this page makes no model test calls.',
78
+ modelsConfigured: 'Configured', modelsNotConfigured: 'Not configured', modelsCredentialReadFailed: 'Credential status is unavailable.',
79
+ modelsReadOnly: 'This credential source is not writable.', modelsCredential: 'New key',
80
+ modelsCredentialSave: 'Save key', modelsCredentialRemove: 'Remove stored key', modelsCredentialCancel: 'Clear input',
81
+ modelsCredentialFailed: 'Credential operation was not confirmed. The input was cleared. Refresh before trying again.',
82
+ modelsCredentialUnknown: 'Credential outcome is unknown. The input was cleared and nothing was retried. Refresh to check status.',
83
+ modelsSource: 'Source', modelsNoRoutes: 'No supported route is deployed.', modelsUsage: 'Usage and cost',
84
+ modelsUsageMissing: 'Usage is unavailable; it is not shown as zero.', modelsRequests: 'Requests',
85
+ modelsUnknownCount: 'Unknown outcomes', modelsSpent: 'Settled', modelsReserved: 'Reserved', modelsCeiling: 'Limit',
86
+ modelsNoCeiling: 'No monetary limit for this scope', modelsPrompt: 'Input tokens', modelsCompletion: 'Output tokens',
87
+ modelsCached: 'Cached input tokens', modelsPending: 'Pending', modelsTokensOnly: 'Token usage only. No CNY cost estimate is supplied.',
88
+ modelsUsageBoundary: 'Scopes are shown separately; web usage is not added to the development budget. Model settings do not change spending authorization.',
89
+ modelsScopeLimited: 'Development research budget (separate)', modelsScopeWeb: 'DeepSeek web',
90
+ modelsScopeOpenai: 'OpenAI web', modelsGate: 'Runtime status', modelsRevision: 'Settings revision',
91
+ };
@@ -0,0 +1,119 @@
1
+ // A visible-page read cache. Credentials and mutations never enter its snapshot.
2
+ const PROVIDERS = new Set(['deepseek-official', 'openai']);
3
+ const own = (value, keys) => value && typeof value === 'object' && !Array.isArray(value) &&
4
+ Object.keys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key));
5
+ const refused = code => ({ status: 'refused', code });
6
+ const remoteCode = code => ['revision_conflict', 'learning_defaults_conflict'].includes(code) ? 'conflict' : 'refused';
7
+ export function deployedRoutes(data) {
8
+ return (data?.routes || []).filter(route => PROVIDERS.has(route.provider));
9
+ }
10
+ export function validLearningChoice(data, request) {
11
+ const route = deployedRoutes(data).find(row => row.provider === request.provider);
12
+ const model = route?.models?.find(row => row.id === request.model);
13
+ return Boolean(model?.reasoningEfforts?.includes(request.reasoningEffort));
14
+ }
15
+
16
+ export function createModelSettingsSource({ rpc, subscribeReset }) {
17
+ let snapshot = { loading: false, data: null, error: null, credentials: {}, credentialError: false };
18
+ const listeners = new Set();
19
+ let generation = 0, readController, mutationController, reset, disposed = false, writing = false;
20
+ const publish = patch => {
21
+ snapshot = { ...snapshot, ...patch };
22
+ for (const listener of listeners) listener();
23
+ };
24
+ const active = () => !disposed && listeners.size > 0;
25
+ async function refresh() {
26
+ if (!active()) return false;
27
+ readController?.abort();
28
+ const controller = new AbortController(), current = ++generation;
29
+ readController = controller;
30
+ const stillCurrent = () => active() && current === generation && !controller.signal.aborted;
31
+ publish({ loading: true, error: null });
32
+ try {
33
+ const reply = await rpc.call('/api', 'arcfoldModels/read', { args: { request: {} } }, controller.signal);
34
+ if (reply?.ok !== true) throw new Error('read refused');
35
+ if (!stillCurrent()) return false;
36
+ const data = reply.value;
37
+ const refs = [...new Set(deployedRoutes(data).map(route => route.apiKeyRef))];
38
+ let credentials = {}, credentialError = false;
39
+ if (refs.length) {
40
+ try {
41
+ const result = await rpc.call('/api', 'credentials/describe', { args: { refs } }, controller.signal);
42
+ if (result?.ok !== true) throw new Error('credential metadata refused');
43
+ credentials = Object.fromEntries(refs.map(ref => {
44
+ const value = result.value?.[ref];
45
+ return [ref, { configured: value?.configured === true, writable: value?.writable === true,
46
+ ...(typeof value?.source === 'string' ? { source: value.source } : {}) }];
47
+ }));
48
+ } catch { credentialError = true; }
49
+ }
50
+ if (!stillCurrent()) return false;
51
+ publish({ loading: false, data, error: null, credentials, credentialError });
52
+ return true;
53
+ } catch {
54
+ if (stillCurrent()) publish({ loading: false, error: 'read_failed' });
55
+ return false;
56
+ }
57
+ }
58
+ function stop() {
59
+ reset?.(); reset = undefined;
60
+ readController?.abort(); mutationController?.abort(); generation++;
61
+ }
62
+ async function mutate(endpoint, args, credential = false) {
63
+ if (!active() || writing) return refused('unavailable');
64
+ writing = true;
65
+ const controller = new AbortController();
66
+ mutationController = controller;
67
+ try {
68
+ const result = await rpc.call('/api', endpoint, { args }, controller.signal);
69
+ if (result?.ok !== true) {
70
+ const code = credential ? 'credential_refused' : remoteCode(result?.error?.code);
71
+ await refresh();
72
+ return refused(code);
73
+ }
74
+ const refreshed = await refresh();
75
+ return { status: 'saved', refreshed };
76
+ } catch {
77
+ // No exception, response details or input value is retained or rethrown.
78
+ return { status: 'unknown', code: credential ? 'credential_unknown' : 'unknown' };
79
+ } finally {
80
+ writing = false;
81
+ if (mutationController === controller) mutationController = undefined;
82
+ }
83
+ }
84
+ return {
85
+ getSnapshot: () => snapshot,
86
+ subscribe(listener) {
87
+ if (disposed) return () => {};
88
+ listeners.add(listener);
89
+ if (listeners.size === 1) {
90
+ reset = subscribeReset(() => { void refresh(); });
91
+ void refresh();
92
+ }
93
+ return () => { listeners.delete(listener); if (!listeners.size) stop(); };
94
+ },
95
+ refresh,
96
+ saveLearning(request) {
97
+ const data = snapshot.data;
98
+ if (!own(request, ['expectedRevision', 'provider', 'model', 'reasoningEffort']) ||
99
+ !Number.isSafeInteger(request.expectedRevision) || request.expectedRevision < 1 ||
100
+ data?.gate?.phase !== 'ready' || snapshot.error || snapshot.loading ||
101
+ !data?.learning?.enabled || !data.learning.defaults ||
102
+ !validLearningChoice(data, request)) return Promise.resolve(refused('unavailable'));
103
+ return mutate('arcfoldModels/saveLearning', { request: { ...request } });
104
+ },
105
+ storeCredential(provider, value) {
106
+ const route = deployedRoutes(snapshot.data).find(row => row.provider === provider);
107
+ if (!route || !snapshot.credentials[route.apiKeyRef]?.writable || snapshot.error || snapshot.loading ||
108
+ typeof value !== 'string' || !value.trim()) return Promise.resolve(refused('credential_unavailable'));
109
+ return mutate('credentials/set', { ref: route.apiKeyRef, value }, true);
110
+ },
111
+ removeCredential(provider) {
112
+ const route = deployedRoutes(snapshot.data).find(row => row.provider === provider);
113
+ if (!route || !snapshot.credentials[route.apiKeyRef]?.writable || snapshot.error || snapshot.loading)
114
+ return Promise.resolve(refused('credential_unavailable'));
115
+ return mutate('credentials/unset', { ref: route.apiKeyRef }, true);
116
+ },
117
+ dispose() { disposed = true; stop(); listeners.clear(); },
118
+ };
119
+ }
@@ -0,0 +1,118 @@
1
+ import { currentMainAgent } from './main-session.mjs';
2
+ import { browseScope } from './browse-scope.mjs';
3
+ import { TypertRemoteService, RemoteError } from '@deepseek-ai/dsh-typert-protocol';
4
+ const NAMESPACE = 'arcfoldOpportunities', SERVICE = 'arcfoldOpportunityWeb';
5
+ const ENDPOINTS = ['list', 'details', 'source', 'savePermission', 'prepare', 'stop', 'dismiss', 'acknowledge', 'continue'];
6
+ const invalid = message => { throw Object.assign(new Error(message), { code: 'invalid_opportunity_request' }); };
7
+ function object(value, fields, required = fields) {
8
+ if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some(key => !fields.includes(key)) ||
9
+ required.some(key => !Object.hasOwn(value, key))) invalid('机会请求缺少必要内容或含未允许字段。');
10
+ }
11
+ function identity(value) { if (typeof value !== 'string' || !value.trim() || value.length > 512 || value.includes('\0')) invalid('请求身份无效。'); }
12
+ function trusted(connection, signal, peer) {
13
+ if (!peer || peer !== connection.browserPeer || peer.principal?.kind !== 'owner' || peer.interactions !== 'answer' ||
14
+ peer.signal?.aborted !== false || signal?.aborted !== false)
15
+ throw Object.assign(new Error('此操作只接受当前已认证网页用户。'), { code: 'trusted_browser_required' });
16
+ }
17
+ export function createOpportunityHandler({ opportunities, preparation, connection, agents, agentPresets, prompt, inflight = new Map() }) {
18
+ const main = (sessionId, expected) => {
19
+ const agent = currentMainAgent({ agents, agentPresets }, sessionId);
20
+ if (!agent || expected && agent !== expected)
21
+ throw Object.assign(new Error('请先打开原来的 Arcfold 主交流,再发送继续工作的要求。'), { code: 'opportunity_session_denied' });
22
+ return agent;
23
+ };
24
+ return async (endpoint, payload, signal, peer) => {
25
+ try {
26
+ trusted(connection, signal, peer); let value; const userIdentity = 'browser:' + peer.id;
27
+ if (endpoint === 'list') {
28
+ object(payload, ['sessionId', 'offset', 'limit', 'viewScope'], ['sessionId']); identity(payload.sessionId); browseScope(payload.viewScope);
29
+ value = { ...opportunities.list(payload.sessionId, payload), canContinue: currentMainAgent({ agents, agentPresets }, payload.sessionId) !== null };
30
+ } else if (endpoint === 'details') {
31
+ object(payload, ['sessionId', 'opportunityId', 'viewScope'], ['sessionId', 'opportunityId']); identity(payload.sessionId); identity(payload.opportunityId);
32
+ const all = browseScope(payload.viewScope) === 'all';
33
+ value = opportunities.details(payload.opportunityId);
34
+ if (!all && value.opportunity.conversationId !== payload.sessionId) invalid('请从机会原来的交流查看。');
35
+ } else if (endpoint === 'source') {
36
+ object(payload, ['sourceId']); identity(payload.sourceId);
37
+ const source = opportunities.store.get('events', payload.sourceId);
38
+ if (!source) invalid('原件不存在。');
39
+ value = { source, control: opportunities.store.get('source_controls', payload.sourceId) };
40
+ } else if (endpoint === 'savePermission') {
41
+ object(payload, ['sessionId', 'enabled', 'extraSourceIds', 'files', 'expectedRevision', 'requestId']);
42
+ value = opportunities.savePermission({ ...payload, userIdentity });
43
+ } else if (endpoint === 'prepare') {
44
+ object(payload, ['opportunityId', 'expectedRevision', 'permissionRevision', 'requestId']); identity(payload.requestId);
45
+ const bound = opportunities.bindPreparation({ ...payload, userIdentity }), key = bound.binding.id;
46
+ if (inflight.has(key)) value = inflight.get(key);
47
+ else if (!bound.created) value = bound;
48
+ else {
49
+ const pending = Promise.resolve().then(async () => {
50
+ let state = 'failed', runId = null;
51
+ try {
52
+ trusted(connection, signal, peer);
53
+ const run = await preparation.prepare(payload); runId = run.id; state = 'accepted';
54
+ } catch { /* Failed/uncertain admission remains a consumed request. */ }
55
+ return { ...bound, binding: opportunities.preparationAdmission(key, state, runId) };
56
+ });
57
+ inflight.set(key, pending); value = pending.finally(() => { if (inflight.get(key) === pending) inflight.delete(key); });
58
+ }
59
+ } else if (endpoint === 'stop') {
60
+ object(payload, ['runId', 'expectedRevision', 'requestId']); value = opportunities.stop({ ...payload, userIdentity });
61
+ } else if (endpoint === 'dismiss') {
62
+ object(payload, ['opportunityId', 'expectedRevision', 'requestId']); value = opportunities.dismiss({ ...payload, userIdentity });
63
+ } else if (endpoint === 'acknowledge') {
64
+ object(payload, ['notificationId']); identity(payload.notificationId); value = opportunities.acknowledge(payload.notificationId);
65
+ } else if (endpoint === 'continue') {
66
+ object(payload, ['sessionId', 'opportunityId', 'expectedRevision', 'requestId', 'text']);
67
+ const agent = main(payload.sessionId);
68
+ const bound = opportunities.bindContinuation({ ...payload, userIdentity }), key = bound.binding.id;
69
+ if (inflight.has(key)) value = inflight.get(key);
70
+ else if (!bound.created) value = bound;
71
+ else {
72
+ let entered = false;
73
+ const pending = Promise.resolve().then(async () => {
74
+ let state;
75
+ try {
76
+ trusted(connection, signal, peer);
77
+ main(payload.sessionId, agent);
78
+ if (!opportunities.isMain(payload.sessionId)) invalid('当前主交流不可用。');
79
+ entered = true;
80
+ const result = await prompt({ sessionId: payload.sessionId, requestId: payload.requestId, mode: 'queue',
81
+ content: [{ type: 'text', text: payload.text }] }, signal);
82
+ state = result?.accepted === true ? 'accepted' : 'unknown';
83
+ } catch { state = entered ? 'unknown' : 'failed'; }
84
+ return { ...bound, binding: opportunities.continuationAdmission(key, state) };
85
+ });
86
+ inflight.set(key, pending); value = pending.finally(() => { if (inflight.get(key) === pending) inflight.delete(key); });
87
+ }
88
+ } else invalid('机会操作不存在。');
89
+ return { ok: true, value: await value };
90
+ } catch (error) { return { ok: false, error: { code: error?.code ?? 'opportunity_request_failed', message: error.message ?? '操作未完成。' } }; }
91
+ };
92
+ }
93
+ class OpportunityWeb extends TypertRemoteService {
94
+ inflight = new Map();
95
+ constructor(ctx) { super(ctx, SERVICE, { namespace: NAMESPACE }); }
96
+ async dispatch(endpoint, payload) {
97
+ const invocation = this.ctx.invocation, controller = this.ctx.sessionController;
98
+ const result = await createOpportunityHandler({ opportunities: this.ctx.arcfoldOpportunities, preparation: this.ctx.arcfoldPreparation,
99
+ connection: this.ctx.connection, agents: this.ctx.agents, agentPresets: this.ctx.agentPresets, inflight: this.inflight,
100
+ prompt: (payload, signal) => controller.prompt(payload, signal) })(endpoint, payload, invocation?.signal, invocation?.peer);
101
+ if (!result.ok) throw new RemoteError(result.error.code, result.error.message, {}); return result.value;
102
+ }
103
+ list(p) { return this.dispatch('list', p); }
104
+ details(p) { return this.dispatch('details', p); }
105
+ source(p) { return this.dispatch('source', p); }
106
+ savePermission(p) { return this.dispatch('savePermission', p); }
107
+ prepare(p) { return this.dispatch('prepare', p); }
108
+ stop(p) { return this.dispatch('stop', p); }
109
+ dismiss(p) { return this.dispatch('dismiss', p); }
110
+ acknowledge(p) { return this.dispatch('acknowledge', p); }
111
+ continue(p) { return this.dispatch('continue', p); }
112
+ }
113
+ export const opportunityInvocations = ENDPOINTS.map(method => ({ id: `@arcfold/dsh-method-ui#${NAMESPACE}/${method}`,
114
+ service: SERVICE, namespace: NAMESPACE, method, invocation: { kind: 'direct' },
115
+ parameters: [{ name: 'request', wire: 'request', source: 'json', codec: { mode: 'src-json' } }], result: { mode: 'src-json' } }));
116
+ export function installOpportunityWeb(ctx) {
117
+ return ctx.inject(['arcfoldOpportunities', 'arcfoldPreparation', 'sessionController', 'connection', 'agents', 'agentPresets'], function opportunityWeb(owner) { new OpportunityWeb(owner); });
118
+ }
@@ -0,0 +1,34 @@
1
+ export const opportunitiesZh = {
2
+ opportunityReadOnly: '这里汇集值得留意的建议与已有准备稿,供你阅读和判断。',
3
+ opportunityRationale: '为什么值得关注', opportunitySuggestion: '可以准备什么', opportunityWorkspace: '工作目录', opportunityDismissed: '已忽略',
4
+ opportunityTitle: '机会与准备', opportunityOpen: '查看机会', opportunityHide: '收起', opportunityEmpty: '暂无已提交的机会。',
5
+ opportunityBoundary: '机会是待判断的建议。许可只允许在选定材料范围内准备一份新稿,不代表采纳或继续执行。',
6
+ opportunityPermission: '自动准备范围', opportunityEnable: '允许本交流的新机会自动准备(含未来消息)',
7
+ opportunityFiles: '本工作目录内的材料文件,每行一个路径,最多四个', opportunitySources: '附加原件编号,每行一个;只授权这些具体原件',
8
+ opportunityCapacity: '每个文件最多 256 KiB,合计 512 KiB;完整材料最多 1 MiB。每份新稿最多 64 KiB、4 次模型请求、6 次获准工具动作、5 分钟。这是单次容量限制,不是费用上限。',
9
+ opportunitySave: '保存范围许可', opportunityOff: '自动准备未开启', opportunityOn: '自动准备已开启', opportunityVersion: '许可版本',
10
+ opportunityHistorical: '开启前已有的机会不会自动启动;需要逐项选择准备。', opportunityPrepare: '准备这一项', opportunityStop: '停止本次准备',
11
+ opportunityDismiss: '忽略机会', opportunityRead: '标为已读', opportunityContinue: '发送新的委托', opportunityText: '你现在希望怎样继续?请写完整原话。',
12
+ opportunityMainRequired: '请先打开原来的主交流,再发送继续工作的要求。',
13
+ opportunityDraft: '已写入并全文回读的准备稿', opportunityEvidence: '冻结时的原件、角色、版本及全部材料', opportunityNotUsefulYet: '完成文件检查不证明建议有用;请审阅内容后决定。',
14
+ opportunitySaved: '决定已保存。', opportunityFailed: '提交未开始执行,原记录已保留,不会自动重发。', opportunityUnknown: '提交结果未确认,保留原记录,不自动重发。', opportunityAccepted: '原生入口已受理;实际执行仍以工作记录为准。',
15
+ opportunityRejected: '材料检查未通过,尚未执行', opportunityPending: '尚未启动', opportunityPrepared: '准备提交中', opportunitySubmitted: '已提交原生执行', opportunityUncertain: '结果未确定,需要查看',
16
+ opportunityStopped: '已停止或依据失效', opportunitySettled: '完成记录已保留', opportunityUnread: '未读', opportunityAlreadyRead: '已读',
17
+ };
18
+ export const opportunitiesEn = {
19
+ opportunityReadOnly: 'Suggestions and existing drafts, collected here for you to read and consider.',
20
+ opportunityRationale: 'Why it matters', opportunitySuggestion: 'Suggested preparation', opportunityWorkspace: 'Workspace', opportunityDismissed: 'Dismissed',
21
+ opportunityTitle: 'Opportunities and drafts', opportunityOpen: 'View opportunities', opportunityHide: 'Hide', opportunityEmpty: 'No committed opportunities yet.',
22
+ opportunityBoundary: 'An opportunity is a suggestion to review. Permission allows one new draft using selected material; it does not authorize adoption or further work.',
23
+ opportunityPermission: 'Automatic preparation scope', opportunityEnable: 'Allow new opportunities in this conversation, including future messages, to prepare a draft',
24
+ opportunityFiles: 'Material files inside this workspace, one path per line, at most four', opportunitySources: 'Additional original source IDs, one per line; only these exact originals are authorized',
25
+ opportunityCapacity: 'Each file: 256 KiB; combined files: 512 KiB; complete input: 1 MiB. Each draft: 64 KiB, 4 model requests, 6 authorized tool actions and 5 minutes. These are per-attempt capacity limits, not spending limits.',
26
+ opportunitySave: 'Save scope permission', opportunityOff: 'Automatic preparation is off', opportunityOn: 'Automatic preparation is on', opportunityVersion: 'Permission version',
27
+ opportunityHistorical: 'Existing opportunities do not start when permission is enabled. Select each one explicitly.', opportunityPrepare: 'Prepare this', opportunityStop: 'Stop preparation',
28
+ opportunityDismiss: 'Dismiss opportunity', opportunityRead: 'Mark read', opportunityContinue: 'Send new instruction', opportunityText: 'What should happen next? Enter your complete new instruction.',
29
+ opportunityMainRequired: 'Open the original main conversation before sending a new instruction.',
30
+ opportunityDraft: 'Draft written and fully read back', opportunityEvidence: 'Originals, roles, versions and complete frozen material', opportunityNotUsefulYet: 'File verification does not prove usefulness. Review the content before deciding.',
31
+ opportunitySaved: 'Decision saved.', opportunityFailed: 'Execution did not start. The original submission is retained and will not be resent automatically.', opportunityUnknown: 'Submission is unconfirmed. Its original record is retained and will not be resent automatically.', opportunityAccepted: 'The native entry accepted the submission. Actual work is shown by its execution record.',
32
+ opportunityRejected: 'Admission refused; no execution occurred', opportunityPending: 'Not started', opportunityPrepared: 'Preparing submission', opportunitySubmitted: 'Submitted to native execution', opportunityUncertain: 'Outcome unknown; review needed',
33
+ opportunityStopped: 'Stopped or supporting material unavailable', opportunitySettled: 'Completion record retained', opportunityUnread: 'Unread', opportunityAlreadyRead: 'Read',
34
+ };
@@ -0,0 +1,56 @@
1
+ import { requireViewScope, viewScopeFields } from './browse-source.mjs';
2
+ export async function callOpportunity(rpc, endpoint, payload, signal) {
3
+ const result = await rpc.call('/api', `arcfoldOpportunities/${endpoint}`, { args: { request: payload } }, signal);
4
+ if (result?.ok !== true) throw Object.assign(new Error(result?.error?.message ?? '机会请求未完成。'), { code: result?.error?.code ?? 'opportunity_request_failed', remote: true });
5
+ return result.value;
6
+ }
7
+ export function createOpportunitySource({ rpc, sessionId, viewScope, subscribeReset, intervalMs = 5000 }) {
8
+ viewScopeFields(viewScope);
9
+ let state = { items: [], total: 0, offset: 0, nextOffset: null, loading: true, permission: null, error: null, resetVersion: 0 };
10
+ let generation = 0, disposed = false, controller, timer, reset; const listeners = new Set(), reads = new Set();
11
+ const publish = value => { state = { ...state, ...value }; for (const listener of listeners) listener(); };
12
+ function invalidateReads() {
13
+ controller?.abort(); generation++;
14
+ for (const read of reads) read.abort();
15
+ publish({ items: [], total: 0, offset: 0, nextOffset: null, loading: true, permission: null,
16
+ canPrepare: false, canContinue: false, error: null, resetVersion: state.resetVersion + 1 });
17
+ }
18
+ async function refresh(offset = state.offset) {
19
+ if (disposed) return; controller?.abort(); controller = new AbortController(); const signal = controller.signal, current = ++generation;
20
+ try { const value = await callOpportunity(rpc, 'list', { sessionId, offset, limit: 20, ...viewScopeFields(viewScope) }, signal);
21
+ if (!disposed && current === generation && !signal.aborted) publish({ ...value, offset, loading: false, error: null });
22
+ } catch (error) { if (!disposed && current === generation && !signal.aborted) publish({ loading: false, error: error.message }); }
23
+ }
24
+ const assertLive = () => { if (disposed) throw Object.assign(new Error('机会视图已关闭。'), { code: 'opportunity_view_disposed' }); };
25
+ const stop = () => { clearInterval(timer); timer = undefined; reset?.(); reset = null; invalidateReads(); };
26
+ return {
27
+ getSnapshot: () => state,
28
+ subscribe(listener) { if (disposed) return () => {}; listeners.add(listener);
29
+ if (listeners.size === 1) { void refresh(); timer = setInterval(() => { void refresh(); }, intervalMs); reset = subscribeReset(() => { invalidateReads(); void refresh(0); }); }
30
+ return () => { listeners.delete(listener); if (!listeners.size) stop(); }; },
31
+ refresh,
32
+ setViewScope(value) {
33
+ requireViewScope(value);
34
+ if (disposed || viewScope === value) return;
35
+ viewScope = value; invalidateReads();
36
+ if (listeners.size) void refresh(0);
37
+ },
38
+ async details(opportunityId) {
39
+ assertLive(); const version = state.resetVersion, read = new AbortController(); reads.add(read);
40
+ try {
41
+ const value = await callOpportunity(rpc, 'details', { sessionId, opportunityId, ...viewScopeFields(viewScope) }, read.signal);
42
+ assertLive();
43
+ if (read.signal.aborted || version !== state.resetVersion)
44
+ throw Object.assign(new Error('查看范围或连接已变化,请重新查看机会。'), { code: 'opportunity_view_reset' });
45
+ return value;
46
+ } finally { reads.delete(read); }
47
+ },
48
+ source: sourceId => { assertLive(); return callOpportunity(rpc, 'source', { sourceId }); },
49
+ async act(endpoint, request) {
50
+ assertLive();
51
+ const payload = ['savePermission', 'continue'].includes(endpoint) ? { ...request, sessionId } : request;
52
+ const value = await callOpportunity(rpc, endpoint, payload); await refresh(); return value;
53
+ },
54
+ dispose() { disposed = true; stop(); listeners.clear(); },
55
+ };
56
+ }
package/src/style.css ADDED
@@ -0,0 +1,99 @@
1
+ .arcfold-methods {box-sizing:border-box;width:calc(100% - 4 * var(--dsh-composer-dock-inset, 8px));max-width:var(--dsh-composer-card-max-width, 800px);margin:0 auto 8px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l1);border-radius:var(--dsw-radius-md, 12px);background:var(--dsw-specific-menu);color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5;}
2
+ .arcfold-methods header,.arcfold-methods nav,.arcfold-methods .actions {display:flex;align-items:center;flex-wrap:wrap;gap:8px;}
3
+ .arcfold-methods header strong {flex:1;}
4
+ .arcfold-methods button {font:inherit;color:inherit;background:transparent;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:4px 8px;cursor:pointer;}
5
+ .arcfold-methods button:disabled {opacity:.5;cursor:not-allowed;}
6
+ .arcfold-methods button:focus-visible {outline:2px solid currentColor;outline-offset:2px;}
7
+ .arcfold-methods ul {list-style:none;padding:0;margin:8px 0;}
8
+ .arcfold-methods li {margin:8px 0;padding-block:4px;}
9
+ .arcfold-methods p {margin:6px 0;}
10
+ .arcfold-methods .detail {max-height:min(60vh,36rem);overflow:auto;overscroll-behavior:contain;margin-top:8px;border-top:1px solid var(--dsw-alias-border-l1);padding-top:8px;}
11
+ .arcfold-methods .text {white-space:pre-wrap;overflow-wrap:anywhere;}
12
+ .arcfold-methods .meta {font-size:12px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere;}
13
+ .arcfold-methods [role=alert] {color:var(--dsw-alias-state-error-primary);white-space:pre-wrap;}
14
+ .arcfold-methods h3,.arcfold-methods h4 {font-size:13px;margin:12px 0 4px;}
15
+ .arcfold-methods .source {border-left:2px solid var(--dsw-alias-border-l1);padding-left:8px;}
16
+ .arcfold-goals textarea {display:block;box-sizing:border-box;width:100%;min-height:5rem;margin:6px 0;padding:8px;font:inherit;resize:vertical;color:inherit;background:transparent;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;}
17
+ .arcfold-goals .goal-discussion {margin-top:12px;}
18
+ .arcfold-goals .goal-work {margin:10px 0;padding-block:4px;}
19
+ .arcfold-goals .goal-work button {margin:4px 8px 4px 0;}
20
+ .arcfold-goals summary {cursor:pointer;}
21
+ .arcfold-goals .goal-cards {display:grid;gap:14px;}
22
+ .arcfold-goals .goal-card {min-width:0;padding:18px;border:1px solid var(--dsw-alias-border-l1);border-radius:12px;background:var(--dsw-alias-bg-layer-1);overflow-wrap:anywhere;}
23
+ .arcfold-goals .goal-card-heading {align-items:flex-start;justify-content:space-between;gap:12px;}
24
+ .arcfold-goals .goal-card-heading h3 {flex:1;min-width:0;margin:0;font-size:15px;line-height:1.6;}
25
+ .arcfold-goals .goal-status-badge {display:inline-block;flex-shrink:0;border:1px solid var(--dsw-alias-border-l1);border-radius:6px;padding:2px 8px;font-size:11px;color:var(--dsw-alias-label-secondary);}
26
+ .arcfold-goals .goal-outcome {margin-top:12px;color:var(--dsw-alias-label-secondary);}
27
+ .arcfold-goals .goal-card-section {margin-top:18px;}
28
+ .arcfold-goals .goal-card-section h4 {margin:0 0 8px;font-size:12px;color:var(--dsw-alias-label-secondary);}
29
+ .arcfold-goals .goal-work {margin:0;padding:0;}
30
+ .arcfold-goals .goal-work + .goal-work {border-top:1px solid var(--dsw-alias-border-l1);margin-top:14px;padding-top:14px;}
31
+ .arcfold-goals .goal-work-blocker {margin-top:10px;padding:10px 12px;border-left:3px solid var(--dsw-alias-border-l1);background:var(--dsw-specific-menu);border-radius:0 6px 6px 0;}
32
+ .arcfold-goals .goal-work-blocker h5 {margin:0;font-size:12px;}
33
+ .arcfold-goals .goal-work-details {margin-top:8px;font-size:12px;}
34
+ .arcfold-goals .goal-card-evidence {border-top:1px solid var(--dsw-alias-border-l1);margin-top:18px;padding-top:12px;}
35
+ .arcfold-goals .goal-card-evidence > summary,.arcfold-goals .goal-work-details > summary {color:var(--dsw-alias-label-secondary);}
36
+ .arcfold-goals .goal-other-work {margin-top:16px;}
37
+ .arcfold-goals summary:focus-visible {outline:2px solid currentColor;outline-offset:3px;}
38
+ @media (max-width:480px) {.arcfold-goals .goal-card {padding:14px;}.arcfold-goals .goal-card-heading h3 {flex-basis:100%;}}
39
+
40
+ .arcfold-model-settings {width:100%;max-width:52rem;margin:0;padding:16px;}
41
+ .arcfold-model-settings header h2 {flex:1;font-size:16px;margin:0;}
42
+ .arcfold-model-settings h3 {font-size:14px;margin-top:18px;}
43
+ .arcfold-model-settings .model-card {border-top:1px solid var(--dsw-alias-border-l1);padding:8px 0;}
44
+ .arcfold-model-settings label {display:flex;flex-direction:column;gap:4px;min-width:0;}
45
+ .arcfold-model-settings input,.arcfold-model-settings select {box-sizing:border-box;min-width:0;max-width:100%;width:100%;padding:7px;font:inherit;color:inherit;background:var(--dsw-specific-menu);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;}
46
+ .arcfold-model-settings .model-fields {display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,150px),1fr));gap:8px;margin:10px 0;}
47
+ .arcfold-model-settings .actions {margin:8px 0;}
48
+ .arcfold-model-settings dl {margin:8px 0;display:flex;flex-wrap:wrap;gap:8px 20px;}
49
+ .arcfold-model-settings dt {font-size:12px;color:var(--dsw-alias-label-secondary);}
50
+ .arcfold-model-settings dd {margin:0;overflow-wrap:anywhere;}
51
+
52
+ .arcfold-understandings .text { white-space: pre-wrap; overflow-wrap: anywhere; }
53
+ .arcfold-understandings textarea { display: block; box-sizing: border-box; width: 100%; min-height: 8rem; resize: vertical; }
54
+ .arcfold-understandings summary { cursor: pointer; overflow-wrap: anywhere; }
55
+
56
+ .arcfold-imports label { display:flex; flex-direction:column; gap:4px; margin:8px 0; }
57
+ .arcfold-imports input,.arcfold-imports select { box-sizing:border-box; width:100%; max-width:100%; min-width:0; padding:7px; font:inherit; color:inherit; background:var(--dsw-specific-menu); border:1px solid var(--dsw-alias-border-l1); border-radius:8px; }
58
+ .arcfold-imports .import-counts { display:flex; flex-wrap:wrap; gap:8px 20px; }
59
+ .arcfold-imports dt { font-size:12px; color:var(--dsw-alias-label-secondary); }
60
+ .arcfold-imports dd { margin:0; }
61
+ .arcfold-imports .import-decision { border-top:1px solid var(--dsw-alias-border-l1); margin-top:12px; padding-top:8px; }
62
+ .arcfold-imports summary { cursor:pointer; overflow-wrap:anywhere; }
63
+
64
+ .arcfold-view {box-sizing:border-box;width:100%;max-width:60rem;margin:0 auto;padding:16px;min-width:0;color:var(--dsw-alias-label-primary);}
65
+ .arcfold-toolbar {display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;margin-bottom:16px;}
66
+ .arcfold-toolbar h2 {font-size:16px;margin:0;}
67
+ .arcfold-view-scope {display:flex;flex-wrap:wrap;gap:6px;margin:0 0 16px;}
68
+ .arcfold-view-scope button {font:inherit;color:inherit;background:transparent;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:7px 12px;cursor:pointer;}
69
+ .arcfold-view-scope button[aria-pressed="true"] {background:var(--dsw-alias-bg-layer-1);border-color:currentColor;}
70
+ .arcfold-view-scope button:focus-visible {outline:2px solid currentColor;outline-offset:2px;}
71
+ .arcfold-toolbar button,.arcfold-hero-entry button {font:inherit;color:inherit;background:transparent;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:7px 12px;cursor:pointer;}
72
+ .arcfold-toolbar button:focus-visible,.arcfold-hero-entry button:focus-visible {outline:2px solid currentColor;outline-offset:2px;}
73
+ .arcfold-view > .arcfold-methods,.arcfold-dialog > .arcfold-model-settings {width:100%;max-width:none;}
74
+ .arcfold-view > .arcfold-methods {margin-bottom:16px;padding:18px;border-radius:14px;}
75
+ .arcfold-view > .arcfold-methods > header {gap:10px;min-height:30px;}
76
+ .arcfold-view > .arcfold-methods > header > strong {font-size:14px;}
77
+ .arcfold-view > .arcfold-methods > .detail {max-height:none;overflow:visible;margin-top:16px;padding-top:16px;}
78
+ .arcfold-view > .arcfold-methods > .detail > ul > li {padding:14px;margin:10px 0;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;overflow-wrap:anywhere;}
79
+ .arcfold-opportunities .opportunity-overview {padding:0 0 16px;}
80
+ .arcfold-opportunities .opportunity-overview > p:first-child {margin:0 0 10px;color:var(--dsw-alias-label-secondary);}
81
+ .arcfold-opportunities .opportunity-status {display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-bottom:10px;}
82
+ .arcfold-opportunities .status-badge {display:inline-block;border:1px solid var(--dsw-alias-border-l1);border-radius:6px;padding:2px 8px;font-size:11px;line-height:1.6;color:var(--dsw-alias-label-secondary);}
83
+ .arcfold-opportunities ul.opportunity-list {display:grid;gap:12px;margin:0;}
84
+ .arcfold-view > .arcfold-opportunities > .detail > ul.opportunity-list > li {min-width:0;margin:0;padding:18px;background:var(--dsw-alias-bg-layer-1);border-radius:12px;}
85
+ .arcfold-opportunities .opportunity-card > h3 {margin:0;font-size:14px;font-weight:600;line-height:1.7;}
86
+ .arcfold-opportunities .opportunity-field {margin-top:14px;}
87
+ .arcfold-opportunities .opportunity-card h4 {margin:0 0 4px;font-size:11px;font-weight:500;color:var(--dsw-alias-label-secondary);}
88
+ .arcfold-opportunities .opportunity-field p {margin:0;line-height:1.7;}
89
+ .arcfold-opportunities .opportunity-draft,.arcfold-opportunities .opportunity-evidence {border-top:1px solid var(--dsw-alias-border-l1);margin-top:16px;padding-top:12px;}
90
+ .arcfold-opportunities .opportunity-evidence summary {cursor:pointer;font-size:12px;color:var(--dsw-alias-label-secondary);}
91
+ .arcfold-opportunities .opportunity-evidence summary:focus-visible {outline:2px solid currentColor;outline-offset:4px;}
92
+ .arcfold-opportunities .opportunity-card pre {font:inherit;font-size:12px;line-height:1.7;max-width:100%;}
93
+ .arcfold-opportunities .opportunity-pagination {margin-top:16px;justify-content:flex-end;}
94
+ .arcfold-hero-entry {display:flex;flex-wrap:wrap;justify-content:center;gap:8px;color:var(--dsw-alias-label-primary);}
95
+ .arcfold-dialog {box-sizing:border-box;width:min(56rem,calc(100vw - 24px));max-width:calc(100vw - 24px);max-height:calc(100dvh - 32px);overflow:auto;overscroll-behavior:contain;padding:16px;border:1px solid var(--dsw-alias-border-l1);border-radius:12px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);}
96
+ .arcfold-dialog::backdrop {background:rgb(0 0 0 / .35);}
97
+ .arcfold-dialog > .arcfold-view {padding:0;}
98
+ @media (max-width:480px) {.arcfold-hero-entry {flex-direction:column;align-items:stretch;}.arcfold-view,.arcfold-dialog {padding:12px;}.arcfold-methods header {align-items:flex-start;}.arcfold-methods header strong {flex-basis:100%;}}
99
+ @media (max-width:480px) {.arcfold-view > .arcfold-methods {padding:14px;margin-bottom:12px;}.arcfold-view > .arcfold-opportunities > .detail > ul.opportunity-list > li {padding:14px;}}
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+ import { createTreeSource } from './tree-source.mjs';
3
+ import { TreeView, TreeDock, TreeTurnTail } from './tree-view.mjs';
4
+ // B7 calls this only in the explicit v3 profile, instead of old goal cards.
5
+ export function installTreeClient(ctx) {
6
+ const sources = new Map();
7
+ const props = sessionId => {
8
+ if (!sources.has(sessionId)) sources.set(sessionId, createTreeSource({ rpc: ctx.connection.rpc, sessionId, subscribeReset: listener => ctx.on('connection/reset', listener) }));
9
+ return { source: sources.get(sessionId) };
10
+ };
11
+ ctx.effect(() => () => { sources.forEach(source => source.dispose()); sources.clear(); }, 'Arcfold tree reads');
12
+ ctx.effect(() => { const style = document.createElement('style'); style.dataset.plugin = 'arcfold-tree-v3'; style.textContent = typeof __ARCFOLD_TREE_CSS__ === 'string' ? __ARCFOLD_TREE_CSS__ : ''; document.head.append(style); return () => style.remove(); }, 'Arcfold tree appearance');
13
+ for (const [name, component] of [['conversation.input.dock', TreeDock], ['conversation.view', TreeView], ['conversation.chat.turnTail', ({ source, turn }) => React.createElement(TreeTurnTail, { source, nativeTurn: turn.turn })]])
14
+ ctx.slots.inject(name, () => ctx.slots.register({ name, id: 'arcfold-tree-v3', order: 20, label: 'Arcfold', inject: props }, component));
15
+ }