/interview command flow:
outputFolder,index.ts exports createInterviewManager.
runtime.ts defines the interview-only session boundary (messages,
notify, continue, and rename). v1 uses nested SDK calls
(createV1InterviewSessionRuntime); v2 supplies a context-backed
implementation without expanding the global client shim.
manager.ts (composition root)
computeInterviewMode (also used by the
v2 interview bridge): interview.dashboard === true || interview.port > 0:createPerSessionInterviewServer (session-server.ts)createDashboardManager (dashboard-manager.ts)registerCommand, handleCommandExecuteBefore, handleEvent, dispose.session-server.ts
createInterviewServer({ port: 0 })
to a single createInterviewService, wiring command/event hooks and
cleanup.dashboard-manager.ts
tryBecomeDashboard(...) to elect one process as dashboard,
non-dashboard processes read auth token via readDashboardAuthFile(port),
registers/unregisters sessions over HTTP, and pushes state back via
/api/interviews/{id}/state; 10-second fallback polling keeps
answer/nudge delivery active if needed.createInterviewService (service.ts)
interviewsById, activeInterviewIds, sessionBusy, sessionModel.resolveExistingInterviewPath, createInterview, resumeInterview.findLatestAssistantState,buildFallbackState when needed,rewriteInterviewDocument.buildKickoffPrompt),buildResumePrompt),buildAnswerPrompt, handleNudgeAction).session.status updates busy tracking,session.deleted marks interview abandoned and drains maps.onStateChange callback for dashboard mode,onInterviewCreated callback for immediate registration,openBrowser for initial UI open.createInterviewServer (server.ts)
ui.ts.createInterviewServerDeps(service, outputFolder, port) builds the
service-delegating deps object shared by the per-session server, the
dashboard fallback server, and the v2 interview bridge.GET /, GET /api/interviews, GET /interview/{id}GET /api/interviews/{id}/statePOST /api/interviews/{id}/answersPOST /api/interviews/{id}/nudgegetSubmissionStatus.ui.ts
escapeHtml from src/utils/escape-html.ts.dashboard.ts
${XDG_DATA_HOME}/opencode/.dashboard-<port>.json,sessions registry,stateCache keyed by interview ID,rebuildFromFiles() from markdown frontmatter,Supporting modules:
document.ts: markdown/file helpers (slugify, path resolution, frontmatter,
title/summary extraction).parser.ts: assistant state parse pipeline (parseAssistantState,
findLatestAssistantState, buildFallbackState).prompts.ts: prompt templates for create/resume/answer/nudge.helpers.ts: request parsing and HTML/JSON response helpers.types.ts: domain schemas and interview contracts.src/index.ts wires this folder through
createInterviewManager(ctx, config).
Per-session mode
createInterviewServer({ port: 0 }),Dashboard mode
createInterviewManager invokes tryBecomeDashboard.setStatePushCallback,
setOnInterviewCreated),/pending and /nudge on idle.handleCommandExecuteBefore
handleEvent
session.status: idle:sessionBusy is reflected accurately.session.deleted:src/index.ts as the interview plugin module.src/interview/*.test.ts.runtime.ts boundary owns message reads, notifications,
orchestrator continuation, and session rename operations. The v2 bridge uses
this boundary instead of expanding the global client shim.confirm-complete, the next clean assistant response is persisted with
rewriteInterviewDocumentWithFinalSpec only after an idle/text-ended
completion event; a stream prefix cannot overwrite the final markdown./api/register, unregister at
/api/unregister, and receive 202 {"status":"queued"} for deferred
browser submissions. Session polling claims each queued value and must
acknowledge it after service delivery; rejected deliveries roll the claim
back without clearing the answer, chat message, block comment, or nudge.sessionID in frontmatter. A
different session cannot resume or mutate an owned document, while the
original session can resume it across plugin processes.