dot-studio 0.0.1 → 0.0.3

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 (148) hide show
  1. package/README.md +20 -200
  2. package/client/assets/ActFrame-BYOBkLYW.js +1 -0
  3. package/client/assets/ActFrame-C_WEt6bv.css +1 -0
  4. package/client/assets/ActInspectorPanel-C3VlS7tB.js +1 -0
  5. package/client/assets/ActInspectorPanel-CE6s6GYv.css +1 -0
  6. package/client/assets/AssistantChat-BOyW0K79.js +1 -0
  7. package/client/assets/AssistantChat-DoVmHvMJ.css +1 -0
  8. package/client/assets/CanvasTerminalFrame-BC-79q9U.css +1 -0
  9. package/client/assets/CanvasTerminalFrame-DxKbexK6.js +4 -0
  10. package/client/assets/CanvasTrackingFrame-DumxhNwg.js +1 -0
  11. package/client/assets/CanvasTrackingFrame-G4rRrfne.css +1 -0
  12. package/client/assets/CanvasWindowFrame-ziJeVfHG.js +1 -0
  13. package/client/assets/DanceBundleEditorFrame-CH8VDUMK.js +1 -0
  14. package/client/assets/DanceBundleEditorFrame-DaLqMflT.css +1 -0
  15. package/client/assets/MarkdownEditorFrame-DVecIZpZ.css +1 -0
  16. package/client/assets/MarkdownEditorFrame-Dwpgs2GX.js +2 -0
  17. package/client/assets/MarkdownRenderer-Cz8A4AgP.js +1 -0
  18. package/client/assets/PublishModal-DUlHz0fT.js +1 -0
  19. package/client/assets/TodoDock-DcVf7zQG.js +1 -0
  20. package/client/assets/WorkspaceToolbar-CXYi_sMD.js +2 -0
  21. package/client/assets/WorkspaceToolbar-CiQvVocC.css +1 -0
  22. package/client/assets/chat-message-visibility-YwJ-AQno.js +11 -0
  23. package/client/assets/dnd-vendor-CIAZE2P2.js +5 -0
  24. package/client/assets/flow-vendor-BZV40eAE.css +1 -0
  25. package/client/assets/flow-vendor-C868rU-6.js +23 -0
  26. package/client/assets/icon-vendor-I2JVIi1s.js +501 -0
  27. package/client/assets/index-BMY4hrBP.js +3 -0
  28. package/client/assets/index-C-vnj9y3.js +1 -0
  29. package/client/assets/index-C9HTqfZw.css +1 -0
  30. package/client/assets/index-CWrv6O3o.js +64 -0
  31. package/client/assets/index-DMS12-Q2.js +8 -0
  32. package/client/assets/index-Dn7t_Y7G.js +1 -0
  33. package/client/assets/index-p-wk7iGH.css +1 -0
  34. package/client/assets/markdown-vendor-BSTcku12.css +10 -0
  35. package/client/assets/markdown-vendor-DnTJ9hmR.js +35 -0
  36. package/client/assets/participant-labels-Cf3qP3GB.js +1 -0
  37. package/client/assets/queries-Dm1jEHfc.js +1 -0
  38. package/client/assets/query-vendor-_taqgrbn.js +1 -0
  39. package/client/assets/react-vendor-DzpMUNDT.js +49 -0
  40. package/client/assets/settings-utils-l7KCS3Ev.js +1 -0
  41. package/client/assets/terminal-vendor-6GBZ9nXN.css +32 -0
  42. package/client/assets/terminal-vendor-D0xRnmbI.js +112 -0
  43. package/client/index.html +13 -3
  44. package/dist/cli.js +25 -3
  45. package/dist/server/app.js +72 -0
  46. package/dist/server/index.js +2 -62
  47. package/dist/server/lib/act-session-policy.js +31 -0
  48. package/dist/server/lib/chat-session.js +101 -0
  49. package/dist/server/lib/config.js +18 -4
  50. package/dist/server/lib/dot-authoring.js +171 -102
  51. package/dist/server/lib/dot-loader.js +9 -8
  52. package/dist/server/lib/dot-login.js +8 -190
  53. package/dist/server/lib/dot-source.js +11 -0
  54. package/dist/server/lib/model-catalog.js +74 -15
  55. package/dist/server/lib/opencode-auth.js +4 -1
  56. package/dist/server/lib/opencode-errors.js +70 -38
  57. package/dist/server/lib/opencode-sidecar.js +5 -2
  58. package/dist/server/lib/project-config.js +8 -0
  59. package/dist/server/lib/runtime-tools.js +46 -8
  60. package/dist/server/lib/safe-mode.js +410 -0
  61. package/dist/server/lib/session-execution.js +81 -0
  62. package/dist/server/lib/sse.js +22 -0
  63. package/dist/server/routes/act-runtime-threads.js +156 -0
  64. package/dist/server/routes/act-runtime-tools.js +157 -0
  65. package/dist/server/routes/act-runtime.js +7 -0
  66. package/dist/server/routes/adapter.js +32 -0
  67. package/dist/server/routes/assets-collection.js +16 -0
  68. package/dist/server/routes/assets-detail.js +38 -0
  69. package/dist/server/routes/assets.js +4 -158
  70. package/dist/server/routes/chat-messages.js +104 -0
  71. package/dist/server/routes/chat-sessions.js +104 -0
  72. package/dist/server/routes/chat-stream.js +15 -0
  73. package/dist/server/routes/chat.js +6 -353
  74. package/dist/server/routes/compile.js +5 -91
  75. package/dist/server/routes/dot-assets.js +77 -0
  76. package/dist/server/routes/dot-core.js +62 -0
  77. package/dist/server/routes/dot-performer.js +80 -0
  78. package/dist/server/routes/dot.js +6 -267
  79. package/dist/server/routes/drafts-collection.js +40 -0
  80. package/dist/server/routes/drafts-dance-bundle.js +113 -0
  81. package/dist/server/routes/drafts-item.js +86 -0
  82. package/dist/server/routes/drafts.js +9 -0
  83. package/dist/server/routes/health.js +18 -33
  84. package/dist/server/routes/opencode-core.js +120 -0
  85. package/dist/server/routes/opencode-file.js +67 -0
  86. package/dist/server/routes/opencode-mcp.js +74 -0
  87. package/dist/server/routes/opencode-provider.js +41 -0
  88. package/dist/server/routes/opencode.js +8 -418
  89. package/dist/server/routes/route-errors.js +10 -0
  90. package/dist/server/routes/safe-actions.js +60 -0
  91. package/dist/server/routes/safe-summary.js +20 -0
  92. package/dist/server/routes/safe.js +7 -0
  93. package/dist/server/routes/workspaces.js +47 -0
  94. package/dist/server/services/act-runtime/act-context-builder.js +81 -0
  95. package/dist/server/services/act-runtime/act-runtime-service.js +313 -0
  96. package/dist/server/services/act-runtime/act-runtime-utils.js +10 -0
  97. package/dist/server/services/act-runtime/act-tool-projection.js +26 -0
  98. package/dist/server/services/act-runtime/act-tools.js +151 -0
  99. package/dist/server/services/act-runtime/board-persistence.js +38 -0
  100. package/dist/server/services/act-runtime/event-logger.js +73 -0
  101. package/dist/server/services/act-runtime/event-router.js +102 -0
  102. package/dist/server/services/act-runtime/mailbox.js +149 -0
  103. package/dist/server/services/act-runtime/safety-guard.js +162 -0
  104. package/dist/server/services/act-runtime/session-queue.js +114 -0
  105. package/dist/server/services/act-runtime/thread-manager.js +351 -0
  106. package/dist/server/services/act-runtime/wake-cascade.js +306 -0
  107. package/dist/server/services/act-runtime/wake-evaluator.js +43 -0
  108. package/dist/server/services/act-runtime/wake-performer-resolver.js +68 -0
  109. package/dist/server/services/act-runtime/wake-prompt-builder.js +77 -0
  110. package/dist/server/services/adapter-view-service.js +6 -0
  111. package/dist/server/services/asset-service.js +366 -0
  112. package/dist/server/services/chat-event-stream-service.js +157 -0
  113. package/dist/server/services/chat-service.js +207 -0
  114. package/dist/server/services/chat-session-service.js +203 -0
  115. package/dist/server/services/compile-service.js +4 -0
  116. package/dist/server/services/dance-bundle-service.js +222 -0
  117. package/dist/server/services/dot-add-service.js +59 -0
  118. package/dist/server/services/dot-service.js +178 -0
  119. package/dist/server/services/draft-service.js +367 -0
  120. package/dist/server/services/opencode-projection/dance-compiler.js +164 -0
  121. package/dist/server/services/opencode-projection/performer-compiler.js +195 -0
  122. package/dist/server/services/opencode-projection/preview-service.js +31 -0
  123. package/dist/server/services/opencode-projection/projection-manifest.js +98 -0
  124. package/dist/server/services/opencode-projection/stage-projection-service.js +188 -0
  125. package/dist/server/services/opencode-service.js +338 -0
  126. package/dist/server/services/safe-service.js +33 -0
  127. package/dist/server/services/studio-assistant/assistant-service.js +172 -0
  128. package/dist/server/services/studio-service.js +69 -0
  129. package/dist/server/services/workspace-service.js +224 -0
  130. package/dist/server/terminal.js +57 -11
  131. package/dist/shared/act-types.js +4 -0
  132. package/dist/shared/adapter-view.js +1 -0
  133. package/dist/shared/asset-contracts.js +1 -0
  134. package/dist/shared/assistant-actions.js +1 -0
  135. package/dist/shared/chat-contracts.js +1 -0
  136. package/dist/shared/dot-contracts.js +1 -0
  137. package/dist/shared/dot-types.js +4 -0
  138. package/dist/shared/draft-contracts.js +2 -0
  139. package/dist/shared/model-types.js +2 -0
  140. package/dist/shared/performer-mcp-portability.js +10 -0
  141. package/dist/shared/safe-mode.js +1 -0
  142. package/dist/shared/session-metadata.js +4 -3
  143. package/package.json +7 -4
  144. package/client/assets/index-C2eIILoa.css +0 -41
  145. package/client/assets/index-DUPZ_Lw5.js +0 -616
  146. package/dist/server/lib/act-runtime.js +0 -1282
  147. package/dist/server/lib/prompt.js +0 -222
  148. package/dist/server/routes/stages.js +0 -137
@@ -0,0 +1,157 @@
1
+ import { getOpencode } from '../lib/opencode.js';
2
+ import { listSessionExecutionContextsForWorkingDir, resolveSessionExecutionContext, } from '../lib/session-execution.js';
3
+ import { sseEncode } from '../lib/sse.js';
4
+ const HEARTBEAT_INTERVAL_MS = 30_000;
5
+ const EXECUTION_DIRECTORY_REFRESH_MS = 1_000;
6
+ async function listEventDirectories(workingDir) {
7
+ const performerContexts = await listSessionExecutionContextsForWorkingDir(workingDir, 'performer');
8
+ const actContexts = await listSessionExecutionContextsForWorkingDir(workingDir, 'act');
9
+ return Array.from(new Set([
10
+ workingDir,
11
+ ...performerContexts.map((context) => context.executionDir),
12
+ ...actContexts.map((context) => context.executionDir),
13
+ ]));
14
+ }
15
+ export async function buildStudioChatEventStream(workingDir, abortSignal) {
16
+ const oc = await getOpencode();
17
+ return new ReadableStream({
18
+ async start(controller) {
19
+ let active = true;
20
+ const subscribedDirectories = new Set();
21
+ const connectingDirectories = new Set();
22
+ let heartbeatTimer = null;
23
+ let refreshTimer = null;
24
+ const close = () => {
25
+ if (!active) {
26
+ return;
27
+ }
28
+ active = false;
29
+ if (heartbeatTimer) {
30
+ clearInterval(heartbeatTimer);
31
+ heartbeatTimer = null;
32
+ }
33
+ if (refreshTimer) {
34
+ clearInterval(refreshTimer);
35
+ refreshTimer = null;
36
+ }
37
+ subscribedDirectories.clear();
38
+ connectingDirectories.clear();
39
+ abortSignal?.removeEventListener('abort', close);
40
+ try {
41
+ controller.close();
42
+ }
43
+ catch {
44
+ // Stream may already be closed.
45
+ }
46
+ };
47
+ const enqueueEvent = (event) => {
48
+ if (!active) {
49
+ return;
50
+ }
51
+ try {
52
+ controller.enqueue(sseEncode(JSON.stringify(event)));
53
+ }
54
+ catch {
55
+ close();
56
+ }
57
+ };
58
+ const subscribeDirectory = async (directory) => {
59
+ if (!active || subscribedDirectories.has(directory) || connectingDirectories.has(directory)) {
60
+ return;
61
+ }
62
+ connectingDirectories.add(directory);
63
+ try {
64
+ const subscription = abortSignal
65
+ ? await oc.event.subscribe({ directory }, { signal: abortSignal })
66
+ : await oc.event.subscribe({ directory });
67
+ if (!active) {
68
+ return;
69
+ }
70
+ connectingDirectories.delete(directory);
71
+ subscribedDirectories.add(directory);
72
+ void (async () => {
73
+ try {
74
+ for await (const event of subscription.stream) {
75
+ if (!active) {
76
+ return;
77
+ }
78
+ if (event.type === 'permission.asked') {
79
+ const sessionID = typeof event.properties?.sessionID === 'string'
80
+ ? event.properties.sessionID
81
+ : null;
82
+ const permissionID = typeof event.properties?.id === 'string'
83
+ ? event.properties.id
84
+ : null;
85
+ if (sessionID && permissionID) {
86
+ const context = await resolveSessionExecutionContext(sessionID);
87
+ if (context?.ownerKind === 'act') {
88
+ try {
89
+ await oc.permission.respond({
90
+ sessionID,
91
+ permissionID,
92
+ response: 'always',
93
+ directory: context.executionDir,
94
+ });
95
+ }
96
+ catch (error) {
97
+ console.error('Failed to auto-accept permission for Act session:', error);
98
+ }
99
+ continue;
100
+ }
101
+ }
102
+ }
103
+ if (event.type?.startsWith('message.') || event.type?.startsWith('session.') || event.type === 'permission.asked' || event.type === 'permission.replied' || event.type === 'question.asked' || event.type === 'question.replied' || event.type === 'question.rejected' || event.type === 'todo.updated') {
104
+ const rawProps = event.properties;
105
+ const sessionID = rawProps?.sessionID || rawProps?.info?.sessionID || rawProps?.part?.sessionID;
106
+ if (sessionID) {
107
+ const context = await resolveSessionExecutionContext(sessionID);
108
+ if (context) {
109
+ enqueueEvent({
110
+ ...event,
111
+ properties: {
112
+ ...(event.properties || {}),
113
+ ownerId: context.ownerId,
114
+ ownerKind: context.ownerKind,
115
+ },
116
+ });
117
+ continue;
118
+ }
119
+ }
120
+ }
121
+ enqueueEvent(event);
122
+ }
123
+ }
124
+ catch {
125
+ // Ignore broken subscription and keep stream alive.
126
+ }
127
+ finally {
128
+ subscribedDirectories.delete(directory);
129
+ connectingDirectories.delete(directory);
130
+ if (active) {
131
+ void subscribeDirectory(directory);
132
+ }
133
+ }
134
+ })();
135
+ }
136
+ catch {
137
+ connectingDirectories.delete(directory);
138
+ }
139
+ };
140
+ const refreshSubscriptions = async () => {
141
+ if (!active) {
142
+ return;
143
+ }
144
+ const directories = await listEventDirectories(workingDir);
145
+ await Promise.all(directories.map((directory) => subscribeDirectory(directory)));
146
+ };
147
+ abortSignal?.addEventListener('abort', close, { once: true });
148
+ heartbeatTimer = setInterval(() => {
149
+ enqueueEvent({ type: 'server.heartbeat' });
150
+ }, HEARTBEAT_INTERVAL_MS);
151
+ refreshTimer = setInterval(() => {
152
+ void refreshSubscriptions();
153
+ }, EXECUTION_DIRECTORY_REFRESH_MS);
154
+ await refreshSubscriptions();
155
+ },
156
+ });
157
+ }
@@ -0,0 +1,207 @@
1
+ import { getOpencode } from '../lib/opencode.js';
2
+ import { buildStudioSessionTitle } from '../../shared/session-metadata.js';
3
+ import { extractNonRetryableSessionError, waitForSessionToSettle } from '../lib/chat-session.js';
4
+ import { describeUnavailableRuntimeTools } from '../lib/runtime-tools.js';
5
+ import { StudioValidationError, unwrapOpencodeResult } from '../lib/opencode-errors.js';
6
+ import { getSafeOwnerExecutionDir } from '../lib/safe-mode.js';
7
+ import { registerSessionExecutionContext } from '../lib/session-execution.js';
8
+ import { resolveActSessionPolicy, ACT_AGENT_POSTURE } from '../lib/act-session-policy.js';
9
+ import { ensurePerformerProjection } from './opencode-projection/stage-projection-service.js';
10
+ import { projectActTools } from './act-runtime/act-tool-projection.js';
11
+ import { getActDefinitionForThread, getActRuntimeService } from './act-runtime/act-runtime-service.js';
12
+ export async function createStudioChatSession(cwd, request) {
13
+ const oc = await getOpencode();
14
+ const isAct = !!request.actId;
15
+ const actPolicy = isAct ? resolveActSessionPolicy(request.actId) : null;
16
+ const ownerKind = isAct ? actPolicy.ownerKind : 'performer';
17
+ // Use the full chatKey as both safe owner and session context owner so each
18
+ // Act participant session resolves back to the correct tab and execution scope.
19
+ const safeOwnerId = request.performerId;
20
+ const contextOwnerId = request.performerId;
21
+ // Act sessions are always direct — never inherit executionMode from request
22
+ const effectiveMode = isAct ? actPolicy.executionMode : (request.executionMode || 'direct');
23
+ const executionDir = await getSafeOwnerExecutionDir(cwd, ownerKind, safeOwnerId, effectiveMode);
24
+ const session = unwrapOpencodeResult(await oc.session.create({
25
+ directory: executionDir,
26
+ title: buildStudioSessionTitle(request.performerId, request.performerName, request.configHash, effectiveMode),
27
+ }));
28
+ await registerSessionExecutionContext({
29
+ sessionId: session.id,
30
+ ownerKind,
31
+ ownerId: contextOwnerId,
32
+ mode: effectiveMode,
33
+ workingDir: cwd,
34
+ executionDir,
35
+ });
36
+ // Persist participant→session mapping to thread.json for Act participants
37
+ // ChatKey format: `act:{actId}:thread:{threadId}:participant:{participantKey}`
38
+ if (request.actId && request.performerId.startsWith('act:')) {
39
+ const threadMatch = request.performerId.match(/^act:[^:]+:thread:([^:]+):participant:(.+)$/);
40
+ if (threadMatch) {
41
+ const [, threadId, participantKey] = threadMatch;
42
+ try {
43
+ const { getActRuntimeService } = await import('./act-runtime/act-runtime-service.js');
44
+ const service = getActRuntimeService(cwd);
45
+ await service.registerParticipantSession(threadId, participantKey, session.id);
46
+ }
47
+ catch {
48
+ // Non-fatal: session still works, just won't persist for reload
49
+ }
50
+ }
51
+ }
52
+ return {
53
+ sessionId: session.id,
54
+ title: session.title,
55
+ };
56
+ }
57
+ export async function sendStudioChatMessage(executionDir, workingDir, sessionId, request) {
58
+ const performer = request.performer;
59
+ if (!performer?.model) {
60
+ throw new StudioValidationError('Select a model for this performer before sending prompts.', 'select_model');
61
+ }
62
+ // Extract raw performer key for Act-namespaced chatKeys
63
+ // ChatKey format: `act:{actId}:thread:{threadId}:participant:{participantKey}`
64
+ // We need just the participantKey part for Act tool generation
65
+ let rawPerformerId = performer.performerId;
66
+ if (request.actId && performer.performerId.startsWith('act:')) {
67
+ const participantPrefix = ':participant:';
68
+ const participantIdx = performer.performerId.indexOf(participantPrefix);
69
+ if (participantIdx !== -1) {
70
+ rawPerformerId = performer.performerId.slice(participantIdx + participantPrefix.length);
71
+ }
72
+ }
73
+ // ── Collaboration tool projection ───────────────────
74
+ // When running in an Act thread, project stable collaboration context and
75
+ // collaboration tools into the participant session.
76
+ let actExtraTools = [];
77
+ let collaborationPromptSection = '';
78
+ if (request.actId && request.actThreadId) {
79
+ try {
80
+ const actDef = await getActDefinitionForThread(workingDir, request.actThreadId);
81
+ if (actDef) {
82
+ const projection = projectActTools(rawPerformerId, actDef, request.actThreadId, workingDir);
83
+ actExtraTools = projection.tools;
84
+ collaborationPromptSection = projection.contextPrompt;
85
+ }
86
+ }
87
+ catch (err) {
88
+ console.warn('[chat-service] Act tool projection failed:', err);
89
+ }
90
+ }
91
+ const isAssistant = rawPerformerId === 'studio-assistant';
92
+ let ensured = null;
93
+ let assistantContextPrefix = '';
94
+ let assistantAgentName = null;
95
+ let capabilitySnapshot = null;
96
+ let toolResolution = {
97
+ selectedMcpServers: [],
98
+ requestedTools: [],
99
+ availableTools: [],
100
+ resolvedTools: [],
101
+ unavailableTools: [],
102
+ unavailableDetails: [],
103
+ };
104
+ if (isAssistant) {
105
+ const { buildAssistantActionPrompt, ensureAssistantAgent } = await import('./studio-assistant/assistant-service.js');
106
+ assistantAgentName = await ensureAssistantAgent(executionDir);
107
+ assistantContextPrefix = buildAssistantActionPrompt(request.assistantContext || null);
108
+ }
109
+ else {
110
+ ensured = await ensurePerformerProjection({
111
+ performerId: rawPerformerId,
112
+ performerName: performer.performerName,
113
+ talRef: performer.talRef,
114
+ danceRefs: [...(performer.danceRefs || []), ...(performer.extraDanceRefs || [])],
115
+ model: performer.model,
116
+ modelVariant: performer.modelVariant || null,
117
+ mcpServerNames: performer.mcpServerNames || [],
118
+ executionDir,
119
+ workingDir,
120
+ ...(request.actId ? { scope: 'act', actId: request.actId } : {}),
121
+ ...(collaborationPromptSection ? { collaborationPromptSection } : {}),
122
+ // Pass Act runtime tools as extraTools so they're included in projection
123
+ ...(actExtraTools.length > 0 ? { extraTools: actExtraTools } : {}),
124
+ });
125
+ capabilitySnapshot = ensured.capabilitySnapshot;
126
+ toolResolution = ensured.toolResolution;
127
+ }
128
+ const unavailableSummary = describeUnavailableRuntimeTools(toolResolution);
129
+ if (toolResolution.selectedMcpServers.length > 0 && toolResolution.resolvedTools.length === 0 && unavailableSummary) {
130
+ throw new StudioValidationError(`Selected MCP servers are unavailable: ${unavailableSummary}.`, 'fix_input');
131
+ }
132
+ const promptSections = [assistantContextPrefix, request.message].filter(Boolean);
133
+ const parts = [{ type: 'text', text: promptSections.join('\n\n---\n\n') }];
134
+ if (request.attachments && request.attachments.length > 0) {
135
+ if (capabilitySnapshot && !capabilitySnapshot.attachment) {
136
+ throw new StudioValidationError('Selected model does not support attachments. Remove the files or choose a model that supports them.', 'choose_model');
137
+ }
138
+ for (const attachment of request.attachments) {
139
+ parts.push({
140
+ type: 'file',
141
+ mime: attachment.mime,
142
+ url: attachment.url,
143
+ filename: attachment.filename,
144
+ });
145
+ }
146
+ }
147
+ const oc = await getOpencode();
148
+ const actRuntime = request.actId && request.actThreadId
149
+ ? getActRuntimeService(workingDir)
150
+ : null;
151
+ if (actRuntime && rawPerformerId) {
152
+ await actRuntime.beginUserTurn(request.actThreadId);
153
+ await actRuntime.markParticipantSessionBusy(request.actThreadId, rawPerformerId);
154
+ }
155
+ try {
156
+ unwrapOpencodeResult(await oc.session.promptAsync({
157
+ sessionID: sessionId,
158
+ directory: executionDir,
159
+ agent: isAssistant
160
+ ? (assistantAgentName || undefined)
161
+ // Act scope always uses build agent, ignoring performer planMode
162
+ : (ensured?.compiled.agentNames[request.actId ? ACT_AGENT_POSTURE : (performer.planMode ? 'plan' : 'build')]),
163
+ // Pass model directly so OpenCode uses the user's selected model,
164
+ // not the (potentially stale) model cached from the agent file.
165
+ model: performer.model ? {
166
+ providerID: performer.model.provider,
167
+ modelID: performer.model.modelId,
168
+ } : undefined,
169
+ parts,
170
+ }));
171
+ }
172
+ catch (error) {
173
+ if (actRuntime && rawPerformerId) {
174
+ await actRuntime.drainParticipantQueue(request.actThreadId, rawPerformerId).catch(() => { });
175
+ }
176
+ throw error;
177
+ }
178
+ if (actRuntime && rawPerformerId) {
179
+ void waitForSessionToSettle(oc, sessionId, { directory: executionDir }, { timeoutMs: 30 * 60_000, pollMs: 250, requireObservedBusy: true }).then((settled) => {
180
+ if (!settled) {
181
+ void actRuntime.clearParticipantSessionBusy(request.actThreadId, rawPerformerId);
182
+ console.warn(`[chat-service] Session ${sessionId} for "${rawPerformerId}" did not settle before timeout`);
183
+ return;
184
+ }
185
+ return Promise.resolve(oc.session.messages({
186
+ sessionID: sessionId,
187
+ directory: executionDir,
188
+ })).then((response) => {
189
+ const rawMessages = unwrapOpencodeResult(response);
190
+ const messages = Array.isArray(rawMessages) ? rawMessages : [];
191
+ const fatalError = extractNonRetryableSessionError(messages);
192
+ if (fatalError) {
193
+ void actRuntime.clearParticipantSessionBusy(request.actThreadId, rawPerformerId);
194
+ void actRuntime.tripParticipantAutoWakeCircuit(request.actThreadId, rawPerformerId, fatalError);
195
+ console.warn(`[chat-service] Opened auto-wake circuit for "${rawPerformerId}": ${fatalError}`);
196
+ return;
197
+ }
198
+ void actRuntime.clearParticipantAutoWakeCircuit(request.actThreadId, rawPerformerId);
199
+ return actRuntime.drainParticipantQueue(request.actThreadId, rawPerformerId);
200
+ });
201
+ }).catch((error) => {
202
+ console.error(`[chat-service] Failed waiting for act session ${sessionId} to settle:`, error);
203
+ void actRuntime.clearParticipantSessionBusy(request.actThreadId, rawPerformerId);
204
+ });
205
+ }
206
+ return { accepted: true };
207
+ }
@@ -0,0 +1,203 @@
1
+ import { getOpencode } from '../lib/opencode.js';
2
+ import { listSessionExecutionContextsForWorkingDir, resolveSessionExecutionContext, unregisterSessionExecutionContext, } from '../lib/session-execution.js';
3
+ import { normalizeIncompleteToolParts, waitForSessionToSettle } from '../lib/chat-session.js';
4
+ import { unwrapOpencodeResult } from '../lib/opencode-errors.js';
5
+ import { responseData } from './opencode-service.js';
6
+ function readResponseHeader(result, name) {
7
+ if (!result || typeof result !== 'object') {
8
+ return null;
9
+ }
10
+ const response = result.response;
11
+ if (!response?.headers || typeof response.headers.get !== 'function') {
12
+ return null;
13
+ }
14
+ const value = response.headers.get(name);
15
+ if (!value) {
16
+ return null;
17
+ }
18
+ const trimmed = value.trim();
19
+ return trimmed.length > 0 ? trimmed : null;
20
+ }
21
+ export async function directoryQueryForSession(workingDir, sessionId) {
22
+ const context = await resolveSessionExecutionContext(sessionId);
23
+ return {
24
+ directory: context?.executionDir || workingDir,
25
+ };
26
+ }
27
+ export async function deleteStudioChatSession(workingDir, sessionId) {
28
+ const oc = await getOpencode();
29
+ const directoryQuery = await directoryQueryForSession(workingDir, sessionId);
30
+ unwrapOpencodeResult(await oc.session.delete({
31
+ sessionID: sessionId,
32
+ ...directoryQuery,
33
+ }));
34
+ await unregisterSessionExecutionContext(sessionId);
35
+ return { ok: true };
36
+ }
37
+ export async function renameStudioChatSession(workingDir, sessionId, title) {
38
+ const oc = await getOpencode();
39
+ const directoryQuery = await directoryQueryForSession(workingDir, sessionId);
40
+ return unwrapOpencodeResult(await oc.session.update({
41
+ sessionID: sessionId,
42
+ ...directoryQuery,
43
+ title,
44
+ }));
45
+ }
46
+ export async function abortStudioChatSession(workingDir, sessionId) {
47
+ const oc = await getOpencode();
48
+ const directoryQuery = await directoryQueryForSession(workingDir, sessionId);
49
+ unwrapOpencodeResult(await oc.session.abort({
50
+ sessionID: sessionId,
51
+ ...directoryQuery,
52
+ }));
53
+ await waitForSessionToSettle(oc, sessionId, directoryQuery).catch(() => { });
54
+ return { ok: true };
55
+ }
56
+ export async function respondSessionPermission(workingDir, sessionId, permissionId, response) {
57
+ const oc = await getOpencode();
58
+ const directoryQuery = await directoryQueryForSession(workingDir, sessionId);
59
+ unwrapOpencodeResult(await oc.permission.respond({
60
+ ...directoryQuery,
61
+ sessionID: sessionId,
62
+ permissionID: permissionId,
63
+ response,
64
+ }));
65
+ return { ok: true };
66
+ }
67
+ export async function respondQuestion(questionId, answers) {
68
+ const oc = await getOpencode();
69
+ unwrapOpencodeResult(await oc.question.reply({
70
+ requestID: questionId,
71
+ answers,
72
+ }));
73
+ return { ok: true };
74
+ }
75
+ export async function rejectQuestion(questionId) {
76
+ const oc = await getOpencode();
77
+ unwrapOpencodeResult(await oc.question.reject({
78
+ requestID: questionId,
79
+ }));
80
+ return { ok: true };
81
+ }
82
+ export async function listPendingPermissions(workingDir) {
83
+ const oc = await getOpencode();
84
+ const res = await oc.permission.list({ directory: workingDir });
85
+ return responseData(res, []);
86
+ }
87
+ export async function listPendingQuestions(workingDir) {
88
+ const oc = await getOpencode();
89
+ const res = await oc.question.list({ directory: workingDir });
90
+ return responseData(res, []);
91
+ }
92
+ export async function listStudioSessionMessages(workingDir, sessionId, options = {}) {
93
+ const oc = await getOpencode();
94
+ const directoryQuery = await directoryQueryForSession(workingDir, sessionId);
95
+ const params = {
96
+ sessionID: sessionId,
97
+ ...directoryQuery,
98
+ };
99
+ if (typeof options.limit === 'number' && Number.isFinite(options.limit) && options.limit > 0) {
100
+ params.limit = options.limit;
101
+ }
102
+ if (typeof options.before === 'string' && options.before.trim()) {
103
+ params.$query_before = options.before.trim();
104
+ }
105
+ const messageResult = await oc.session.messages(params);
106
+ const data = unwrapOpencodeResult(messageResult);
107
+ const statuses = unwrapOpencodeResult(await oc.session.status({
108
+ ...directoryQuery,
109
+ }));
110
+ const status = statuses?.[sessionId];
111
+ const messages = !status || status.type === 'idle'
112
+ ? normalizeIncompleteToolParts(data || [], Date.now())
113
+ : (data || []);
114
+ return {
115
+ messages,
116
+ nextCursor: readResponseHeader(messageResult, 'x-next-cursor'),
117
+ };
118
+ }
119
+ export async function listStudioSessionDiff(workingDir, sessionId) {
120
+ const oc = await getOpencode();
121
+ const directoryQuery = await directoryQueryForSession(workingDir, sessionId);
122
+ return unwrapOpencodeResult(await oc.session.diff({
123
+ sessionID: sessionId,
124
+ ...directoryQuery,
125
+ })) || [];
126
+ }
127
+ export async function shareStudioChatSession(workingDir, sessionId) {
128
+ const oc = await getOpencode();
129
+ const directoryQuery = await directoryQueryForSession(workingDir, sessionId);
130
+ return unwrapOpencodeResult(await oc.session.share({
131
+ sessionID: sessionId,
132
+ ...directoryQuery,
133
+ }));
134
+ }
135
+ export async function summarizeStudioChatSession(workingDir, sessionId, options) {
136
+ const oc = await getOpencode();
137
+ const directoryQuery = await directoryQueryForSession(workingDir, sessionId);
138
+ return unwrapOpencodeResult(await oc.session.summarize({
139
+ sessionID: sessionId,
140
+ ...directoryQuery,
141
+ ...(options.providerID && options.modelID ? { providerID: options.providerID, modelID: options.modelID } : {}),
142
+ ...(typeof options.auto === 'boolean' ? { auto: options.auto } : {}),
143
+ }));
144
+ }
145
+ export async function revertStudioChatSession(workingDir, sessionId, input) {
146
+ const oc = await getOpencode();
147
+ const directoryQuery = await directoryQueryForSession(workingDir, sessionId);
148
+ return unwrapOpencodeResult(await oc.session.revert({
149
+ sessionID: sessionId,
150
+ ...directoryQuery,
151
+ messageID: input.messageId,
152
+ ...(input.partId ? { partID: input.partId } : {}),
153
+ }));
154
+ }
155
+ export async function unrevertStudioChatSession(workingDir, sessionId) {
156
+ const oc = await getOpencode();
157
+ const directoryQuery = await directoryQueryForSession(workingDir, sessionId);
158
+ return unwrapOpencodeResult(await oc.session.unrevert({
159
+ sessionID: sessionId,
160
+ ...directoryQuery,
161
+ }));
162
+ }
163
+ export async function listStudioChatSessions(workingDir) {
164
+ const oc = await getOpencode();
165
+ const performerContexts = await listSessionExecutionContextsForWorkingDir(workingDir, 'performer');
166
+ const actContexts = await listSessionExecutionContextsForWorkingDir(workingDir, 'act');
167
+ const directories = Array.from(new Set([
168
+ workingDir,
169
+ ...performerContexts.map((context) => context.executionDir),
170
+ ...actContexts.map((context) => context.executionDir),
171
+ ]));
172
+ const directoryData = await Promise.all(directories.map(async (directory) => {
173
+ const [sessions, statuses] = await Promise.all([
174
+ unwrapOpencodeResult(await oc.session.list({ directory })),
175
+ unwrapOpencodeResult(await oc.session.status({ directory })),
176
+ ]);
177
+ return {
178
+ sessions: sessions || [],
179
+ statuses: statuses || {},
180
+ };
181
+ }));
182
+ const sessions = new Map();
183
+ for (const entry of directoryData) {
184
+ for (const session of entry.sessions) {
185
+ if (!session?.id)
186
+ continue;
187
+ const normalized = {
188
+ ...session,
189
+ createdAt: typeof session.createdAt === 'number' ? session.createdAt : session.time?.created,
190
+ updatedAt: typeof session.updatedAt === 'number' ? session.updatedAt : session.time?.updated,
191
+ parentId: typeof session.parentID === 'string' ? session.parentID : null,
192
+ status: entry.statuses[session.id]?.type,
193
+ };
194
+ const existing = sessions.get(session.id);
195
+ const existingUpdatedAt = existing?.updatedAt || 0;
196
+ const nextUpdatedAt = normalized.updatedAt || 0;
197
+ if (!existing || nextUpdatedAt >= existingUpdatedAt) {
198
+ sessions.set(session.id, normalized);
199
+ }
200
+ }
201
+ }
202
+ return Array.from(sessions.values());
203
+ }
@@ -0,0 +1,4 @@
1
+ import { compileProjectionPreview } from './opencode-projection/preview-service.js';
2
+ export async function compileStudioPromptPreview(workingDir, request) {
3
+ return compileProjectionPreview(workingDir, request);
4
+ }