oh-my-opencode-slim 2.0.3 → 2.0.5
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.
- package/README.ja-JP.md +14 -0
- package/README.ko-KR.md +14 -0
- package/README.md +32 -1
- package/README.zh-CN.md +14 -0
- package/dist/agents/permissions.d.ts +10 -0
- package/dist/cli/index.js +27 -10
- package/dist/cli/skills.d.ts +1 -1
- package/dist/companion/manager.d.ts +1 -0
- package/dist/config/constants.d.ts +3 -2
- package/dist/config/schema.d.ts +3 -0
- package/dist/index.js +1430 -260
- package/dist/interview/dashboard.d.ts +5 -0
- package/dist/interview/document.d.ts +4 -1
- package/dist/interview/server.d.ts +2 -0
- package/dist/interview/service.d.ts +2 -0
- package/dist/interview/types.d.ts +13 -0
- package/dist/multiplexer/session-manager.d.ts +2 -0
- package/dist/tools/acp-run.d.ts +1 -1
- package/dist/tui-state.d.ts +3 -0
- package/dist/tui.d.ts +4 -1
- package/dist/tui.js +66 -22
- package/dist/utils/background-job-board.d.ts +4 -0
- package/dist/utils/env.d.ts +3 -0
- package/oh-my-opencode-slim.schema.json +18 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -18420,7 +18420,8 @@ var PERMISSION_ONLY_SKILLS = [
|
|
|
18420
18420
|
description: "Code review template for reviewer subagents in multi-step workflows"
|
|
18421
18421
|
}
|
|
18422
18422
|
];
|
|
18423
|
-
function getSkillPermissionsForAgent(agentName, skillList) {
|
|
18423
|
+
function getSkillPermissionsForAgent(agentName, skillList, disabledSkillNames) {
|
|
18424
|
+
const disabledSkills = new Set(disabledSkillNames ?? []);
|
|
18424
18425
|
const permissions = {
|
|
18425
18426
|
"*": agentName === "orchestrator" ? "allow" : "deny"
|
|
18426
18427
|
};
|
|
@@ -18431,24 +18432,30 @@ function getSkillPermissionsForAgent(agentName, skillList) {
|
|
|
18431
18432
|
permissions["*"] = "allow";
|
|
18432
18433
|
} else if (name.startsWith("!")) {
|
|
18433
18434
|
permissions[name.slice(1)] = "deny";
|
|
18434
|
-
} else {
|
|
18435
|
+
} else if (!disabledSkills.has(name)) {
|
|
18435
18436
|
permissions[name] = "allow";
|
|
18436
18437
|
}
|
|
18437
18438
|
}
|
|
18439
|
+
for (const name of disabledSkills) {
|
|
18440
|
+
permissions[name] = "deny";
|
|
18441
|
+
}
|
|
18438
18442
|
return permissions;
|
|
18439
18443
|
}
|
|
18440
18444
|
for (const skill of CUSTOM_SKILLS) {
|
|
18441
18445
|
const isAllowed = skill.allowedAgents.includes("*") || skill.allowedAgents.includes(agentName);
|
|
18442
|
-
if (isAllowed) {
|
|
18446
|
+
if (isAllowed && !disabledSkills.has(skill.name)) {
|
|
18443
18447
|
permissions[skill.name] = "allow";
|
|
18444
18448
|
}
|
|
18445
18449
|
}
|
|
18446
18450
|
for (const skill of PERMISSION_ONLY_SKILLS) {
|
|
18447
18451
|
const isAllowed = skill.allowedAgents.includes("*") || skill.allowedAgents.includes(agentName);
|
|
18448
|
-
if (isAllowed) {
|
|
18452
|
+
if (isAllowed && !disabledSkills.has(skill.name)) {
|
|
18449
18453
|
permissions[skill.name] = "allow";
|
|
18450
18454
|
}
|
|
18451
18455
|
}
|
|
18456
|
+
for (const name of disabledSkills) {
|
|
18457
|
+
permissions[name] = "deny";
|
|
18458
|
+
}
|
|
18452
18459
|
return permissions;
|
|
18453
18460
|
}
|
|
18454
18461
|
|
|
@@ -18471,14 +18478,14 @@ var ALL_AGENT_NAMES = ["orchestrator", ...SUBAGENT_NAMES];
|
|
|
18471
18478
|
var PROTECTED_AGENTS = new Set(["orchestrator", "councillor"]);
|
|
18472
18479
|
var DEFAULT_MODELS = {
|
|
18473
18480
|
orchestrator: undefined,
|
|
18474
|
-
oracle:
|
|
18475
|
-
librarian:
|
|
18476
|
-
explorer:
|
|
18477
|
-
designer:
|
|
18478
|
-
fixer:
|
|
18479
|
-
observer:
|
|
18480
|
-
council:
|
|
18481
|
-
councillor:
|
|
18481
|
+
oracle: undefined,
|
|
18482
|
+
librarian: undefined,
|
|
18483
|
+
explorer: undefined,
|
|
18484
|
+
designer: undefined,
|
|
18485
|
+
fixer: undefined,
|
|
18486
|
+
observer: undefined,
|
|
18487
|
+
council: undefined,
|
|
18488
|
+
councillor: undefined
|
|
18482
18489
|
};
|
|
18483
18490
|
var POLL_INTERVAL_BACKGROUND_MS = 2000;
|
|
18484
18491
|
var MAX_POLL_TIME_MS = 5 * 60 * 1000;
|
|
@@ -18719,6 +18726,7 @@ var CompanionConfigSchema = z2.object({
|
|
|
18719
18726
|
debug: z2.boolean().optional().describe("Enable verbose native companion debug logs.")
|
|
18720
18727
|
});
|
|
18721
18728
|
var AcpAgentPermissionModeSchema = z2.enum(["ask", "allow", "reject"]);
|
|
18729
|
+
var MAX_ACP_TIMEOUT_MS = 2147483647;
|
|
18722
18730
|
var AcpAgentConfigSchema = z2.object({
|
|
18723
18731
|
command: z2.string().min(1),
|
|
18724
18732
|
args: z2.array(z2.string()).default([]),
|
|
@@ -18728,7 +18736,7 @@ var AcpAgentConfigSchema = z2.object({
|
|
|
18728
18736
|
prompt: z2.string().min(1).optional(),
|
|
18729
18737
|
orchestratorPrompt: z2.string().min(1).optional(),
|
|
18730
18738
|
wrapperModel: ProviderModelIdSchema.optional(),
|
|
18731
|
-
timeoutMs: z2.number().int().min(
|
|
18739
|
+
timeoutMs: z2.number().int().min(0).max(MAX_ACP_TIMEOUT_MS).default(0).describe("Timeout for a single ACP run in milliseconds. Set to 0 to disable the timeout."),
|
|
18732
18740
|
permissionMode: AcpAgentPermissionModeSchema.default("ask")
|
|
18733
18741
|
}).strict();
|
|
18734
18742
|
var AcpAgentsConfigSchema = z2.record(z2.string(), AcpAgentConfigSchema);
|
|
@@ -18762,6 +18770,8 @@ var PluginConfigSchema = z2.object({
|
|
|
18762
18770
|
agents: z2.record(z2.string(), AgentOverrideConfigSchema).optional(),
|
|
18763
18771
|
disabled_agents: z2.array(z2.string()).optional().describe("Agent names to disable completely. " + "Disabled agents are not instantiated and cannot be delegated to. " + "Orchestrator and council internal agents (councillor) cannot be disabled. " + "By default, 'observer' is disabled. Remove it from this list and configure a vision-capable model to enable."),
|
|
18764
18772
|
disabled_mcps: z2.array(z2.string()).optional(),
|
|
18773
|
+
disabled_tools: z2.array(z2.string()).optional().describe("Tool names to disable completely. Disabled tools are not registered with OpenCode and cannot be used by agents."),
|
|
18774
|
+
disabled_skills: z2.array(z2.string()).optional().describe("Skill names to disable completely. Disabled skills are not granted to agents, even when referenced by presets or agent overrides."),
|
|
18765
18775
|
multiplexer: MultiplexerConfigSchema.optional(),
|
|
18766
18776
|
tmux: TmuxConfigSchema.optional(),
|
|
18767
18777
|
websearch: WebsearchConfigSchema.optional(),
|
|
@@ -19251,6 +19261,11 @@ Build a short work graph before dispatching:
|
|
|
19251
19261
|
- Advisory ownership for write-capable lanes
|
|
19252
19262
|
- Verification/review lanes that run after implementation
|
|
19253
19263
|
|
|
19264
|
+
### Todo Continuity
|
|
19265
|
+
- When the user adds a new task while a todo list exists, append the new task to the end of the existing todo list instead of replacing the list.
|
|
19266
|
+
- Preserve existing todo order, statuses, and priorities unless the user explicitly asks to reprioritize, cancel, or replace them.
|
|
19267
|
+
- Finish the current in-progress task before starting the newly appended task unless the current task is blocked or the user explicitly overrides the order.
|
|
19268
|
+
|
|
19254
19269
|
Can tasks be split into background specialist work?
|
|
19255
19270
|
${enabledParallelExamples}
|
|
19256
19271
|
|
|
@@ -19258,6 +19273,7 @@ Balance: respect dependencies, avoid parallelizing what must be sequential, and
|
|
|
19258
19273
|
|
|
19259
19274
|
### Background Task Discipline
|
|
19260
19275
|
- Prefer \`task(..., background: true)\` for delegated work that can run independently.
|
|
19276
|
+
- Launch specialist agents in the background by default so the orchestrator stays unblocked and can reconcile results when they return.
|
|
19261
19277
|
- Track each task's specialist, objective, task/session ID, and file/topic ownership.
|
|
19262
19278
|
- Continue orchestration only on non-overlapping work; otherwise briefly report what was launched and stop.
|
|
19263
19279
|
- Before local edits or another writer task, compare against running task scopes.
|
|
@@ -19346,10 +19362,31 @@ function createOrchestratorAgent(model, customPrompt, customAppendPrompt, disabl
|
|
|
19346
19362
|
return definition;
|
|
19347
19363
|
}
|
|
19348
19364
|
|
|
19365
|
+
// src/agents/permissions.ts
|
|
19366
|
+
function createReadOnlyAgentPermission() {
|
|
19367
|
+
return {
|
|
19368
|
+
"*": "deny",
|
|
19369
|
+
bash: "deny",
|
|
19370
|
+
edit: "deny",
|
|
19371
|
+
write: "deny",
|
|
19372
|
+
apply_patch: "deny",
|
|
19373
|
+
ast_grep_replace: "deny",
|
|
19374
|
+
task: "deny",
|
|
19375
|
+
question: "deny",
|
|
19376
|
+
read: "allow",
|
|
19377
|
+
glob: "allow",
|
|
19378
|
+
grep: "allow",
|
|
19379
|
+
lsp: "allow",
|
|
19380
|
+
list: "allow",
|
|
19381
|
+
codesearch: "allow",
|
|
19382
|
+
ast_grep_search: "allow"
|
|
19383
|
+
};
|
|
19384
|
+
}
|
|
19385
|
+
|
|
19349
19386
|
// src/agents/council.ts
|
|
19350
19387
|
var COUNCIL_AGENT_PROMPT = `You are the Council agent — a multi-LLM orchestration system that runs consensus across multiple models.
|
|
19351
19388
|
|
|
19352
|
-
**Tool**: You have access to the \`council_session\` tool.
|
|
19389
|
+
**Tool**: You have access to the \`council_session\` tool. You also have read-only codebase inspection tools. You do not have write, edit, shell, or subagent-delegation tools.
|
|
19353
19390
|
|
|
19354
19391
|
**When to use**:
|
|
19355
19392
|
- When invoked by a user with a request
|
|
@@ -19410,7 +19447,11 @@ function createCouncilAgent(model, customPrompt, customAppendPrompt) {
|
|
|
19410
19447
|
description: "Multi-LLM council agent that synthesizes responses from multiple models for higher-quality outputs",
|
|
19411
19448
|
config: {
|
|
19412
19449
|
temperature: 0.1,
|
|
19413
|
-
prompt
|
|
19450
|
+
prompt,
|
|
19451
|
+
permission: {
|
|
19452
|
+
...createReadOnlyAgentPermission(),
|
|
19453
|
+
council_session: "allow"
|
|
19454
|
+
}
|
|
19414
19455
|
}
|
|
19415
19456
|
};
|
|
19416
19457
|
if (model) {
|
|
@@ -19519,17 +19560,7 @@ function createCouncillorAgent(model, customPrompt, customAppendPrompt) {
|
|
|
19519
19560
|
model,
|
|
19520
19561
|
temperature: 0.2,
|
|
19521
19562
|
prompt,
|
|
19522
|
-
permission:
|
|
19523
|
-
"*": "deny",
|
|
19524
|
-
question: "deny",
|
|
19525
|
-
read: "allow",
|
|
19526
|
-
glob: "allow",
|
|
19527
|
-
grep: "allow",
|
|
19528
|
-
lsp: "allow",
|
|
19529
|
-
list: "allow",
|
|
19530
|
-
codesearch: "allow",
|
|
19531
|
-
ast_grep_search: "allow"
|
|
19532
|
-
}
|
|
19563
|
+
permission: createReadOnlyAgentPermission()
|
|
19533
19564
|
}
|
|
19534
19565
|
};
|
|
19535
19566
|
}
|
|
@@ -19869,7 +19900,38 @@ function normalizeDisplayName(displayName) {
|
|
|
19869
19900
|
const trimmed = displayName.trim();
|
|
19870
19901
|
return trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
|
|
19871
19902
|
}
|
|
19872
|
-
function
|
|
19903
|
+
function getPrimaryModelFromOverride(override) {
|
|
19904
|
+
const model = override?.model;
|
|
19905
|
+
if (typeof model === "string") {
|
|
19906
|
+
return model;
|
|
19907
|
+
}
|
|
19908
|
+
if (Array.isArray(model) && model.length > 0) {
|
|
19909
|
+
const first = model[0];
|
|
19910
|
+
return typeof first === "string" ? first : first?.id;
|
|
19911
|
+
}
|
|
19912
|
+
return;
|
|
19913
|
+
}
|
|
19914
|
+
function getActivePresetPrimaryModel(config) {
|
|
19915
|
+
const activePreset = config?.preset ? config.presets?.[config.preset] : undefined;
|
|
19916
|
+
if (!activePreset) {
|
|
19917
|
+
return;
|
|
19918
|
+
}
|
|
19919
|
+
const orchestratorModel = getPrimaryModelFromOverride(activePreset.orchestrator);
|
|
19920
|
+
if (orchestratorModel) {
|
|
19921
|
+
return orchestratorModel;
|
|
19922
|
+
}
|
|
19923
|
+
for (const name of SUBAGENT_NAMES) {
|
|
19924
|
+
const model = getPrimaryModelFromOverride(activePreset[name]);
|
|
19925
|
+
if (model) {
|
|
19926
|
+
return model;
|
|
19927
|
+
}
|
|
19928
|
+
}
|
|
19929
|
+
return;
|
|
19930
|
+
}
|
|
19931
|
+
function getConfigPrimaryModel(config) {
|
|
19932
|
+
return getActivePresetPrimaryModel(config);
|
|
19933
|
+
}
|
|
19934
|
+
function buildAcpAgentDefinition(name, config, fallbackModel) {
|
|
19873
19935
|
const description = config.description ?? `External ACP agent '${name}' via ${config.command}`;
|
|
19874
19936
|
const prompt = config.prompt ?? [
|
|
19875
19937
|
`You are the ${name} ACP wrapper agent.`,
|
|
@@ -19883,7 +19945,7 @@ function buildAcpAgentDefinition(name, config) {
|
|
|
19883
19945
|
name,
|
|
19884
19946
|
description,
|
|
19885
19947
|
config: {
|
|
19886
|
-
model: config.wrapperModel ??
|
|
19948
|
+
model: config.wrapperModel ?? fallbackModel ?? DEFAULT_MODELS.oracle,
|
|
19887
19949
|
temperature: 0,
|
|
19888
19950
|
prompt,
|
|
19889
19951
|
permission: {
|
|
@@ -19948,10 +20010,11 @@ function hasCustomAgentModel(override) {
|
|
|
19948
20010
|
}
|
|
19949
20011
|
function buildCustomAgentDefinition(name, override, filePrompt, fileAppendPrompt) {
|
|
19950
20012
|
const basePrompt = override.prompt ?? `You are the ${name} specialist.`;
|
|
20013
|
+
const primaryModel = getPrimaryModelFromOverride(override);
|
|
19951
20014
|
return {
|
|
19952
20015
|
name,
|
|
19953
20016
|
config: {
|
|
19954
|
-
model:
|
|
20017
|
+
model: primaryModel ?? DEFAULT_MODELS.oracle,
|
|
19955
20018
|
temperature: 0.2,
|
|
19956
20019
|
prompt: resolvePrompt(basePrompt, filePrompt, fileAppendPrompt)
|
|
19957
20020
|
}
|
|
@@ -19968,9 +20031,9 @@ function injectDisplayNames(orchestrator, nameMap) {
|
|
|
19968
20031
|
}
|
|
19969
20032
|
orchestrator.config.prompt = prompt;
|
|
19970
20033
|
}
|
|
19971
|
-
function applyDefaultPermissions(agent, configuredSkills) {
|
|
20034
|
+
function applyDefaultPermissions(agent, configuredSkills, disabledSkills) {
|
|
19972
20035
|
const existing = agent.config.permission ?? {};
|
|
19973
|
-
const skillPermissions = getSkillPermissionsForAgent(agent.name, configuredSkills);
|
|
20036
|
+
const skillPermissions = getSkillPermissionsForAgent(agent.name, configuredSkills, disabledSkills);
|
|
19974
20037
|
const questionPerm = existing.question === "deny" ? "deny" : "allow";
|
|
19975
20038
|
const councilSessionPerm = COUNCIL_TOOL_ALLOWED_AGENTS.has(agent.name) ? existing.council_session ?? "allow" : "deny";
|
|
19976
20039
|
const cancelTaskPerm = CANCEL_TASK_ALLOWED_AGENTS.has(agent.name) ? existing.cancel_task ?? "allow" : "deny";
|
|
@@ -20003,6 +20066,7 @@ function createAgents(config) {
|
|
|
20003
20066
|
if (!config?.council) {
|
|
20004
20067
|
disabled.add("council");
|
|
20005
20068
|
}
|
|
20069
|
+
const primaryModel = getConfigPrimaryModel(config);
|
|
20006
20070
|
const getModelForAgent = (name) => {
|
|
20007
20071
|
if (name === "fixer" && !getAgentOverride(config, "fixer")?.model) {
|
|
20008
20072
|
const librarianOverride = getAgentOverride(config, "librarian")?.model;
|
|
@@ -20013,9 +20077,9 @@ function createAgents(config) {
|
|
|
20013
20077
|
} else {
|
|
20014
20078
|
librarianModel = librarianOverride;
|
|
20015
20079
|
}
|
|
20016
|
-
return librarianModel ?? DEFAULT_MODELS.librarian;
|
|
20080
|
+
return librarianModel ?? primaryModel ?? DEFAULT_MODELS.librarian;
|
|
20017
20081
|
}
|
|
20018
|
-
return DEFAULT_MODELS[name];
|
|
20082
|
+
return primaryModel ?? DEFAULT_MODELS[name];
|
|
20019
20083
|
};
|
|
20020
20084
|
const protoSubAgents = Object.entries(SUBAGENT_FACTORIES).filter(([name]) => !disabled.has(name)).map(([name, factory]) => {
|
|
20021
20085
|
const customPrompts = loadAgentPrompt(name, config?.preset);
|
|
@@ -20057,14 +20121,14 @@ function createAgents(config) {
|
|
|
20057
20121
|
const acp = config?.acpAgents?.[name];
|
|
20058
20122
|
if (!acp)
|
|
20059
20123
|
throw new Error(`ACP agent '${name}' is missing config`);
|
|
20060
|
-
return buildAcpAgentDefinition(name, acp);
|
|
20124
|
+
return buildAcpAgentDefinition(name, acp, primaryModel);
|
|
20061
20125
|
});
|
|
20062
20126
|
const builtInSubAgents = protoSubAgents.map((agent) => {
|
|
20063
20127
|
const override = getAgentOverride(config, agent.name);
|
|
20064
20128
|
if (override) {
|
|
20065
20129
|
applyOverrides(agent, override);
|
|
20066
20130
|
}
|
|
20067
|
-
applyDefaultPermissions(agent, override?.skills);
|
|
20131
|
+
applyDefaultPermissions(agent, override?.skills, config?.disabled_skills);
|
|
20068
20132
|
return agent;
|
|
20069
20133
|
});
|
|
20070
20134
|
const legacyMasterModel = config?.council?._legacyMasterModel;
|
|
@@ -20079,11 +20143,11 @@ function createAgents(config) {
|
|
|
20079
20143
|
if (override) {
|
|
20080
20144
|
applyOverrides(agent, override);
|
|
20081
20145
|
}
|
|
20082
|
-
applyDefaultPermissions(agent, override?.skills);
|
|
20146
|
+
applyDefaultPermissions(agent, override?.skills, config?.disabled_skills);
|
|
20083
20147
|
return agent;
|
|
20084
20148
|
});
|
|
20085
20149
|
const acpSubAgents = protoAcpAgents.map((agent) => {
|
|
20086
|
-
applyDefaultPermissions(agent);
|
|
20150
|
+
applyDefaultPermissions(agent, undefined, config?.disabled_skills);
|
|
20087
20151
|
return agent;
|
|
20088
20152
|
});
|
|
20089
20153
|
const allSubAgents = [
|
|
@@ -20095,7 +20159,7 @@ function createAgents(config) {
|
|
|
20095
20159
|
const orchestratorModel = orchestratorOverride?.model ?? DEFAULT_MODELS.orchestrator;
|
|
20096
20160
|
const orchestratorPrompts = loadAgentPrompt("orchestrator", config?.preset);
|
|
20097
20161
|
const orchestrator = createOrchestratorAgent(orchestratorModel, orchestratorPrompts.prompt, orchestratorPrompts.appendPrompt, disabled);
|
|
20098
|
-
applyDefaultPermissions(orchestrator, orchestratorOverride?.skills);
|
|
20162
|
+
applyDefaultPermissions(orchestrator, orchestratorOverride?.skills, config?.disabled_skills);
|
|
20099
20163
|
if (orchestratorOverride) {
|
|
20100
20164
|
applyOverrides(orchestrator, orchestratorOverride);
|
|
20101
20165
|
}
|
|
@@ -20374,6 +20438,7 @@ class CompanionManager {
|
|
|
20374
20438
|
status = "idle";
|
|
20375
20439
|
busyAgentSessions = new Map;
|
|
20376
20440
|
config;
|
|
20441
|
+
companionProcess = null;
|
|
20377
20442
|
constructor(sessionId, cwd, config) {
|
|
20378
20443
|
this.id = sessionId;
|
|
20379
20444
|
this.cwd = cwd;
|
|
@@ -20438,6 +20503,12 @@ class CompanionManager {
|
|
|
20438
20503
|
onExit() {
|
|
20439
20504
|
if (this.config?.enabled !== true)
|
|
20440
20505
|
return;
|
|
20506
|
+
if (this.companionProcess) {
|
|
20507
|
+
try {
|
|
20508
|
+
this.companionProcess.kill();
|
|
20509
|
+
} catch {}
|
|
20510
|
+
this.companionProcess = null;
|
|
20511
|
+
}
|
|
20441
20512
|
writeState((state) => {
|
|
20442
20513
|
state.sessions = state.sessions.filter((s) => s.session_id !== this.id);
|
|
20443
20514
|
});
|
|
@@ -20514,6 +20585,7 @@ class CompanionManager {
|
|
|
20514
20585
|
},
|
|
20515
20586
|
stdio: "ignore"
|
|
20516
20587
|
});
|
|
20588
|
+
this.companionProcess = child;
|
|
20517
20589
|
child.unref();
|
|
20518
20590
|
log("[companion] spawned", JSON.stringify({
|
|
20519
20591
|
bin,
|
|
@@ -21010,7 +21082,15 @@ class CouncilManager {
|
|
|
21010
21082
|
const body = {
|
|
21011
21083
|
agent: options.agent,
|
|
21012
21084
|
model: modelRef,
|
|
21013
|
-
tools: {
|
|
21085
|
+
tools: {
|
|
21086
|
+
task: false,
|
|
21087
|
+
question: false,
|
|
21088
|
+
edit: false,
|
|
21089
|
+
write: false,
|
|
21090
|
+
apply_patch: false,
|
|
21091
|
+
ast_grep_replace: false,
|
|
21092
|
+
bash: false
|
|
21093
|
+
},
|
|
21014
21094
|
parts: [{ type: "text", text: options.promptText }]
|
|
21015
21095
|
};
|
|
21016
21096
|
if (options.variant) {
|
|
@@ -23537,6 +23617,7 @@ var AGENT_PREFIX = {
|
|
|
23537
23617
|
class BackgroundJobBoard {
|
|
23538
23618
|
jobs = new Map;
|
|
23539
23619
|
counters = new Map;
|
|
23620
|
+
terminalStateListener;
|
|
23540
23621
|
maxReusablePerAgent;
|
|
23541
23622
|
readContextMinLines;
|
|
23542
23623
|
readContextMaxFiles;
|
|
@@ -23545,6 +23626,9 @@ class BackgroundJobBoard {
|
|
|
23545
23626
|
this.readContextMinLines = options.readContextMinLines ?? 10;
|
|
23546
23627
|
this.readContextMaxFiles = options.readContextMaxFiles ?? 8;
|
|
23547
23628
|
}
|
|
23629
|
+
setTerminalStateListener(listener) {
|
|
23630
|
+
this.terminalStateListener = listener;
|
|
23631
|
+
}
|
|
23548
23632
|
registerLaunch(input) {
|
|
23549
23633
|
const now = input.now ?? Date.now();
|
|
23550
23634
|
const existing = this.jobs.get(input.taskID);
|
|
@@ -23602,6 +23686,7 @@ class BackgroundJobBoard {
|
|
|
23602
23686
|
}
|
|
23603
23687
|
const now = input.now ?? Date.now();
|
|
23604
23688
|
const terminal = TERMINAL_STATES.has(input.state);
|
|
23689
|
+
const notifyTerminal = terminal && !TERMINAL_STATES.has(existing.state);
|
|
23605
23690
|
const updated = {
|
|
23606
23691
|
...existing,
|
|
23607
23692
|
state: input.state,
|
|
@@ -23616,6 +23701,8 @@ class BackgroundJobBoard {
|
|
|
23616
23701
|
};
|
|
23617
23702
|
this.jobs.set(input.taskID, updated);
|
|
23618
23703
|
this.trimReusable(input.taskID);
|
|
23704
|
+
if (notifyTerminal)
|
|
23705
|
+
this.terminalStateListener?.(input.taskID);
|
|
23619
23706
|
return updated;
|
|
23620
23707
|
}
|
|
23621
23708
|
updateFromStatusOutput(output) {
|
|
@@ -23680,6 +23767,7 @@ class BackgroundJobBoard {
|
|
|
23680
23767
|
if (TERMINAL_STATES.has(existing.state))
|
|
23681
23768
|
return existing;
|
|
23682
23769
|
}
|
|
23770
|
+
const notifyTerminal = !TERMINAL_STATES.has(existing.state) && existing.state !== "reconciled";
|
|
23683
23771
|
const summary = normalizeCancelReason(reason);
|
|
23684
23772
|
const updated = {
|
|
23685
23773
|
...existing,
|
|
@@ -23695,6 +23783,8 @@ class BackgroundJobBoard {
|
|
|
23695
23783
|
lastStatusError: undefined
|
|
23696
23784
|
};
|
|
23697
23785
|
this.jobs.set(taskID, updated);
|
|
23786
|
+
if (notifyTerminal)
|
|
23787
|
+
this.terminalStateListener?.(taskID);
|
|
23698
23788
|
return updated;
|
|
23699
23789
|
}
|
|
23700
23790
|
get(taskID) {
|
|
@@ -24147,7 +24237,7 @@ function createFilterAvailableSkillsHook(_ctx, config) {
|
|
|
24147
24237
|
return cached;
|
|
24148
24238
|
}
|
|
24149
24239
|
const configuredSkills = getAgentOverride(config, agentName)?.skills;
|
|
24150
|
-
const permissionRules = getSkillPermissionsForAgent(agentName, configuredSkills);
|
|
24240
|
+
const permissionRules = getSkillPermissionsForAgent(agentName, configuredSkills, config.disabled_skills);
|
|
24151
24241
|
permissionRulesByAgent.set(agentName, permissionRules);
|
|
24152
24242
|
return permissionRules;
|
|
24153
24243
|
};
|
|
@@ -24747,17 +24837,6 @@ function createReflectCommandHook() {
|
|
|
24747
24837
|
}
|
|
24748
24838
|
// src/hooks/task-session-manager/index.ts
|
|
24749
24839
|
import path12 from "node:path";
|
|
24750
|
-
var AGENT_NAME_SET = new Set([
|
|
24751
|
-
"orchestrator",
|
|
24752
|
-
"oracle",
|
|
24753
|
-
"designer",
|
|
24754
|
-
"explorer",
|
|
24755
|
-
"librarian",
|
|
24756
|
-
"fixer",
|
|
24757
|
-
"observer",
|
|
24758
|
-
"council",
|
|
24759
|
-
"councillor"
|
|
24760
|
-
]);
|
|
24761
24840
|
var MAX_PENDING_TASK_CALLS = 100;
|
|
24762
24841
|
var BACKGROUND_JOB_BOARD_SENTINEL = "SENTINEL: background-job-board-v2";
|
|
24763
24842
|
var BACKGROUND_COMPLETION_COMPLETED = /^Background task completed: /;
|
|
@@ -24789,9 +24868,6 @@ function createOccurrenceId(part, message, partIndex) {
|
|
|
24789
24868
|
const hash = djb2Hash(`${sessionID}:${content}`);
|
|
24790
24869
|
return `anon:${hash}`;
|
|
24791
24870
|
}
|
|
24792
|
-
function isAgentName(value) {
|
|
24793
|
-
return typeof value === "string" && AGENT_NAME_SET.has(value);
|
|
24794
|
-
}
|
|
24795
24871
|
function extractPath(output) {
|
|
24796
24872
|
return /<path>([^<]+)<\/path>/.exec(output)?.[1];
|
|
24797
24873
|
}
|
|
@@ -25072,16 +25148,17 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
25072
25148
|
if (!isRecord(output.args))
|
|
25073
25149
|
return;
|
|
25074
25150
|
const args = output.args;
|
|
25075
|
-
if (
|
|
25151
|
+
if (typeof args.subagent_type !== "string" || args.subagent_type.trim() === "") {
|
|
25076
25152
|
if (typeof args.task_id === "string" && args.task_id.trim() !== "") {
|
|
25077
25153
|
delete args.task_id;
|
|
25078
25154
|
}
|
|
25079
25155
|
return;
|
|
25080
25156
|
}
|
|
25157
|
+
const agentType = args.subagent_type.trim();
|
|
25081
25158
|
const label = deriveTaskSessionLabel({
|
|
25082
25159
|
description: typeof args.description === "string" ? args.description : undefined,
|
|
25083
25160
|
prompt: typeof args.prompt === "string" ? args.prompt : undefined,
|
|
25084
|
-
agentType
|
|
25161
|
+
agentType
|
|
25085
25162
|
});
|
|
25086
25163
|
const pendingCall = {
|
|
25087
25164
|
callId: pendingCallId({
|
|
@@ -25089,7 +25166,7 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
25089
25166
|
sessionID: input.sessionID
|
|
25090
25167
|
}),
|
|
25091
25168
|
parentSessionId: input.sessionID,
|
|
25092
|
-
agentType
|
|
25169
|
+
agentType,
|
|
25093
25170
|
label
|
|
25094
25171
|
};
|
|
25095
25172
|
rememberPendingCall(pendingCall);
|
|
@@ -25097,7 +25174,7 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
25097
25174
|
return;
|
|
25098
25175
|
}
|
|
25099
25176
|
const requested = args.task_id.trim();
|
|
25100
|
-
const remembered = backgroundJobBoard.resolveReusable(input.sessionID, requested,
|
|
25177
|
+
const remembered = backgroundJobBoard.resolveReusable(input.sessionID, requested, agentType);
|
|
25101
25178
|
if (!remembered) {
|
|
25102
25179
|
if (RAW_SESSION_ID_PATTERN.test(requested)) {
|
|
25103
25180
|
pendingCall.resumedTaskId = requested;
|
|
@@ -25434,26 +25511,25 @@ function slugify(value) {
|
|
|
25434
25511
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
|
|
25435
25512
|
}
|
|
25436
25513
|
function extractHistorySection(document) {
|
|
25437
|
-
const marker =
|
|
25438
|
-
|
|
25439
|
-
|
|
25440
|
-
|
|
25441
|
-
return
|
|
25514
|
+
const marker = /## Q&A history/i;
|
|
25515
|
+
const match = document.match(marker);
|
|
25516
|
+
if (!match || match.index === undefined)
|
|
25517
|
+
return "";
|
|
25518
|
+
return document.slice(match.index + match[0].length).trim();
|
|
25442
25519
|
}
|
|
25443
25520
|
function extractSummarySection(document) {
|
|
25444
25521
|
const marker = `## Current spec
|
|
25445
25522
|
|
|
25446
25523
|
`;
|
|
25447
|
-
const historyMarker = `
|
|
25448
|
-
|
|
25449
|
-
## Q&A history`;
|
|
25450
25524
|
const start = document.indexOf(marker);
|
|
25451
25525
|
if (start < 0) {
|
|
25452
25526
|
return "";
|
|
25453
25527
|
}
|
|
25454
25528
|
const summaryStart = start + marker.length;
|
|
25455
|
-
const
|
|
25456
|
-
|
|
25529
|
+
const historyMarker = /\n\n## Q&A history/i;
|
|
25530
|
+
const historyMatch = document.slice(summaryStart).match(historyMarker);
|
|
25531
|
+
const summaryEnd = historyMatch?.index !== undefined ? summaryStart + historyMatch.index : undefined;
|
|
25532
|
+
return document.slice(summaryStart, summaryEnd).trim();
|
|
25457
25533
|
}
|
|
25458
25534
|
function extractTitle(document) {
|
|
25459
25535
|
const match = document.match(/^#\s+(.+)$/m);
|
|
@@ -25462,11 +25538,19 @@ function extractTitle(document) {
|
|
|
25462
25538
|
function buildInterviewDocument(idea, summary, history, meta) {
|
|
25463
25539
|
const normalizedSummary = summary.trim() || "Waiting for interview answers.";
|
|
25464
25540
|
const normalizedHistory = history.trim() || "No answers yet.";
|
|
25541
|
+
const now = new Date;
|
|
25542
|
+
const dateStr = now.toISOString().split("T")[0];
|
|
25543
|
+
const owner = meta?.owner ?? "agent";
|
|
25544
|
+
const tags = meta?.tags ?? ["spec", "diagnostic"];
|
|
25465
25545
|
const frontmatter = meta?.sessionID ? [
|
|
25466
25546
|
"---",
|
|
25467
25547
|
`sessionID: ${meta.sessionID}`,
|
|
25468
25548
|
`baseMessageCount: ${meta.baseMessageCount ?? 0}`,
|
|
25469
|
-
`updatedAt: ${
|
|
25549
|
+
`updatedAt: ${now.toISOString()}`,
|
|
25550
|
+
`version: 1.0`,
|
|
25551
|
+
`date_created: ${dateStr}`,
|
|
25552
|
+
`owner: ${owner}`,
|
|
25553
|
+
`tags: [${tags.join(", ")}]`,
|
|
25470
25554
|
"---",
|
|
25471
25555
|
""
|
|
25472
25556
|
].join(`
|
|
@@ -25548,6 +25632,55 @@ A: ${answer.answer.trim()}` : null;
|
|
|
25548
25632
|
baseMessageCount: record.baseMessageCount
|
|
25549
25633
|
}), "utf8");
|
|
25550
25634
|
}
|
|
25635
|
+
function parseSpecBlocks(markdown) {
|
|
25636
|
+
const blocks = [];
|
|
25637
|
+
const lines = markdown.split(`
|
|
25638
|
+
`);
|
|
25639
|
+
let currentBlockId = null;
|
|
25640
|
+
let currentBlockTitle = null;
|
|
25641
|
+
let currentBlockLines = [];
|
|
25642
|
+
const flush = () => {
|
|
25643
|
+
if (currentBlockId) {
|
|
25644
|
+
blocks.push({
|
|
25645
|
+
id: currentBlockId,
|
|
25646
|
+
title: currentBlockTitle || currentBlockId,
|
|
25647
|
+
content: currentBlockLines.join(`
|
|
25648
|
+
`).trim()
|
|
25649
|
+
});
|
|
25650
|
+
}
|
|
25651
|
+
};
|
|
25652
|
+
for (const line of lines) {
|
|
25653
|
+
if (/^##\s+Q&A history\s*$/i.test(line)) {
|
|
25654
|
+
break;
|
|
25655
|
+
}
|
|
25656
|
+
const headerMatch = line.match(/^##\s+(\d+)\.\s+(.+)$/);
|
|
25657
|
+
if (headerMatch) {
|
|
25658
|
+
flush();
|
|
25659
|
+
const num = headerMatch[1];
|
|
25660
|
+
const name = headerMatch[2].trim();
|
|
25661
|
+
currentBlockId = `section-${num}`;
|
|
25662
|
+
currentBlockTitle = `${num}. ${name}`;
|
|
25663
|
+
currentBlockLines = [];
|
|
25664
|
+
} else if (line.startsWith("# ") && !line.startsWith("## ")) {
|
|
25665
|
+
if (currentBlockId === null) {
|
|
25666
|
+
currentBlockId = "section-0";
|
|
25667
|
+
currentBlockTitle = "Introduction";
|
|
25668
|
+
currentBlockLines = [];
|
|
25669
|
+
}
|
|
25670
|
+
} else if (line.startsWith("## ") && !headerMatch) {
|
|
25671
|
+
flush();
|
|
25672
|
+
const name = line.replace(/^##\s+/, "").trim();
|
|
25673
|
+
currentBlockId = `section-${slugify(name)}`;
|
|
25674
|
+
currentBlockTitle = name;
|
|
25675
|
+
currentBlockLines = [];
|
|
25676
|
+
}
|
|
25677
|
+
if (currentBlockId !== null) {
|
|
25678
|
+
currentBlockLines.push(line);
|
|
25679
|
+
}
|
|
25680
|
+
}
|
|
25681
|
+
flush();
|
|
25682
|
+
return blocks;
|
|
25683
|
+
}
|
|
25551
25684
|
|
|
25552
25685
|
// src/interview/helpers.ts
|
|
25553
25686
|
function sendJson(response, status, value) {
|
|
@@ -26037,6 +26170,65 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
26037
26170
|
<style>
|
|
26038
26171
|
${sharedStyles()}
|
|
26039
26172
|
.brand-mark { width: 144px; height: 144px; }
|
|
26173
|
+
.spec-block-card {
|
|
26174
|
+
background: rgba(255,255,255,0.01);
|
|
26175
|
+
border: 1px solid rgba(255,255,255,0.05);
|
|
26176
|
+
border-radius: 8px;
|
|
26177
|
+
padding: 24px;
|
|
26178
|
+
margin-bottom: 20px;
|
|
26179
|
+
text-align: left;
|
|
26180
|
+
transition: all 0.2s ease;
|
|
26181
|
+
}
|
|
26182
|
+
.spec-block-card:hover {
|
|
26183
|
+
border-color: rgba(255,255,255,0.15);
|
|
26184
|
+
background: rgba(255,255,255,0.02);
|
|
26185
|
+
}
|
|
26186
|
+
.spec-block-card h3 {
|
|
26187
|
+
margin-top: 0;
|
|
26188
|
+
font-size: 18px;
|
|
26189
|
+
color: #ffffff;
|
|
26190
|
+
border-bottom: 1px solid rgba(255,255,255,0.08);
|
|
26191
|
+
padding-bottom: 8px;
|
|
26192
|
+
margin-bottom: 14px;
|
|
26193
|
+
}
|
|
26194
|
+
.spec-block-content {
|
|
26195
|
+
font-size: 15px;
|
|
26196
|
+
line-height: 1.6;
|
|
26197
|
+
color: rgba(255,255,255,0.8);
|
|
26198
|
+
}
|
|
26199
|
+
.spec-block-comment-box {
|
|
26200
|
+
margin-top: 16px;
|
|
26201
|
+
padding-top: 16px;
|
|
26202
|
+
border-top: 1px dashed rgba(255,255,255,0.08);
|
|
26203
|
+
display: flex;
|
|
26204
|
+
flex-direction: column;
|
|
26205
|
+
gap: 10px;
|
|
26206
|
+
}
|
|
26207
|
+
.spec-block-comment-input {
|
|
26208
|
+
min-height: 60px !important;
|
|
26209
|
+
font-size: 14px !important;
|
|
26210
|
+
padding: 10px !important;
|
|
26211
|
+
}
|
|
26212
|
+
.comment-submit-btn {
|
|
26213
|
+
align-self: flex-end;
|
|
26214
|
+
background: transparent;
|
|
26215
|
+
border: 1px solid rgba(255,255,255,0.2);
|
|
26216
|
+
color: rgba(255,255,255,0.8);
|
|
26217
|
+
padding: 8px 16px;
|
|
26218
|
+
border-radius: 6px;
|
|
26219
|
+
font-size: 13px;
|
|
26220
|
+
cursor: pointer;
|
|
26221
|
+
transition: all 0.2s ease;
|
|
26222
|
+
}
|
|
26223
|
+
.comment-submit-btn:hover:not(:disabled) {
|
|
26224
|
+
border-color: #34d399;
|
|
26225
|
+
color: #34d399;
|
|
26226
|
+
background: rgba(52,211,153,0.05);
|
|
26227
|
+
}
|
|
26228
|
+
.comment-submit-btn:disabled {
|
|
26229
|
+
opacity: 0.3;
|
|
26230
|
+
cursor: not-allowed;
|
|
26231
|
+
}
|
|
26040
26232
|
h1 { font-size: 32px; font-weight: 600; letter-spacing: -0.02em; margin-bottom: 12px; line-height: 1.2; }
|
|
26041
26233
|
h2 { font-size: 18px; font-weight: 500; letter-spacing: 0.05em; text-transform: uppercase; color: rgba(255,255,255,0.4); margin-bottom: 24px; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 12px; }
|
|
26042
26234
|
h3 { font-size: 18px; font-weight: 500; margin-bottom: 16px; line-height: 1.4; }
|
|
@@ -26402,9 +26594,102 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
26402
26594
|
background: rgba(52,211,153,0.05);
|
|
26403
26595
|
}
|
|
26404
26596
|
.nudge-btn:disabled { opacity: 0.3; cursor: not-allowed; }
|
|
26597
|
+
|
|
26598
|
+
/* ── TOC Sidebar ──────────────────────────────────────────────── */
|
|
26599
|
+
.toc-sidebar {
|
|
26600
|
+
position: fixed;
|
|
26601
|
+
top: 0; left: 0; bottom: 0;
|
|
26602
|
+
width: 240px;
|
|
26603
|
+
background: rgba(255,255,255,0.02);
|
|
26604
|
+
border-right: 1px solid rgba(255,255,255,0.06);
|
|
26605
|
+
padding: 24px 0 80px;
|
|
26606
|
+
overflow-y: auto;
|
|
26607
|
+
z-index: 50;
|
|
26608
|
+
display: none;
|
|
26609
|
+
}
|
|
26610
|
+
.toc-sidebar.visible { display: block; }
|
|
26611
|
+
.toc-header {
|
|
26612
|
+
font-size: 11px;
|
|
26613
|
+
font-weight: 700;
|
|
26614
|
+
letter-spacing: 0.12em;
|
|
26615
|
+
text-transform: uppercase;
|
|
26616
|
+
color: rgba(255,255,255,0.28);
|
|
26617
|
+
padding: 0 20px 14px;
|
|
26618
|
+
border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
26619
|
+
margin-bottom: 8px;
|
|
26620
|
+
}
|
|
26621
|
+
.toc-item {
|
|
26622
|
+
display: block;
|
|
26623
|
+
padding: 7px 20px;
|
|
26624
|
+
font-size: 13px;
|
|
26625
|
+
color: rgba(255,255,255,0.5);
|
|
26626
|
+
text-decoration: none;
|
|
26627
|
+
cursor: pointer;
|
|
26628
|
+
transition: all 0.15s ease;
|
|
26629
|
+
border-left: 2px solid transparent;
|
|
26630
|
+
line-height: 1.4;
|
|
26631
|
+
}
|
|
26632
|
+
.toc-item:hover {
|
|
26633
|
+
color: rgba(255,255,255,0.85);
|
|
26634
|
+
background: rgba(255,255,255,0.03);
|
|
26635
|
+
}
|
|
26636
|
+
.toc-item.active {
|
|
26637
|
+
color: #ffffff;
|
|
26638
|
+
border-left-color: #ffffff;
|
|
26639
|
+
background: rgba(255,255,255,0.04);
|
|
26640
|
+
}
|
|
26641
|
+
body.toc-visible .wrap {
|
|
26642
|
+
margin-left: 240px;
|
|
26643
|
+
}
|
|
26644
|
+
|
|
26645
|
+
/* ── Chat Panel ───────────────────────────────────────────────── */
|
|
26646
|
+
.chat-panel {
|
|
26647
|
+
position: fixed;
|
|
26648
|
+
bottom: 0; left: 0; right: 0;
|
|
26649
|
+
background: rgba(0,0,0,0.92);
|
|
26650
|
+
backdrop-filter: blur(12px);
|
|
26651
|
+
border-top: 1px solid rgba(255,255,255,0.08);
|
|
26652
|
+
padding: 12px 20px;
|
|
26653
|
+
z-index: 60;
|
|
26654
|
+
display: none;
|
|
26655
|
+
gap: 10px;
|
|
26656
|
+
align-items: center;
|
|
26657
|
+
}
|
|
26658
|
+
.chat-panel.visible { display: flex; }
|
|
26659
|
+
body.chat-visible .wrap { padding-bottom: 72px; }
|
|
26660
|
+
body.toc-visible .chat-panel { left: 240px; }
|
|
26661
|
+
.chat-input {
|
|
26662
|
+
flex: 1;
|
|
26663
|
+
background: rgba(255,255,255,0.06);
|
|
26664
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
26665
|
+
border-radius: 8px;
|
|
26666
|
+
color: #ffffff;
|
|
26667
|
+
font-family: inherit;
|
|
26668
|
+
font-size: 14px;
|
|
26669
|
+
padding: 10px 14px;
|
|
26670
|
+
outline: none;
|
|
26671
|
+
transition: border-color 0.2s ease;
|
|
26672
|
+
}
|
|
26673
|
+
.chat-input:focus { border-color: rgba(255,255,255,0.35); }
|
|
26674
|
+
.chat-input::placeholder { color: rgba(255,255,255,0.3); }
|
|
26675
|
+
.chat-send {
|
|
26676
|
+
flex-shrink: 0;
|
|
26677
|
+
background: #ffffff;
|
|
26678
|
+
color: #000000;
|
|
26679
|
+
border: 0;
|
|
26680
|
+
border-radius: 8px;
|
|
26681
|
+
padding: 10px 18px;
|
|
26682
|
+
font-size: 14px;
|
|
26683
|
+
font-weight: 600;
|
|
26684
|
+
cursor: pointer;
|
|
26685
|
+
transition: opacity 0.2s ease;
|
|
26686
|
+
}
|
|
26687
|
+
.chat-send:hover:not(:disabled) { opacity: 0.85; }
|
|
26688
|
+
.chat-send:disabled { opacity: 0.3; cursor: not-allowed; }
|
|
26405
26689
|
</style>
|
|
26406
26690
|
</head>
|
|
26407
26691
|
<body>
|
|
26692
|
+
<nav id="tocSidebar" class="toc-sidebar"></nav>
|
|
26408
26693
|
<div class="wrap">
|
|
26409
26694
|
<a href="/" class="back-link">← All Interviews</a>
|
|
26410
26695
|
<div class="brand-header">
|
|
@@ -26445,6 +26730,11 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
26445
26730
|
<div class="status-text" id="loadingText">Processing...</div>
|
|
26446
26731
|
</div>
|
|
26447
26732
|
|
|
26733
|
+
<div class="chat-panel" id="chatPanel">
|
|
26734
|
+
<input type="text" id="chatInput" class="chat-input" placeholder="Send a message to the agent — add a section, revise content, ask questions..." autocomplete="off" />
|
|
26735
|
+
<button class="chat-send" id="chatSendBtn" type="button" disabled>Send</button>
|
|
26736
|
+
</div>
|
|
26737
|
+
|
|
26448
26738
|
<script>
|
|
26449
26739
|
${clipboardHelperJs()}
|
|
26450
26740
|
const interviewId = ${JSON.stringify(interviewId).replace(/</g, "\\u003c")};
|
|
@@ -26814,21 +27104,94 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
26814
27104
|
return paragraphs.map(p => \`<p>\${p.replace(/\\n/g, '<br>')}</p>\`).join('');
|
|
26815
27105
|
}
|
|
26816
27106
|
|
|
27107
|
+
async function submitBlockComment(sectionTitle, commentText, button) {
|
|
27108
|
+
if (!commentText.trim()) return;
|
|
27109
|
+
button.disabled = true;
|
|
27110
|
+
const submitStatus = document.getElementById('submitStatus');
|
|
27111
|
+
submitStatus.textContent = '';
|
|
27112
|
+
|
|
27113
|
+
try {
|
|
27114
|
+
const res = await fetch('/api/interviews/' + encodeURIComponent(interviewId) + '/block-comment', {
|
|
27115
|
+
method: 'POST',
|
|
27116
|
+
headers: { 'content-type': 'application/json' },
|
|
27117
|
+
body: JSON.stringify({ section: sectionTitle, comment: commentText }),
|
|
27118
|
+
});
|
|
27119
|
+
const payload = await res.json();
|
|
27120
|
+
if (res.ok) {
|
|
27121
|
+
submitStatus.textContent = 'Feedback queued for block: ' + sectionTitle;
|
|
27122
|
+
const input = button.previousElementSibling;
|
|
27123
|
+
if (input) input.value = '';
|
|
27124
|
+
refresh().catch(() => {});
|
|
27125
|
+
} else {
|
|
27126
|
+
submitStatus.textContent = payload.error || 'Failed to submit comment.';
|
|
27127
|
+
}
|
|
27128
|
+
} catch (err) {
|
|
27129
|
+
submitStatus.textContent = 'Error submitting block connection feedback.';
|
|
27130
|
+
} finally {
|
|
27131
|
+
button.disabled = false;
|
|
27132
|
+
}
|
|
27133
|
+
}
|
|
27134
|
+
|
|
26817
27135
|
function renderCompletedView(data) {
|
|
26818
27136
|
const container = document.getElementById('questions');
|
|
26819
|
-
const { spec, qaPairs } = parseDocument(data.document);
|
|
26820
27137
|
const frag = document.createDocumentFragment();
|
|
26821
27138
|
|
|
26822
|
-
|
|
26823
|
-
if (
|
|
27139
|
+
const blocks = data.blocks || [];
|
|
27140
|
+
if (blocks.length > 0) {
|
|
26824
27141
|
const specLabel = document.createElement('div');
|
|
26825
27142
|
specLabel.className = 'section-label';
|
|
26826
|
-
specLabel.textContent = '
|
|
27143
|
+
specLabel.textContent = 'Interactive Specifications (11 Sections)';
|
|
26827
27144
|
frag.appendChild(specLabel);
|
|
26828
|
-
|
|
26829
|
-
|
|
26830
|
-
|
|
26831
|
-
|
|
27145
|
+
|
|
27146
|
+
for (const block of blocks) {
|
|
27147
|
+
const card = document.createElement('div');
|
|
27148
|
+
card.className = 'spec-block-card';
|
|
27149
|
+
|
|
27150
|
+
const header = document.createElement('h3');
|
|
27151
|
+
header.textContent = block.title;
|
|
27152
|
+
card.appendChild(header);
|
|
27153
|
+
|
|
27154
|
+
const content = document.createElement('div');
|
|
27155
|
+
content.className = 'spec-block-content';
|
|
27156
|
+
content.innerHTML = simpleMarkdown(block.content);
|
|
27157
|
+
card.appendChild(content);
|
|
27158
|
+
|
|
27159
|
+
// Per-block comment integration for micro annotations
|
|
27160
|
+
const commentBox = document.createElement('div');
|
|
27161
|
+
commentBox.className = 'spec-block-comment-box';
|
|
27162
|
+
|
|
27163
|
+
const textarea = document.createElement('textarea');
|
|
27164
|
+
textarea.className = 'spec-block-comment-input';
|
|
27165
|
+
textarea.placeholder = 'Provide review comment/revision for ' + block.title + '...';
|
|
27166
|
+
commentBox.appendChild(textarea);
|
|
27167
|
+
|
|
27168
|
+
const sendBtn = document.createElement('button');
|
|
27169
|
+
sendBtn.className = 'comment-submit-btn';
|
|
27170
|
+
sendBtn.type = 'button';
|
|
27171
|
+
sendBtn.textContent = 'Revise Section';
|
|
27172
|
+
sendBtn.addEventListener('click', () => {
|
|
27173
|
+
submitBlockComment(block.title, textarea.value, sendBtn);
|
|
27174
|
+
});
|
|
27175
|
+
commentBox.appendChild(sendBtn);
|
|
27176
|
+
|
|
27177
|
+
card.appendChild(commentBox);
|
|
27178
|
+
frag.appendChild(card);
|
|
27179
|
+
}
|
|
27180
|
+
} else {
|
|
27181
|
+
// Fallback legacy parse if blocks array isn't populated
|
|
27182
|
+
const { spec, qaPairs } = parseDocument(data.document);
|
|
27183
|
+
|
|
27184
|
+
// Spec section
|
|
27185
|
+
if (spec) {
|
|
27186
|
+
const specLabel = document.createElement('div');
|
|
27187
|
+
specLabel.className = 'section-label';
|
|
27188
|
+
specLabel.textContent = 'Current Spec';
|
|
27189
|
+
frag.appendChild(specLabel);
|
|
27190
|
+
const specBlock = document.createElement('div');
|
|
27191
|
+
specBlock.className = 'spec-block';
|
|
27192
|
+
specBlock.innerHTML = simpleMarkdown(spec);
|
|
27193
|
+
frag.appendChild(specBlock);
|
|
27194
|
+
}
|
|
26832
27195
|
}
|
|
26833
27196
|
|
|
26834
27197
|
// Q&A section
|
|
@@ -26837,6 +27200,7 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
26837
27200
|
qaLabel.textContent = 'Q&A History';
|
|
26838
27201
|
frag.appendChild(qaLabel);
|
|
26839
27202
|
|
|
27203
|
+
const { qaPairs } = parseDocument(data.document);
|
|
26840
27204
|
if (!qaPairs.length) {
|
|
26841
27205
|
const empty = document.createElement('p');
|
|
26842
27206
|
empty.className = 'qa-empty';
|
|
@@ -26885,6 +27249,103 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
26885
27249
|
container.replaceChildren(frag);
|
|
26886
27250
|
}
|
|
26887
27251
|
|
|
27252
|
+
// ── TOC Sidebar ──────────────────────────────────────────────
|
|
27253
|
+
function updateTocSidebar(data) {
|
|
27254
|
+
const sidebar = document.getElementById('tocSidebar');
|
|
27255
|
+
const blocks = data.blocks || [];
|
|
27256
|
+
const isDone = ['completed', 'session-disconnected'].includes(data.mode);
|
|
27257
|
+
if (!isDone || !blocks.length) {
|
|
27258
|
+
sidebar.classList.remove('visible');
|
|
27259
|
+
document.body.classList.remove('toc-visible');
|
|
27260
|
+
return;
|
|
27261
|
+
}
|
|
27262
|
+
sidebar.classList.add('visible');
|
|
27263
|
+
document.body.classList.add('toc-visible');
|
|
27264
|
+
sidebar.innerHTML = '';
|
|
27265
|
+
const header = document.createElement('div');
|
|
27266
|
+
header.className = 'toc-header';
|
|
27267
|
+
header.textContent = 'Sections';
|
|
27268
|
+
sidebar.appendChild(header);
|
|
27269
|
+
blocks.forEach((block) => {
|
|
27270
|
+
const item = document.createElement('a');
|
|
27271
|
+
item.className = 'toc-item';
|
|
27272
|
+
item.textContent = block.title;
|
|
27273
|
+
item.addEventListener('click', () => {
|
|
27274
|
+
const cards = document.querySelectorAll('.spec-block-card h3');
|
|
27275
|
+
for (const h3 of cards) {
|
|
27276
|
+
if (h3.textContent === block.title) {
|
|
27277
|
+
h3.closest('.spec-block-card').scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
27278
|
+
break;
|
|
27279
|
+
}
|
|
27280
|
+
}
|
|
27281
|
+
document.querySelectorAll('.toc-item').forEach((el) => el.classList.remove('active'));
|
|
27282
|
+
item.classList.add('active');
|
|
27283
|
+
});
|
|
27284
|
+
sidebar.appendChild(item);
|
|
27285
|
+
});
|
|
27286
|
+
}
|
|
27287
|
+
|
|
27288
|
+
// ── Chat Panel ───────────────────────────────────────────────
|
|
27289
|
+
function updateChatPanel(data) {
|
|
27290
|
+
const panel = document.getElementById('chatPanel');
|
|
27291
|
+
const input = document.getElementById('chatInput');
|
|
27292
|
+
const sendBtn = document.getElementById('chatSendBtn');
|
|
27293
|
+
const isDone = ['completed', 'session-disconnected'].includes(data.mode);
|
|
27294
|
+
if (!isDone) {
|
|
27295
|
+
panel.classList.remove('visible');
|
|
27296
|
+
document.body.classList.remove('chat-visible');
|
|
27297
|
+
return;
|
|
27298
|
+
}
|
|
27299
|
+
panel.classList.add('visible');
|
|
27300
|
+
document.body.classList.add('chat-visible');
|
|
27301
|
+
const busy = data.isBusy || false;
|
|
27302
|
+
sendBtn.disabled = busy;
|
|
27303
|
+
input.disabled = busy;
|
|
27304
|
+
if (busy) {
|
|
27305
|
+
input.placeholder = 'Agent is processing...';
|
|
27306
|
+
} else {
|
|
27307
|
+
input.placeholder = 'Send a message to the agent — add a section, revise content, ask questions...';
|
|
27308
|
+
}
|
|
27309
|
+
}
|
|
27310
|
+
|
|
27311
|
+
async function sendChatMessage() {
|
|
27312
|
+
const input = document.getElementById('chatInput');
|
|
27313
|
+
const sendBtn = document.getElementById('chatSendBtn');
|
|
27314
|
+
const message = input.value.trim();
|
|
27315
|
+
if (!message) return;
|
|
27316
|
+
sendBtn.disabled = true;
|
|
27317
|
+
input.value = '';
|
|
27318
|
+
const submitStatus = document.getElementById('submitStatus');
|
|
27319
|
+
submitStatus.textContent = '';
|
|
27320
|
+
try {
|
|
27321
|
+
const res = await fetch('/api/interviews/' + encodeURIComponent(interviewId) + '/chat', {
|
|
27322
|
+
method: 'POST',
|
|
27323
|
+
headers: { 'content-type': 'application/json' },
|
|
27324
|
+
body: JSON.stringify({ message }),
|
|
27325
|
+
});
|
|
27326
|
+
const payload = await res.json();
|
|
27327
|
+
if (res.ok) {
|
|
27328
|
+
submitStatus.textContent = 'Chat message sent to agent.';
|
|
27329
|
+
refresh().catch(() => {});
|
|
27330
|
+
schedulePoll();
|
|
27331
|
+
} else {
|
|
27332
|
+
submitStatus.textContent = payload.message || payload.error || 'Failed to send chat message.';
|
|
27333
|
+
sendBtn.disabled = false;
|
|
27334
|
+
}
|
|
27335
|
+
} catch (_err) {
|
|
27336
|
+
submitStatus.textContent = 'Network error sending chat message.';
|
|
27337
|
+
sendBtn.disabled = false;
|
|
27338
|
+
}
|
|
27339
|
+
}
|
|
27340
|
+
|
|
27341
|
+
document.getElementById('chatSendBtn').addEventListener('click', sendChatMessage);
|
|
27342
|
+
document.getElementById('chatInput').addEventListener('keydown', (e) => {
|
|
27343
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
27344
|
+
e.preventDefault();
|
|
27345
|
+
sendChatMessage();
|
|
27346
|
+
}
|
|
27347
|
+
});
|
|
27348
|
+
|
|
26888
27349
|
function renderQuestions(questions) {
|
|
26889
27350
|
const sig = JSON.stringify([questions, state.data?.mode]);
|
|
26890
27351
|
const container = document.getElementById('questions');
|
|
@@ -27041,6 +27502,19 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
27041
27502
|
|
|
27042
27503
|
renderQuestions(data.questions || []);
|
|
27043
27504
|
updateSubmitButton();
|
|
27505
|
+
updateTocSidebar(data);
|
|
27506
|
+
updateChatPanel(data);
|
|
27507
|
+
|
|
27508
|
+
// If we transitioned to a non-terminal state and connection/polling is stopped, restart them
|
|
27509
|
+
const terminalModes = ['abandoned', 'completed', 'session-disconnected'];
|
|
27510
|
+
if (!terminalModes.includes(data.mode)) {
|
|
27511
|
+
if (!sseConnected && !activeEs) {
|
|
27512
|
+
connectSse();
|
|
27513
|
+
}
|
|
27514
|
+
if (!sseConnected && !pollFallbackTimer) {
|
|
27515
|
+
schedulePoll();
|
|
27516
|
+
}
|
|
27517
|
+
}
|
|
27044
27518
|
}
|
|
27045
27519
|
|
|
27046
27520
|
async function refresh() {
|
|
@@ -27151,19 +27625,65 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
27151
27625
|
document.getElementById('moreQuestionsBtn').addEventListener('click', () => sendNudge('more-questions'));
|
|
27152
27626
|
document.getElementById('confirmCompleteBtn').addEventListener('click', () => sendNudge('confirm-complete'));
|
|
27153
27627
|
|
|
27628
|
+
// ── Real-time updates via SSE ─────────────────────────────────
|
|
27629
|
+
let sseConnected = false;
|
|
27630
|
+
let pollFallbackTimer = null;
|
|
27631
|
+
let activeEs = null;
|
|
27632
|
+
|
|
27633
|
+
function connectSse() {
|
|
27634
|
+
if (activeEs) {
|
|
27635
|
+
activeEs.close();
|
|
27636
|
+
}
|
|
27637
|
+
const sseUrl = '/api/interviews/' + encodeURIComponent(interviewId) + '/events';
|
|
27638
|
+
const es = new EventSource(sseUrl);
|
|
27639
|
+
activeEs = es;
|
|
27640
|
+
|
|
27641
|
+
es.addEventListener('state', (e) => {
|
|
27642
|
+
sseConnected = true;
|
|
27643
|
+
if (pollFallbackTimer) {
|
|
27644
|
+
clearTimeout(pollFallbackTimer);
|
|
27645
|
+
pollFallbackTimer = null;
|
|
27646
|
+
}
|
|
27647
|
+
try {
|
|
27648
|
+
const data = JSON.parse(e.data);
|
|
27649
|
+
render(data);
|
|
27650
|
+
} catch (_) {}
|
|
27651
|
+
});
|
|
27652
|
+
|
|
27653
|
+
es.onerror = () => {
|
|
27654
|
+
sseConnected = false;
|
|
27655
|
+
es.close();
|
|
27656
|
+
activeEs = null;
|
|
27657
|
+
const terminalModes = ['abandoned', 'completed', 'session-disconnected'];
|
|
27658
|
+
if (state.data && terminalModes.includes(state.data.mode)) {
|
|
27659
|
+
return;
|
|
27660
|
+
}
|
|
27661
|
+
// Retry SSE after 3s, fall back to polling in the meantime
|
|
27662
|
+
if (!pollFallbackTimer) schedulePoll();
|
|
27663
|
+
setTimeout(() => connectSse(), 3000);
|
|
27664
|
+
};
|
|
27665
|
+
}
|
|
27666
|
+
|
|
27154
27667
|
function schedulePoll() {
|
|
27155
|
-
|
|
27668
|
+
// Only poll if SSE is not connected
|
|
27669
|
+
if (sseConnected) return;
|
|
27670
|
+
const terminalModes = ['abandoned', 'completed', 'session-disconnected'];
|
|
27671
|
+
if (state.data && terminalModes.includes(state.data.mode)) {
|
|
27672
|
+
return;
|
|
27673
|
+
}
|
|
27674
|
+
pollFallbackTimer = setTimeout(async () => {
|
|
27156
27675
|
try { await refresh(); } catch (_) {}
|
|
27157
|
-
|
|
27158
|
-
|
|
27159
|
-
|
|
27676
|
+
pollFallbackTimer = null;
|
|
27677
|
+
if (!sseConnected) {
|
|
27678
|
+
schedulePoll();
|
|
27679
|
+
}
|
|
27160
27680
|
}, 2500);
|
|
27161
27681
|
}
|
|
27162
27682
|
|
|
27163
27683
|
refresh().catch((error) => {
|
|
27164
27684
|
document.getElementById('submitStatus').textContent = error.message || 'Failed to load interview.';
|
|
27165
27685
|
});
|
|
27166
|
-
|
|
27686
|
+
connectSse();
|
|
27167
27687
|
</script>
|
|
27168
27688
|
</body>
|
|
27169
27689
|
</html>`;
|
|
@@ -27232,6 +27752,51 @@ function createDashboardServer(config) {
|
|
|
27232
27752
|
let baseUrl = null;
|
|
27233
27753
|
const sessions = new Map;
|
|
27234
27754
|
const stateCache = new Map;
|
|
27755
|
+
const sseClients = new Map;
|
|
27756
|
+
function formatSseState(entry) {
|
|
27757
|
+
const markdownPath = entry.filePath;
|
|
27758
|
+
const displayPath = markdownPath ? markdownPath.split("/").pop() || markdownPath : "interview.md";
|
|
27759
|
+
const document = entry.document ?? "";
|
|
27760
|
+
return {
|
|
27761
|
+
interview: {
|
|
27762
|
+
id: entry.interviewId,
|
|
27763
|
+
sessionID: entry.sessionID,
|
|
27764
|
+
idea: entry.idea,
|
|
27765
|
+
markdownPath: displayPath,
|
|
27766
|
+
createdAt: new Date(entry.lastUpdatedAt).toISOString(),
|
|
27767
|
+
status: entry.mode === "session-disconnected" ? "abandoned" : "active",
|
|
27768
|
+
baseMessageCount: 0
|
|
27769
|
+
},
|
|
27770
|
+
url: `${baseUrl}/interview/${entry.interviewId}`,
|
|
27771
|
+
markdownPath,
|
|
27772
|
+
mode: entry.mode,
|
|
27773
|
+
isBusy: entry.mode === "awaiting-agent",
|
|
27774
|
+
summary: entry.summary,
|
|
27775
|
+
questions: entry.questions,
|
|
27776
|
+
document,
|
|
27777
|
+
lastUpdatedAt: entry.lastUpdatedAt,
|
|
27778
|
+
nudgeAction: entry.nudgeAction,
|
|
27779
|
+
blocks: entry.blocks ?? parseSpecBlocks(document)
|
|
27780
|
+
};
|
|
27781
|
+
}
|
|
27782
|
+
function broadcastSse(interviewId, entry) {
|
|
27783
|
+
const clients = sseClients.get(interviewId);
|
|
27784
|
+
if (!clients || clients.size === 0)
|
|
27785
|
+
return;
|
|
27786
|
+
const payload = `event: state
|
|
27787
|
+
data: ${JSON.stringify(formatSseState(entry))}
|
|
27788
|
+
|
|
27789
|
+
`;
|
|
27790
|
+
for (const res of clients) {
|
|
27791
|
+
try {
|
|
27792
|
+
res.write(payload);
|
|
27793
|
+
} catch {
|
|
27794
|
+
clients.delete(res);
|
|
27795
|
+
}
|
|
27796
|
+
}
|
|
27797
|
+
if (clients.size === 0)
|
|
27798
|
+
sseClients.delete(interviewId);
|
|
27799
|
+
}
|
|
27235
27800
|
const TERMINAL_MODES = new Set([
|
|
27236
27801
|
"abandoned",
|
|
27237
27802
|
"completed",
|
|
@@ -27390,7 +27955,9 @@ function createDashboardServer(config) {
|
|
|
27390
27955
|
pendingAnswers: null,
|
|
27391
27956
|
lastUpdatedAt: fm.updatedAt ? new Date(fm.updatedAt).getTime() : Date.now(),
|
|
27392
27957
|
filePath: path14.join(interviewDir, entry),
|
|
27393
|
-
nudgeAction: null
|
|
27958
|
+
nudgeAction: null,
|
|
27959
|
+
pendingBlockComment: null,
|
|
27960
|
+
pendingChatMessage: null
|
|
27394
27961
|
});
|
|
27395
27962
|
if (!sessions.has(fm.sessionID)) {
|
|
27396
27963
|
sessions.set(fm.sessionID, {
|
|
@@ -27571,7 +28138,9 @@ function createDashboardServer(config) {
|
|
|
27571
28138
|
pendingAnswers: null,
|
|
27572
28139
|
lastUpdatedAt: Date.now(),
|
|
27573
28140
|
filePath: "",
|
|
27574
|
-
nudgeAction: null
|
|
28141
|
+
nudgeAction: null,
|
|
28142
|
+
pendingBlockComment: null,
|
|
28143
|
+
pendingChatMessage: null
|
|
27575
28144
|
});
|
|
27576
28145
|
dedupRecovered(interviewId, stateCache);
|
|
27577
28146
|
fileCache = null;
|
|
@@ -27582,6 +28151,48 @@ function createDashboardServer(config) {
|
|
|
27582
28151
|
});
|
|
27583
28152
|
return;
|
|
27584
28153
|
}
|
|
28154
|
+
if (request.method === "GET" && pathname.startsWith("/api/interviews/") && pathname.endsWith("/events")) {
|
|
28155
|
+
const interviewId = pathname.replace("/api/interviews/", "").replace("/events", "");
|
|
28156
|
+
if (!interviewId || !isValidId(interviewId)) {
|
|
28157
|
+
sendJson(response, 400, { error: "Invalid interview ID" });
|
|
28158
|
+
return;
|
|
28159
|
+
}
|
|
28160
|
+
const entry = stateCache.get(interviewId);
|
|
28161
|
+
if (!entry) {
|
|
28162
|
+
sendJson(response, 404, { error: "Interview not found" });
|
|
28163
|
+
return;
|
|
28164
|
+
}
|
|
28165
|
+
response.writeHead(200, {
|
|
28166
|
+
"content-type": "text/event-stream",
|
|
28167
|
+
"cache-control": "no-cache",
|
|
28168
|
+
connection: "keep-alive",
|
|
28169
|
+
"access-control-allow-origin": "*"
|
|
28170
|
+
});
|
|
28171
|
+
let clients = sseClients.get(interviewId);
|
|
28172
|
+
if (!clients) {
|
|
28173
|
+
clients = new Set;
|
|
28174
|
+
sseClients.set(interviewId, clients);
|
|
28175
|
+
}
|
|
28176
|
+
clients.add(response);
|
|
28177
|
+
response.write(`event: state
|
|
28178
|
+
data: ${JSON.stringify(formatSseState(entry))}
|
|
28179
|
+
|
|
28180
|
+
`);
|
|
28181
|
+
const heartbeat = setInterval(() => {
|
|
28182
|
+
try {
|
|
28183
|
+
response.write(`: hb
|
|
28184
|
+
|
|
28185
|
+
`);
|
|
28186
|
+
} catch {}
|
|
28187
|
+
}, 15000);
|
|
28188
|
+
request.on("close", () => {
|
|
28189
|
+
clearInterval(heartbeat);
|
|
28190
|
+
clients?.delete(response);
|
|
28191
|
+
if (clients && clients.size === 0)
|
|
28192
|
+
sseClients.delete(interviewId);
|
|
28193
|
+
});
|
|
28194
|
+
return;
|
|
28195
|
+
}
|
|
27585
28196
|
if (request.method === "POST" && pathname.startsWith("/api/interviews/") && pathname.endsWith("/state")) {
|
|
27586
28197
|
const interviewId = pathname.replace("/api/interviews/", "").replace("/state", "");
|
|
27587
28198
|
if (!interviewId || !isValidId(interviewId)) {
|
|
@@ -27608,10 +28219,15 @@ function createDashboardServer(config) {
|
|
|
27608
28219
|
existing.questions = state.questions;
|
|
27609
28220
|
if (state.filePath)
|
|
27610
28221
|
existing.filePath = state.filePath;
|
|
28222
|
+
if (state.document !== undefined)
|
|
28223
|
+
existing.document = state.document;
|
|
28224
|
+
if (state.blocks !== undefined)
|
|
28225
|
+
existing.blocks = state.blocks;
|
|
27611
28226
|
existing.lastUpdatedAt = Date.now();
|
|
27612
28227
|
dedupRecovered(interviewId, stateCache);
|
|
28228
|
+
broadcastSse(interviewId, existing);
|
|
27613
28229
|
} else {
|
|
27614
|
-
|
|
28230
|
+
const entry = {
|
|
27615
28231
|
interviewId,
|
|
27616
28232
|
sessionID: state.sessionID ?? "",
|
|
27617
28233
|
idea: state.idea ?? "",
|
|
@@ -27622,8 +28238,14 @@ function createDashboardServer(config) {
|
|
|
27622
28238
|
pendingAnswers: null,
|
|
27623
28239
|
lastUpdatedAt: Date.now(),
|
|
27624
28240
|
filePath: state.filePath ?? "",
|
|
27625
|
-
nudgeAction: null
|
|
27626
|
-
|
|
28241
|
+
nudgeAction: null,
|
|
28242
|
+
pendingBlockComment: state.pendingBlockComment ?? null,
|
|
28243
|
+
pendingChatMessage: state.pendingChatMessage ?? null,
|
|
28244
|
+
document: state.document,
|
|
28245
|
+
blocks: state.blocks
|
|
28246
|
+
};
|
|
28247
|
+
stateCache.set(interviewId, entry);
|
|
28248
|
+
broadcastSse(interviewId, entry);
|
|
27627
28249
|
}
|
|
27628
28250
|
sendJson(response, 200, { status: "ok" });
|
|
27629
28251
|
return;
|
|
@@ -27681,7 +28303,8 @@ function createDashboardServer(config) {
|
|
|
27681
28303
|
questions: entry.questions,
|
|
27682
28304
|
document,
|
|
27683
28305
|
lastUpdatedAt: entry.lastUpdatedAt,
|
|
27684
|
-
nudgeAction: entry.nudgeAction
|
|
28306
|
+
nudgeAction: entry.nudgeAction,
|
|
28307
|
+
blocks: parseSpecBlocks(document)
|
|
27685
28308
|
});
|
|
27686
28309
|
return;
|
|
27687
28310
|
}
|
|
@@ -27716,12 +28339,12 @@ function createDashboardServer(config) {
|
|
|
27716
28339
|
sendJson(response, 200, { status: "ok" });
|
|
27717
28340
|
return;
|
|
27718
28341
|
}
|
|
27719
|
-
if (request.method === "
|
|
28342
|
+
if (request.method === "POST" && pathname.startsWith("/api/interviews/") && pathname.endsWith("/block-comment")) {
|
|
27720
28343
|
if (!isAuthenticated(request)) {
|
|
27721
28344
|
sendJson(response, 401, { error: "Unauthorized" });
|
|
27722
28345
|
return;
|
|
27723
28346
|
}
|
|
27724
|
-
const interviewId = pathname.replace("/api/interviews/", "").replace("/
|
|
28347
|
+
const interviewId = pathname.replace("/api/interviews/", "").replace("/block-comment", "");
|
|
27725
28348
|
if (!isValidId(interviewId)) {
|
|
27726
28349
|
sendJson(response, 400, { error: "Invalid interview ID" });
|
|
27727
28350
|
return;
|
|
@@ -27731,12 +28354,126 @@ function createDashboardServer(config) {
|
|
|
27731
28354
|
sendJson(response, 404, { error: "Interview not found" });
|
|
27732
28355
|
return;
|
|
27733
28356
|
}
|
|
27734
|
-
|
|
27735
|
-
|
|
27736
|
-
|
|
28357
|
+
let body;
|
|
28358
|
+
try {
|
|
28359
|
+
body = await readJsonBody(request);
|
|
28360
|
+
} catch {
|
|
28361
|
+
sendJson(response, 400, { error: "Invalid JSON" });
|
|
28362
|
+
return;
|
|
27737
28363
|
}
|
|
27738
|
-
|
|
27739
|
-
|
|
28364
|
+
const { section, comment } = body;
|
|
28365
|
+
if (typeof section !== "string" || typeof comment !== "string") {
|
|
28366
|
+
sendJson(response, 400, {
|
|
28367
|
+
error: "section and comment must be strings"
|
|
28368
|
+
});
|
|
28369
|
+
return;
|
|
28370
|
+
}
|
|
28371
|
+
entry.pendingBlockComment = { section, comment };
|
|
28372
|
+
entry.mode = "awaiting-agent";
|
|
28373
|
+
entry.lastUpdatedAt = Date.now();
|
|
28374
|
+
sendJson(response, 200, { status: "ok" });
|
|
28375
|
+
return;
|
|
28376
|
+
}
|
|
28377
|
+
if (request.method === "GET" && pathname.startsWith("/api/interviews/") && pathname.endsWith("/block-comment")) {
|
|
28378
|
+
if (!isAuthenticated(request)) {
|
|
28379
|
+
sendJson(response, 401, { error: "Unauthorized" });
|
|
28380
|
+
return;
|
|
28381
|
+
}
|
|
28382
|
+
const interviewId = pathname.replace("/api/interviews/", "").replace("/block-comment", "");
|
|
28383
|
+
if (!isValidId(interviewId)) {
|
|
28384
|
+
sendJson(response, 400, { error: "Invalid interview ID" });
|
|
28385
|
+
return;
|
|
28386
|
+
}
|
|
28387
|
+
const entry = stateCache.get(interviewId);
|
|
28388
|
+
if (!entry) {
|
|
28389
|
+
sendJson(response, 404, { error: "Interview not found" });
|
|
28390
|
+
return;
|
|
28391
|
+
}
|
|
28392
|
+
const val = entry.pendingBlockComment;
|
|
28393
|
+
if (val) {
|
|
28394
|
+
entry.pendingBlockComment = null;
|
|
28395
|
+
}
|
|
28396
|
+
sendJson(response, 200, val || {});
|
|
28397
|
+
return;
|
|
28398
|
+
}
|
|
28399
|
+
if (request.method === "POST" && pathname.startsWith("/api/interviews/") && pathname.endsWith("/chat")) {
|
|
28400
|
+
if (!isAuthenticated(request)) {
|
|
28401
|
+
sendJson(response, 401, { error: "Unauthorized" });
|
|
28402
|
+
return;
|
|
28403
|
+
}
|
|
28404
|
+
const interviewId = pathname.replace("/api/interviews/", "").replace("/chat", "");
|
|
28405
|
+
if (!isValidId(interviewId)) {
|
|
28406
|
+
sendJson(response, 400, { error: "Invalid interview ID" });
|
|
28407
|
+
return;
|
|
28408
|
+
}
|
|
28409
|
+
const entry = stateCache.get(interviewId);
|
|
28410
|
+
if (!entry) {
|
|
28411
|
+
sendJson(response, 404, { error: "Interview not found" });
|
|
28412
|
+
return;
|
|
28413
|
+
}
|
|
28414
|
+
let body;
|
|
28415
|
+
try {
|
|
28416
|
+
body = await readJsonBody(request);
|
|
28417
|
+
} catch {
|
|
28418
|
+
sendJson(response, 400, { error: "Invalid JSON" });
|
|
28419
|
+
return;
|
|
28420
|
+
}
|
|
28421
|
+
const { message } = body;
|
|
28422
|
+
if (typeof message !== "string" || !message.trim()) {
|
|
28423
|
+
sendJson(response, 400, {
|
|
28424
|
+
error: "message must be a non-empty string"
|
|
28425
|
+
});
|
|
28426
|
+
return;
|
|
28427
|
+
}
|
|
28428
|
+
entry.pendingChatMessage = message.trim();
|
|
28429
|
+
entry.mode = "awaiting-agent";
|
|
28430
|
+
entry.lastUpdatedAt = Date.now();
|
|
28431
|
+
sendJson(response, 200, { status: "ok" });
|
|
28432
|
+
return;
|
|
28433
|
+
}
|
|
28434
|
+
if (request.method === "GET" && pathname.startsWith("/api/interviews/") && pathname.endsWith("/chat")) {
|
|
28435
|
+
if (!isAuthenticated(request)) {
|
|
28436
|
+
sendJson(response, 401, { error: "Unauthorized" });
|
|
28437
|
+
return;
|
|
28438
|
+
}
|
|
28439
|
+
const interviewId = pathname.replace("/api/interviews/", "").replace("/chat", "");
|
|
28440
|
+
if (!isValidId(interviewId)) {
|
|
28441
|
+
sendJson(response, 400, { error: "Invalid interview ID" });
|
|
28442
|
+
return;
|
|
28443
|
+
}
|
|
28444
|
+
const entry = stateCache.get(interviewId);
|
|
28445
|
+
if (!entry) {
|
|
28446
|
+
sendJson(response, 404, { error: "Interview not found" });
|
|
28447
|
+
return;
|
|
28448
|
+
}
|
|
28449
|
+
const val = entry.pendingChatMessage;
|
|
28450
|
+
if (val) {
|
|
28451
|
+
entry.pendingChatMessage = null;
|
|
28452
|
+
}
|
|
28453
|
+
sendJson(response, 200, { message: val || null });
|
|
28454
|
+
return;
|
|
28455
|
+
}
|
|
28456
|
+
if (request.method === "GET" && pathname.startsWith("/api/interviews/") && pathname.endsWith("/pending")) {
|
|
28457
|
+
if (!isAuthenticated(request)) {
|
|
28458
|
+
sendJson(response, 401, { error: "Unauthorized" });
|
|
28459
|
+
return;
|
|
28460
|
+
}
|
|
28461
|
+
const interviewId = pathname.replace("/api/interviews/", "").replace("/pending", "");
|
|
28462
|
+
if (!isValidId(interviewId)) {
|
|
28463
|
+
sendJson(response, 400, { error: "Invalid interview ID" });
|
|
28464
|
+
return;
|
|
28465
|
+
}
|
|
28466
|
+
const entry = stateCache.get(interviewId);
|
|
28467
|
+
if (!entry) {
|
|
28468
|
+
sendJson(response, 404, { error: "Interview not found" });
|
|
28469
|
+
return;
|
|
28470
|
+
}
|
|
28471
|
+
const answers = entry.pendingAnswers;
|
|
28472
|
+
if (answers) {
|
|
28473
|
+
entry.pendingAnswers = null;
|
|
28474
|
+
}
|
|
28475
|
+
sendJson(response, 200, {
|
|
28476
|
+
answers
|
|
27740
28477
|
});
|
|
27741
28478
|
return;
|
|
27742
28479
|
}
|
|
@@ -27885,9 +28622,20 @@ function createDashboardServer(config) {
|
|
|
27885
28622
|
entry.pendingAnswers ??= existing.pendingAnswers;
|
|
27886
28623
|
if (existing.nudgeAction)
|
|
27887
28624
|
entry.nudgeAction ??= existing.nudgeAction;
|
|
28625
|
+
if (existing.pendingBlockComment)
|
|
28626
|
+
entry.pendingBlockComment ??= existing.pendingBlockComment;
|
|
28627
|
+
if (existing.pendingChatMessage)
|
|
28628
|
+
entry.pendingChatMessage ??= existing.pendingChatMessage;
|
|
28629
|
+
if (entry.document === undefined && existing.document !== undefined) {
|
|
28630
|
+
entry.document = existing.document;
|
|
28631
|
+
}
|
|
28632
|
+
if (entry.blocks === undefined && existing.blocks !== undefined) {
|
|
28633
|
+
entry.blocks = existing.blocks;
|
|
28634
|
+
}
|
|
27888
28635
|
}
|
|
27889
28636
|
stateCache.set(entry.interviewId, entry);
|
|
27890
28637
|
dedupRecovered(entry.interviewId, stateCache);
|
|
28638
|
+
broadcastSse(entry.interviewId, entry);
|
|
27891
28639
|
},
|
|
27892
28640
|
getState: (id) => stateCache.get(id),
|
|
27893
28641
|
storeAnswers: (id, answers) => {
|
|
@@ -27915,6 +28663,22 @@ function createDashboardServer(config) {
|
|
|
27915
28663
|
entry.nudgeAction = null;
|
|
27916
28664
|
return action;
|
|
27917
28665
|
},
|
|
28666
|
+
consumeBlockComment: (id) => {
|
|
28667
|
+
const entry = stateCache.get(id);
|
|
28668
|
+
if (!entry?.pendingBlockComment)
|
|
28669
|
+
return null;
|
|
28670
|
+
const comment = entry.pendingBlockComment;
|
|
28671
|
+
entry.pendingBlockComment = null;
|
|
28672
|
+
return comment;
|
|
28673
|
+
},
|
|
28674
|
+
consumeChatMessage: (id) => {
|
|
28675
|
+
const entry = stateCache.get(id);
|
|
28676
|
+
if (!entry?.pendingChatMessage)
|
|
28677
|
+
return null;
|
|
28678
|
+
const message = entry.pendingChatMessage;
|
|
28679
|
+
entry.pendingChatMessage = null;
|
|
28680
|
+
return message;
|
|
28681
|
+
},
|
|
27918
28682
|
authToken,
|
|
27919
28683
|
discoverSessionDirectories,
|
|
27920
28684
|
addManualFolder: (dir) => {
|
|
@@ -28102,6 +28866,50 @@ function createInterviewServer(deps) {
|
|
|
28102
28866
|
}
|
|
28103
28867
|
return;
|
|
28104
28868
|
}
|
|
28869
|
+
const blockCommentMatch = pathname.match(/^\/api\/interviews\/([^/]+)\/block-comment$/);
|
|
28870
|
+
if (request.method === "POST" && blockCommentMatch) {
|
|
28871
|
+
try {
|
|
28872
|
+
const body = await readJsonBody(request);
|
|
28873
|
+
if (typeof body.section !== "string" || typeof body.comment !== "string") {
|
|
28874
|
+
sendJson(response, 400, {
|
|
28875
|
+
error: "section and comment must be strings"
|
|
28876
|
+
});
|
|
28877
|
+
return;
|
|
28878
|
+
}
|
|
28879
|
+
await deps.submitBlockComment(decodeURIComponent(blockCommentMatch[1]), body.section, body.comment);
|
|
28880
|
+
sendJson(response, 200, {
|
|
28881
|
+
ok: true,
|
|
28882
|
+
message: "Block feedback forwarded."
|
|
28883
|
+
});
|
|
28884
|
+
} catch (error) {
|
|
28885
|
+
const message = error instanceof Error ? error.message : "Failed to submit block comment.";
|
|
28886
|
+
const status = getSubmissionStatus(error);
|
|
28887
|
+
sendJson(response, status, { ok: false, message });
|
|
28888
|
+
}
|
|
28889
|
+
return;
|
|
28890
|
+
}
|
|
28891
|
+
const chatMatch = pathname.match(/^\/api\/interviews\/([^/]+)\/chat$/);
|
|
28892
|
+
if (request.method === "POST" && chatMatch) {
|
|
28893
|
+
try {
|
|
28894
|
+
const body = await readJsonBody(request);
|
|
28895
|
+
if (typeof body.message !== "string" || !body.message.trim()) {
|
|
28896
|
+
sendJson(response, 400, {
|
|
28897
|
+
error: "message must be a non-empty string"
|
|
28898
|
+
});
|
|
28899
|
+
return;
|
|
28900
|
+
}
|
|
28901
|
+
await deps.submitChat(decodeURIComponent(chatMatch[1]), body.message.trim());
|
|
28902
|
+
sendJson(response, 200, {
|
|
28903
|
+
ok: true,
|
|
28904
|
+
message: "Chat message forwarded to agent."
|
|
28905
|
+
});
|
|
28906
|
+
} catch (error) {
|
|
28907
|
+
const message = error instanceof Error ? error.message : "Failed to submit chat message.";
|
|
28908
|
+
const status = getSubmissionStatus(error);
|
|
28909
|
+
sendJson(response, status, { ok: false, message });
|
|
28910
|
+
}
|
|
28911
|
+
return;
|
|
28912
|
+
}
|
|
28105
28913
|
const nudgeMatch = pathname.match(/^\/api\/interviews\/([^/]+)\/nudge$/);
|
|
28106
28914
|
if (request.method === "POST" && nudgeMatch) {
|
|
28107
28915
|
try {
|
|
@@ -28216,6 +29024,39 @@ function normalizeQuestion(value, index) {
|
|
|
28216
29024
|
suggested: typeof result.data.suggested === "string" && result.data.suggested.trim().length > 0 ? result.data.suggested.trim() : undefined
|
|
28217
29025
|
};
|
|
28218
29026
|
}
|
|
29027
|
+
function repairJsonNewlines(json) {
|
|
29028
|
+
let result = "";
|
|
29029
|
+
let inString = false;
|
|
29030
|
+
let escaped = false;
|
|
29031
|
+
for (let i = 0;i < json.length; i++) {
|
|
29032
|
+
const char = json[i];
|
|
29033
|
+
if (inString) {
|
|
29034
|
+
if (escaped) {
|
|
29035
|
+
result += char;
|
|
29036
|
+
escaped = false;
|
|
29037
|
+
} else if (char === "\\") {
|
|
29038
|
+
result += char;
|
|
29039
|
+
escaped = true;
|
|
29040
|
+
} else if (char === '"') {
|
|
29041
|
+
result += char;
|
|
29042
|
+
inString = false;
|
|
29043
|
+
} else if (char === `
|
|
29044
|
+
`) {
|
|
29045
|
+
result += "\\n";
|
|
29046
|
+
} else if (char === "\r") {
|
|
29047
|
+
result += "\\r";
|
|
29048
|
+
} else {
|
|
29049
|
+
result += char;
|
|
29050
|
+
}
|
|
29051
|
+
} else {
|
|
29052
|
+
if (char === '"') {
|
|
29053
|
+
inString = true;
|
|
29054
|
+
}
|
|
29055
|
+
result += char;
|
|
29056
|
+
}
|
|
29057
|
+
}
|
|
29058
|
+
return result;
|
|
29059
|
+
}
|
|
28219
29060
|
function flattenMessage(message) {
|
|
28220
29061
|
return (message.parts ?? []).map((part) => part.text ?? "").join(`
|
|
28221
29062
|
`).trim();
|
|
@@ -28232,8 +29073,14 @@ function parseAssistantState(text, maxQuestions = 2) {
|
|
|
28232
29073
|
if (!match) {
|
|
28233
29074
|
return { state: null };
|
|
28234
29075
|
}
|
|
29076
|
+
let rawJson = match[1].trim();
|
|
28235
29077
|
try {
|
|
28236
|
-
|
|
29078
|
+
JSON.parse(rawJson);
|
|
29079
|
+
} catch {
|
|
29080
|
+
rawJson = repairJsonNewlines(rawJson);
|
|
29081
|
+
}
|
|
29082
|
+
try {
|
|
29083
|
+
const raw = JSON.parse(rawJson);
|
|
28237
29084
|
const parsed = RawInterviewStateSchema.parse(raw);
|
|
28238
29085
|
const summary = typeof parsed.summary === "string" ? parsed.summary.trim() : "";
|
|
28239
29086
|
const title = typeof parsed.title === "string" && parsed.title.trim().length > 0 ? parsed.title.trim() : undefined;
|
|
@@ -28291,17 +29138,67 @@ ${suggested}`;
|
|
|
28291
29138
|
|
|
28292
29139
|
`);
|
|
28293
29140
|
}
|
|
29141
|
+
var SPECIFICATION_TEMPLATE_GUIDELINE = `
|
|
29142
|
+
Your target document MUST be structured strictly using the following 11-section template (exclude the frontmatter itself from the summary JSON field, as the tool handles frontmatter automatically):
|
|
29143
|
+
|
|
29144
|
+
# Introduction
|
|
29145
|
+
[Short intro to the spec and goals]
|
|
29146
|
+
|
|
29147
|
+
## 1. Purpose & Scope
|
|
29148
|
+
[Intended audience, boundaries, and assumptions]
|
|
29149
|
+
|
|
29150
|
+
## 2. Definitions
|
|
29151
|
+
[Acronyms, terms defined]
|
|
29152
|
+
|
|
29153
|
+
## 3. Requirements, Constraints & Guidelines
|
|
29154
|
+
[Explicitly list requirements using:
|
|
29155
|
+
- **REQ-001**: Description
|
|
29156
|
+
- **SEC-001**: Security constraints
|
|
29157
|
+
- **CON-001**: System constraints/technologies
|
|
29158
|
+
- **GUD-001**: Guidelines]
|
|
29159
|
+
|
|
29160
|
+
## 4. Interfaces & Data Contracts
|
|
29161
|
+
[APIs, JSON schemas, protocol buffers, or class/data structures]
|
|
29162
|
+
|
|
29163
|
+
## 5. Acceptance Criteria
|
|
29164
|
+
[Define testable criteria in Given-When-Then format:
|
|
29165
|
+
- **AC-001**: Given [context], When [action], Then [expected outcome]]
|
|
29166
|
+
|
|
29167
|
+
## 6. Test Automation Strategy
|
|
29168
|
+
[Mocking approach, test framework, unit/integration details]
|
|
29169
|
+
|
|
29170
|
+
## 7. Rationale & Context
|
|
29171
|
+
[Trade-offs and architectural decisions]
|
|
29172
|
+
|
|
29173
|
+
## 8. Dependencies & External Integrations
|
|
29174
|
+
[Conceptual integrations or external dependencies:
|
|
29175
|
+
- **EXT-001**: Dependency details]
|
|
29176
|
+
|
|
29177
|
+
## 9. Examples & Edge Cases
|
|
29178
|
+
[Concrete code examples, settings, or JSON structures]
|
|
29179
|
+
|
|
29180
|
+
## 10. Validation Criteria
|
|
29181
|
+
[Testing or validation check logic]
|
|
29182
|
+
|
|
29183
|
+
## 11. Related Specifications / Further Reading
|
|
29184
|
+
[Internal doc references]
|
|
29185
|
+
|
|
29186
|
+
ANTI-ASSUMPTION & SUB-AGENT DELEGATION RULE:
|
|
29187
|
+
Do not invent file structures, API signatures, package lists, or library behaviors. If you need to trace local code, verify file paths, or check configurations, you MUST call the sub-agent '@explorer' or search files directly. If you need to search documentation or web info for external APIs/libraries, you MUST call the sub-agent '@librarian' or search the web. Do not guess. Pause, run discovery, and integrate facts into the spec.
|
|
29188
|
+
`;
|
|
28294
29189
|
function buildKickoffPrompt(idea, maxQuestions) {
|
|
28295
29190
|
return [
|
|
28296
29191
|
"You are running an interview q&a session for the user inside their repository.",
|
|
28297
29192
|
`Initial idea: ${idea}`,
|
|
29193
|
+
`Goal: Iteratively generate and populate a highly structured Specification document.`,
|
|
29194
|
+
SPECIFICATION_TEMPLATE_GUIDELINE,
|
|
28298
29195
|
`Clarify the idea through short rounds of at most ${maxQuestions} questions at a time.`,
|
|
28299
29196
|
"When useful, each question may include 2 to 4 answer options and one suggested option.",
|
|
28300
29197
|
"Be practical. Focus on the highest-ambiguity and highest-risk decisions first.",
|
|
28301
29198
|
"After any short human-friendly preface, you MUST include a machine-readable block in this exact format:",
|
|
28302
29199
|
"<interview_state>",
|
|
28303
29200
|
"{",
|
|
28304
|
-
' "summary": "
|
|
29201
|
+
' "summary": "Full specification markdown (strictly matching the 11 section titles above)",',
|
|
28305
29202
|
' "title": "concise-kebab-case-title-for-filename",',
|
|
28306
29203
|
' "questions": [',
|
|
28307
29204
|
" {",
|
|
@@ -28315,7 +29212,7 @@ function buildKickoffPrompt(idea, maxQuestions) {
|
|
|
28315
29212
|
"</interview_state>",
|
|
28316
29213
|
"Rules:",
|
|
28317
29214
|
`- Return 0 to ${maxQuestions} questions.`,
|
|
28318
|
-
"- If there are no more useful questions, return zero questions.",
|
|
29215
|
+
"- If there are no more useful questions or the specification is complete, return zero questions.",
|
|
28319
29216
|
`- Do not ask more than ${maxQuestions} questions in one round.`,
|
|
28320
29217
|
'- Provide a concise "title" field (kebab-case, 3-6 words) suitable for a filename.'
|
|
28321
29218
|
].join(`
|
|
@@ -28325,12 +29222,13 @@ function buildResumePrompt(document, maxQuestions) {
|
|
|
28325
29222
|
return [
|
|
28326
29223
|
"Resume the interview from this existing markdown document.",
|
|
28327
29224
|
"Use the current spec and Q&A history as ground truth so far.",
|
|
29225
|
+
SPECIFICATION_TEMPLATE_GUIDELINE,
|
|
28328
29226
|
"Do not restart from scratch.",
|
|
28329
29227
|
"",
|
|
28330
29228
|
document,
|
|
28331
29229
|
"",
|
|
28332
29230
|
`Ask the next highest-value clarifying questions, up to ${maxQuestions} at a time.`,
|
|
28333
|
-
"If there are no more useful questions, return zero questions.",
|
|
29231
|
+
"If there are no more useful questions or the spec is complete, return zero questions.",
|
|
28334
29232
|
"Return the same <interview_state> JSON block format as before."
|
|
28335
29233
|
].join(`
|
|
28336
29234
|
`);
|
|
@@ -28340,12 +29238,13 @@ function buildAnswerPrompt(answers, questions, maxQuestions) {
|
|
|
28340
29238
|
`);
|
|
28341
29239
|
return [
|
|
28342
29240
|
"Continue the same interview.",
|
|
29241
|
+
SPECIFICATION_TEMPLATE_GUIDELINE,
|
|
28343
29242
|
"These were the active questions:",
|
|
28344
29243
|
formatQuestionContext(questions),
|
|
28345
29244
|
"The user answered:",
|
|
28346
29245
|
answerText,
|
|
28347
|
-
"Now update
|
|
28348
|
-
`Return 0 to ${maxQuestions} questions. If there are no more useful questions, return zero questions.`,
|
|
29246
|
+
"Now update the specification summary document and ask the next highest-value clarifying questions.",
|
|
29247
|
+
`Return 0 to ${maxQuestions} questions. If there are no more useful questions or the spec is complete, return zero questions.`,
|
|
28349
29248
|
"Return the same <interview_state> JSON block format as before."
|
|
28350
29249
|
].join(`
|
|
28351
29250
|
|
|
@@ -28405,6 +29304,7 @@ function createInterviewService(ctx, config, deps) {
|
|
|
28405
29304
|
const browserOpener = deps?.openBrowser ?? openBrowser;
|
|
28406
29305
|
const activeInterviewIds = new Map;
|
|
28407
29306
|
const interviewsById = new Map;
|
|
29307
|
+
const activeSyncs = new Map;
|
|
28408
29308
|
const sessionBusy = new Map;
|
|
28409
29309
|
const sessionModel = new Map;
|
|
28410
29310
|
const browserOpened = new Set;
|
|
@@ -28477,6 +29377,19 @@ function createInterviewService(ctx, config, deps) {
|
|
|
28477
29377
|
});
|
|
28478
29378
|
return result.data;
|
|
28479
29379
|
}
|
|
29380
|
+
async function loadMessagesWithRetry(sessionID) {
|
|
29381
|
+
for (let i = 0;i < 8; i++) {
|
|
29382
|
+
const messages = await loadMessages(sessionID);
|
|
29383
|
+
if (messages.length > 0) {
|
|
29384
|
+
const last = messages[messages.length - 1];
|
|
29385
|
+
if (last?.info?.role === "assistant") {
|
|
29386
|
+
return messages;
|
|
29387
|
+
}
|
|
29388
|
+
}
|
|
29389
|
+
await new Promise((resolve3) => setTimeout(resolve3, 250));
|
|
29390
|
+
}
|
|
29391
|
+
return loadMessages(sessionID);
|
|
29392
|
+
}
|
|
28480
29393
|
function isUserVisibleMessage(message) {
|
|
28481
29394
|
return !(message.parts ?? []).some((part) => hasInternalInitiatorMarker(part));
|
|
28482
29395
|
}
|
|
@@ -28545,8 +29458,19 @@ function createInterviewService(ctx, config, deps) {
|
|
|
28545
29458
|
}
|
|
28546
29459
|
return record;
|
|
28547
29460
|
}
|
|
28548
|
-
|
|
28549
|
-
const
|
|
29461
|
+
function syncInterview(interview) {
|
|
29462
|
+
const existing = activeSyncs.get(interview.id);
|
|
29463
|
+
if (existing) {
|
|
29464
|
+
return existing;
|
|
29465
|
+
}
|
|
29466
|
+
const sync = performSyncInterview(interview).finally(() => {
|
|
29467
|
+
activeSyncs.delete(interview.id);
|
|
29468
|
+
});
|
|
29469
|
+
activeSyncs.set(interview.id, sync);
|
|
29470
|
+
return sync;
|
|
29471
|
+
}
|
|
29472
|
+
async function performSyncInterview(interview) {
|
|
29473
|
+
const allMessages = await loadMessagesWithRetry(interview.sessionID);
|
|
28550
29474
|
const interviewMessages = allMessages.slice(interview.baseMessageCount).filter(isUserVisibleMessage);
|
|
28551
29475
|
const parsed = findLatestAssistantState(interviewMessages, maxQuestions);
|
|
28552
29476
|
const existingDocument = await readInterviewDocument(interview);
|
|
@@ -28556,17 +29480,24 @@ function createInterviewService(ctx, config, deps) {
|
|
|
28556
29480
|
summary: extractSummarySection(existingDocument) || fallbackState.summary
|
|
28557
29481
|
};
|
|
28558
29482
|
await maybeRenameWithTitle(interview, state.title);
|
|
28559
|
-
|
|
29483
|
+
let document;
|
|
29484
|
+
if (parsed.state) {
|
|
29485
|
+
document = await rewriteInterviewDocument(interview, state.summary);
|
|
29486
|
+
} else {
|
|
29487
|
+
document = await readInterviewDocument(interview);
|
|
29488
|
+
}
|
|
29489
|
+
const blocks = parseSpecBlocks(document);
|
|
28560
29490
|
const interviewState = {
|
|
28561
29491
|
interview,
|
|
28562
29492
|
url: `${await ensureServer()}/interview/${interview.id}`,
|
|
28563
29493
|
markdownPath: relativeInterviewPath(ctx.directory, interview.markdownPath),
|
|
28564
|
-
mode: interview.status === "abandoned" ? "abandoned" : parsed.state && state.questions.length === 0 ? "completed" : sessionBusy.get(interview.sessionID) === true ? "awaiting-agent" : state.questions.length > 0 ? "awaiting-user" : parsed.latestAssistantError ? "error" : "awaiting-agent",
|
|
29494
|
+
mode: interview.status === "abandoned" ? "abandoned" : parsed.state && state.questions.length === 0 ? "completed" : sessionBusy.get(interview.sessionID) === true ? "awaiting-agent" : state.questions.length > 0 ? "awaiting-user" : parsed.latestAssistantError ? "error" : !parsed.state && sessionBusy.get(interview.sessionID) === false ? "completed" : "awaiting-agent",
|
|
28565
29495
|
lastParseError: parsed.latestAssistantError,
|
|
28566
29496
|
isBusy: sessionBusy.get(interview.sessionID) === true,
|
|
28567
29497
|
summary: state.summary,
|
|
28568
29498
|
questions: state.questions,
|
|
28569
|
-
document
|
|
29499
|
+
document,
|
|
29500
|
+
blocks
|
|
28570
29501
|
};
|
|
28571
29502
|
if (onStateChange) {
|
|
28572
29503
|
onStateChange(interview.id, interviewState);
|
|
@@ -28792,6 +29723,105 @@ function createInterviewService(ctx, config, deps) {
|
|
|
28792
29723
|
fileCache = { items: sorted, at: Date.now() };
|
|
28793
29724
|
return sorted;
|
|
28794
29725
|
}
|
|
29726
|
+
async function submitBlockComment(interviewId, sectionTitle, comment) {
|
|
29727
|
+
const interview = getInterviewById(interviewId);
|
|
29728
|
+
if (!interview) {
|
|
29729
|
+
throw new Error("Interview not found");
|
|
29730
|
+
}
|
|
29731
|
+
if (interview.status === "abandoned") {
|
|
29732
|
+
throw new Error("Interview session is no longer active.");
|
|
29733
|
+
}
|
|
29734
|
+
if (sessionBusy.get(interview.sessionID) === true) {
|
|
29735
|
+
throw new Error("Interview session is busy. Wait for the current response.");
|
|
29736
|
+
}
|
|
29737
|
+
sessionBusy.set(interview.sessionID, true);
|
|
29738
|
+
let promptSent = false;
|
|
29739
|
+
try {
|
|
29740
|
+
const state = await getInterviewState(interviewId);
|
|
29741
|
+
if (state.mode === "error") {
|
|
29742
|
+
throw new Error("Interview is waiting for a valid agent update.");
|
|
29743
|
+
}
|
|
29744
|
+
const relativePath = relativeInterviewPath(ctx.directory, interview.markdownPath);
|
|
29745
|
+
const prompt = [
|
|
29746
|
+
`You are updating the active interview specification document at "${relativePath}".`,
|
|
29747
|
+
`The current document content on disk is:`,
|
|
29748
|
+
`\`\`\`markdown`,
|
|
29749
|
+
state.document,
|
|
29750
|
+
`\`\`\``,
|
|
29751
|
+
``,
|
|
29752
|
+
`The user submitted specific feedback/comments for the section "${sectionTitle}".`,
|
|
29753
|
+
`Feedback: ${comment}`,
|
|
29754
|
+
``,
|
|
29755
|
+
`Update the specification summary (focusing heavily on making changes to the "${sectionTitle}" section) to address this feedback.`,
|
|
29756
|
+
`If this feedback implies other parts of the spec should change, update them too.`,
|
|
29757
|
+
`Include the updated 11-section specification and ask the next highest-value clarifying questions as questions (up to ${maxQuestions} questions) if needed.`,
|
|
29758
|
+
`Return the same <interview_state> JSON block format as before.`
|
|
29759
|
+
].join(`
|
|
29760
|
+
`);
|
|
29761
|
+
const model = sessionModel.get(interview.sessionID);
|
|
29762
|
+
await ctx.client.session.promptAsync({
|
|
29763
|
+
path: { id: interview.sessionID },
|
|
29764
|
+
body: {
|
|
29765
|
+
parts: [createInternalAgentTextPart(prompt)],
|
|
29766
|
+
...model ? { model: parseModelReference(model) ?? undefined } : {}
|
|
29767
|
+
}
|
|
29768
|
+
});
|
|
29769
|
+
promptSent = true;
|
|
29770
|
+
} finally {
|
|
29771
|
+
if (!promptSent) {
|
|
29772
|
+
sessionBusy.set(interview.sessionID, false);
|
|
29773
|
+
}
|
|
29774
|
+
}
|
|
29775
|
+
}
|
|
29776
|
+
async function submitChat(interviewId, message) {
|
|
29777
|
+
const interview = getInterviewById(interviewId);
|
|
29778
|
+
if (!interview) {
|
|
29779
|
+
throw new Error("Interview not found");
|
|
29780
|
+
}
|
|
29781
|
+
if (interview.status === "abandoned") {
|
|
29782
|
+
throw new Error("Interview session is no longer active.");
|
|
29783
|
+
}
|
|
29784
|
+
if (sessionBusy.get(interview.sessionID) === true) {
|
|
29785
|
+
throw new Error("Interview session is busy. Wait for the current response.");
|
|
29786
|
+
}
|
|
29787
|
+
sessionBusy.set(interview.sessionID, true);
|
|
29788
|
+
let promptSent = false;
|
|
29789
|
+
try {
|
|
29790
|
+
const state = await getInterviewState(interviewId);
|
|
29791
|
+
if (state.mode === "error") {
|
|
29792
|
+
throw new Error("Interview is waiting for a valid agent update.");
|
|
29793
|
+
}
|
|
29794
|
+
const relativePath = relativeInterviewPath(ctx.directory, interview.markdownPath);
|
|
29795
|
+
const prompt = [
|
|
29796
|
+
`You are continuing the interview for the specification document at "${relativePath}".`,
|
|
29797
|
+
`The current document content on disk is:`,
|
|
29798
|
+
`\`\`\`markdown`,
|
|
29799
|
+
state.document,
|
|
29800
|
+
`\`\`\``,
|
|
29801
|
+
``,
|
|
29802
|
+
`The user sent a freeform message via the dashboard chat panel:`,
|
|
29803
|
+
`${message}`,
|
|
29804
|
+
``,
|
|
29805
|
+
`Process this request — it may be a request to add a new section, revise existing content, ask clarifying questions, or make structural changes.`,
|
|
29806
|
+
`Update the specification document accordingly and include the updated 11-section specification.`,
|
|
29807
|
+
`Ask up to ${maxQuestions} clarifying questions if needed using the same <interview_state> JSON block format as before.`
|
|
29808
|
+
].join(`
|
|
29809
|
+
`);
|
|
29810
|
+
const model = sessionModel.get(interview.sessionID);
|
|
29811
|
+
await ctx.client.session.promptAsync({
|
|
29812
|
+
path: { id: interview.sessionID },
|
|
29813
|
+
body: {
|
|
29814
|
+
parts: [createInternalAgentTextPart(prompt)],
|
|
29815
|
+
...model ? { model: parseModelReference(model) ?? undefined } : {}
|
|
29816
|
+
}
|
|
29817
|
+
});
|
|
29818
|
+
promptSent = true;
|
|
29819
|
+
} finally {
|
|
29820
|
+
if (!promptSent) {
|
|
29821
|
+
sessionBusy.set(interview.sessionID, false);
|
|
29822
|
+
}
|
|
29823
|
+
}
|
|
29824
|
+
}
|
|
28795
29825
|
async function handleNudgeAction(interviewId, action) {
|
|
28796
29826
|
const interview = getInterviewById(interviewId);
|
|
28797
29827
|
if (!interview) {
|
|
@@ -28807,12 +29837,17 @@ function createInterviewService(ctx, config, deps) {
|
|
|
28807
29837
|
let promptSent = false;
|
|
28808
29838
|
try {
|
|
28809
29839
|
const state = await getInterviewState(interviewId);
|
|
29840
|
+
const relativePath = relativeInterviewPath(ctx.directory, interview.markdownPath);
|
|
28810
29841
|
let prompt;
|
|
28811
29842
|
if (action === "more-questions") {
|
|
28812
29843
|
prompt = [
|
|
28813
|
-
`
|
|
29844
|
+
`You are continuing the interview for the specification document at "${relativePath}".`,
|
|
29845
|
+
`The current document content on disk is:`,
|
|
29846
|
+
`\`\`\`markdown`,
|
|
29847
|
+
state.document,
|
|
29848
|
+
`\`\`\``,
|
|
28814
29849
|
``,
|
|
28815
|
-
`
|
|
29850
|
+
`The user reviewed the completed interview spec and wants you to continue.`,
|
|
28816
29851
|
``,
|
|
28817
29852
|
`Ask up to ${maxQuestions} new clarifying questions about aspects that are still unclear or underspecified.`,
|
|
28818
29853
|
`Include the structured <interview_state> block with new questions.`
|
|
@@ -28820,9 +29855,13 @@ function createInterviewService(ctx, config, deps) {
|
|
|
28820
29855
|
`);
|
|
28821
29856
|
} else {
|
|
28822
29857
|
prompt = [
|
|
28823
|
-
`
|
|
29858
|
+
`You are finishing the interview for the specification document at "${relativePath}".`,
|
|
29859
|
+
`The current document content on disk is:`,
|
|
29860
|
+
`\`\`\`markdown`,
|
|
29861
|
+
state.document,
|
|
29862
|
+
`\`\`\``,
|
|
28824
29863
|
``,
|
|
28825
|
-
`
|
|
29864
|
+
`The user confirmed the interview spec is complete.`,
|
|
28826
29865
|
``,
|
|
28827
29866
|
`Produce a final, polished version of the full spec document.`,
|
|
28828
29867
|
`Do NOT include any <interview_state> block — just output the final spec as clean markdown.`,
|
|
@@ -28857,6 +29896,8 @@ function createInterviewService(ctx, config, deps) {
|
|
|
28857
29896
|
listInterviewFiles,
|
|
28858
29897
|
listInterviews,
|
|
28859
29898
|
submitAnswers,
|
|
29899
|
+
submitBlockComment,
|
|
29900
|
+
submitChat,
|
|
28860
29901
|
handleNudgeAction
|
|
28861
29902
|
};
|
|
28862
29903
|
}
|
|
@@ -28875,6 +29916,8 @@ function createInterviewManager(ctx, config) {
|
|
|
28875
29916
|
listInterviewFiles: async () => service2.listInterviewFiles(),
|
|
28876
29917
|
listInterviews: () => service2.listInterviews(),
|
|
28877
29918
|
submitAnswers: async (interviewId, answers) => service2.submitAnswers(interviewId, answers),
|
|
29919
|
+
submitBlockComment: async (interviewId, section, comment) => service2.submitBlockComment(interviewId, section, comment),
|
|
29920
|
+
submitChat: async (interviewId, message) => service2.submitChat(interviewId, message),
|
|
28878
29921
|
handleNudgeAction: async (interviewId, action) => service2.handleNudgeAction(interviewId, action),
|
|
28879
29922
|
outputFolder: resolvedOutputPath,
|
|
28880
29923
|
port: 0
|
|
@@ -28908,6 +29951,8 @@ function createInterviewManager(ctx, config) {
|
|
|
28908
29951
|
continue;
|
|
28909
29952
|
pollPendingAnswers(sessionID).catch(() => {});
|
|
28910
29953
|
pollNudgeAction(sessionID).catch(() => {});
|
|
29954
|
+
pollBlockComment(sessionID).catch(() => {});
|
|
29955
|
+
pollChat(sessionID).catch(() => {});
|
|
28911
29956
|
}
|
|
28912
29957
|
}, FALLBACK_POLL_INTERVAL);
|
|
28913
29958
|
fallbackTimer?.unref();
|
|
@@ -28939,7 +29984,9 @@ function createInterviewManager(ctx, config) {
|
|
|
28939
29984
|
pendingAnswers: null,
|
|
28940
29985
|
lastUpdatedAt: Date.now(),
|
|
28941
29986
|
filePath: interview.markdownPath,
|
|
28942
|
-
nudgeAction: null
|
|
29987
|
+
nudgeAction: null,
|
|
29988
|
+
pendingBlockComment: null,
|
|
29989
|
+
pendingChatMessage: null
|
|
28943
29990
|
});
|
|
28944
29991
|
dashboard?.registerSession({
|
|
28945
29992
|
sessionID: interview.sessionID,
|
|
@@ -28965,122 +30012,140 @@ function createInterviewManager(ctx, config) {
|
|
|
28965
30012
|
await new Promise((r) => setTimeout(r, 500));
|
|
28966
30013
|
const retry = await probeDashboard(dashboardPort);
|
|
28967
30014
|
if (!retry.alive) {
|
|
28968
|
-
log("[interview] dashboard
|
|
28969
|
-
|
|
28970
|
-
});
|
|
28971
|
-
const perSessionServer = createInterviewServer({
|
|
28972
|
-
getState: async (interviewId) => service.getInterviewState(interviewId),
|
|
28973
|
-
listInterviewFiles: async () => service.listInterviewFiles(),
|
|
28974
|
-
listInterviews: () => service.listInterviews(),
|
|
28975
|
-
submitAnswers: async (interviewId, answers) => service.submitAnswers(interviewId, answers),
|
|
28976
|
-
handleNudgeAction: async (interviewId, action) => service.handleNudgeAction(interviewId, action),
|
|
28977
|
-
outputFolder: path16.join(ctx.directory, outputFolder),
|
|
28978
|
-
port: 0
|
|
28979
|
-
});
|
|
28980
|
-
service.setBaseUrlResolver(() => perSessionServer.ensureStarted());
|
|
28981
|
-
isDashboard = false;
|
|
28982
|
-
initDone = true;
|
|
28983
|
-
return;
|
|
30015
|
+
log("[interview] dashboard probe failed twice, falling back to local server");
|
|
30016
|
+
throw new Error("Dashboard not reachable");
|
|
28984
30017
|
}
|
|
28985
30018
|
}
|
|
30019
|
+
const creds = await readDashboardAuthFile(dashboardPort);
|
|
30020
|
+
if (!creds) {
|
|
30021
|
+
throw new Error("Dashboard credentials file missing");
|
|
30022
|
+
}
|
|
28986
30023
|
dashboardBaseUrl = `http://127.0.0.1:${dashboardPort}`;
|
|
28987
|
-
|
|
28988
|
-
authToken = auth?.token ?? "";
|
|
30024
|
+
authToken = creds.token;
|
|
28989
30025
|
service.setBaseUrlResolver(() => Promise.resolve(dashboardBaseUrl));
|
|
28990
30026
|
service.setStatePushCallback((id, state) => {
|
|
28991
|
-
|
|
28992
|
-
|
|
28993
|
-
|
|
30027
|
+
if (dashboardBaseUrl && authToken) {
|
|
30028
|
+
pushStateViaHttp(dashboardBaseUrl, authToken, id, state).catch((err) => {
|
|
30029
|
+
log("[interview] failed to push state to dashboard:", {
|
|
30030
|
+
error: err instanceof Error ? err.message : String(err)
|
|
30031
|
+
});
|
|
28994
30032
|
});
|
|
28995
|
-
}
|
|
30033
|
+
}
|
|
28996
30034
|
});
|
|
28997
30035
|
service.setOnInterviewCreated((interview) => {
|
|
28998
|
-
|
|
28999
|
-
|
|
29000
|
-
|
|
30036
|
+
if (dashboardBaseUrl && authToken) {
|
|
30037
|
+
registerInterviewViaHttp(dashboardBaseUrl, authToken, interview).catch((err) => {
|
|
30038
|
+
log("[interview] failed to register interview with dashboard:", {
|
|
30039
|
+
error: err instanceof Error ? err.message : String(err)
|
|
30040
|
+
});
|
|
29001
30041
|
});
|
|
29002
|
-
}
|
|
30042
|
+
}
|
|
29003
30043
|
});
|
|
29004
|
-
log("[interview] dashboard mode:
|
|
29005
|
-
|
|
30044
|
+
log("[interview] dashboard mode: registered as session client", {
|
|
30045
|
+
dashboardUrl: dashboardBaseUrl
|
|
29006
30046
|
});
|
|
29007
|
-
startFallbackTimer();
|
|
29008
30047
|
}
|
|
29009
30048
|
} catch (err) {
|
|
29010
|
-
log("[interview] dashboard
|
|
29011
|
-
|
|
30049
|
+
log("[interview] dashboard election failed or unreachable. Falling back to per-session server.", { error: err instanceof Error ? err.message : String(err) });
|
|
30050
|
+
isDashboard = false;
|
|
30051
|
+
const resolvedOutputPath = path16.join(ctx.directory, outputFolder);
|
|
30052
|
+
const server = createInterviewServer({
|
|
30053
|
+
getState: async (interviewId) => service.getInterviewState(interviewId),
|
|
30054
|
+
listInterviewFiles: async () => service.listInterviewFiles(),
|
|
30055
|
+
listInterviews: () => service.listInterviews(),
|
|
30056
|
+
submitAnswers: async (interviewId, answers) => service.submitAnswers(interviewId, answers),
|
|
30057
|
+
submitBlockComment: async (interviewId, section, comment) => service.submitBlockComment(interviewId, section, comment),
|
|
30058
|
+
submitChat: async (interviewId, message) => service.submitChat(interviewId, message),
|
|
30059
|
+
handleNudgeAction: async (interviewId, action) => service.handleNudgeAction(interviewId, action),
|
|
30060
|
+
outputFolder: resolvedOutputPath,
|
|
30061
|
+
port: 0
|
|
29012
30062
|
});
|
|
30063
|
+
service.setBaseUrlResolver(() => server.ensureStarted());
|
|
30064
|
+
service.setStatePushCallback(() => {});
|
|
29013
30065
|
} finally {
|
|
29014
30066
|
initDone = true;
|
|
29015
30067
|
}
|
|
29016
30068
|
})();
|
|
29017
|
-
async function
|
|
29018
|
-
if (!initDone)
|
|
30069
|
+
async function ensureInitialized() {
|
|
30070
|
+
if (!initDone) {
|
|
29019
30071
|
await initPromise;
|
|
30072
|
+
}
|
|
29020
30073
|
}
|
|
29021
|
-
async function
|
|
29022
|
-
|
|
29023
|
-
|
|
29024
|
-
registeredSessions.add(sessionID);
|
|
29025
|
-
if (isDashboard)
|
|
30074
|
+
async function pollPendingAnswers(sessionID) {
|
|
30075
|
+
const interviewId = service.getActiveInterviewId(sessionID);
|
|
30076
|
+
if (!interviewId)
|
|
29026
30077
|
return;
|
|
29027
30078
|
try {
|
|
29028
|
-
await fetch(`${dashboardBaseUrl}/api/
|
|
29029
|
-
|
|
29030
|
-
|
|
29031
|
-
|
|
29032
|
-
|
|
29033
|
-
|
|
29034
|
-
|
|
29035
|
-
|
|
29036
|
-
|
|
29037
|
-
});
|
|
30079
|
+
const res = await fetch(`${dashboardBaseUrl}/api/interviews/${interviewId}/pending?token=${authToken}`, { signal: AbortSignal.timeout(3000) });
|
|
30080
|
+
const body = await res.json();
|
|
30081
|
+
if (res.ok && body.answers && body.answers.length > 0) {
|
|
30082
|
+
log("[interview] delivering pending answers (HTTP poll)", {
|
|
30083
|
+
interviewId,
|
|
30084
|
+
count: body.answers.length
|
|
30085
|
+
});
|
|
30086
|
+
await service.submitAnswers(interviewId, body.answers);
|
|
30087
|
+
}
|
|
29038
30088
|
} catch (err) {
|
|
29039
|
-
log("[interview] failed
|
|
30089
|
+
log("[interview] failed polling pending answers:", {
|
|
29040
30090
|
error: err instanceof Error ? err.message : String(err)
|
|
29041
30091
|
});
|
|
29042
30092
|
}
|
|
29043
30093
|
}
|
|
29044
|
-
async function
|
|
30094
|
+
async function pollNudgeAction(sessionID) {
|
|
29045
30095
|
const interviewId = service.getActiveInterviewId(sessionID);
|
|
29046
30096
|
if (!interviewId)
|
|
29047
30097
|
return;
|
|
29048
30098
|
try {
|
|
29049
|
-
const
|
|
29050
|
-
|
|
29051
|
-
|
|
29052
|
-
|
|
29053
|
-
|
|
29054
|
-
|
|
29055
|
-
|
|
29056
|
-
interviewId,
|
|
29057
|
-
|
|
29058
|
-
});
|
|
29059
|
-
await service.submitAnswers(interviewId, data.answers);
|
|
30099
|
+
const res = await fetch(`${dashboardBaseUrl}/api/interviews/${interviewId}/nudge?token=${authToken}`, { signal: AbortSignal.timeout(3000) });
|
|
30100
|
+
const body = await res.json();
|
|
30101
|
+
if (res.ok && body.action) {
|
|
30102
|
+
log("[interview] delivering nudge action (HTTP poll)", {
|
|
30103
|
+
interviewId,
|
|
30104
|
+
action: body.action
|
|
30105
|
+
});
|
|
30106
|
+
await service.handleNudgeAction(interviewId, body.action);
|
|
30107
|
+
}
|
|
29060
30108
|
} catch (err) {
|
|
29061
|
-
log("[interview] failed
|
|
30109
|
+
log("[interview] failed polling nudge action:", {
|
|
29062
30110
|
error: err instanceof Error ? err.message : String(err)
|
|
29063
30111
|
});
|
|
29064
30112
|
}
|
|
29065
30113
|
}
|
|
29066
|
-
async function
|
|
30114
|
+
async function pollBlockComment(sessionID) {
|
|
29067
30115
|
const interviewId = service.getActiveInterviewId(sessionID);
|
|
29068
30116
|
if (!interviewId)
|
|
29069
30117
|
return;
|
|
29070
30118
|
try {
|
|
29071
|
-
const
|
|
29072
|
-
|
|
29073
|
-
|
|
29074
|
-
|
|
29075
|
-
|
|
29076
|
-
|
|
29077
|
-
|
|
29078
|
-
interviewId,
|
|
29079
|
-
|
|
30119
|
+
const res = await fetch(`${dashboardBaseUrl}/api/interviews/${interviewId}/block-comment?token=${authToken}`, { signal: AbortSignal.timeout(3000) });
|
|
30120
|
+
const body = await res.json();
|
|
30121
|
+
if (res.ok && body.section && body.comment) {
|
|
30122
|
+
log("[interview] delivering block comment (HTTP poll)", {
|
|
30123
|
+
interviewId,
|
|
30124
|
+
section: body.section
|
|
30125
|
+
});
|
|
30126
|
+
await service.submitBlockComment(interviewId, body.section, body.comment);
|
|
30127
|
+
}
|
|
30128
|
+
} catch (err) {
|
|
30129
|
+
log("[interview] failed polling block comment:", {
|
|
30130
|
+
error: err instanceof Error ? err.message : String(err)
|
|
29080
30131
|
});
|
|
29081
|
-
|
|
30132
|
+
}
|
|
30133
|
+
}
|
|
30134
|
+
async function pollChat(sessionID) {
|
|
30135
|
+
const interviewId = service.getActiveInterviewId(sessionID);
|
|
30136
|
+
if (!interviewId)
|
|
30137
|
+
return;
|
|
30138
|
+
try {
|
|
30139
|
+
const res = await fetch(`${dashboardBaseUrl}/api/interviews/${interviewId}/chat?token=${authToken}`, { signal: AbortSignal.timeout(3000) });
|
|
30140
|
+
const body = await res.json();
|
|
30141
|
+
if (res.ok && body.message) {
|
|
30142
|
+
log("[interview] delivering chat message (HTTP poll)", {
|
|
30143
|
+
interviewId
|
|
30144
|
+
});
|
|
30145
|
+
await service.submitChat(interviewId, body.message);
|
|
30146
|
+
}
|
|
29082
30147
|
} catch (err) {
|
|
29083
|
-
log("[interview] failed
|
|
30148
|
+
log("[interview] failed polling chat message:", {
|
|
29084
30149
|
error: err instanceof Error ? err.message : String(err)
|
|
29085
30150
|
});
|
|
29086
30151
|
}
|
|
@@ -29088,28 +30153,39 @@ function createInterviewManager(ctx, config) {
|
|
|
29088
30153
|
return {
|
|
29089
30154
|
registerCommand: (c) => service.registerCommand(c),
|
|
29090
30155
|
handleCommandExecuteBefore: async (input, output) => {
|
|
29091
|
-
await
|
|
29092
|
-
|
|
29093
|
-
|
|
29094
|
-
|
|
30156
|
+
await ensureInitialized();
|
|
30157
|
+
const sessionID = input.sessionID;
|
|
30158
|
+
registeredSessions.add(sessionID);
|
|
30159
|
+
if (!isDashboard && dashboardBaseUrl) {
|
|
30160
|
+
fetch(`${dashboardBaseUrl}/api/sessions?token=${authToken}`, {
|
|
30161
|
+
method: "POST",
|
|
30162
|
+
headers: { "content-type": "application/json" },
|
|
30163
|
+
body: JSON.stringify({
|
|
30164
|
+
sessionID,
|
|
30165
|
+
directory: ctx.directory,
|
|
30166
|
+
pid: process.pid
|
|
30167
|
+
}),
|
|
30168
|
+
signal: AbortSignal.timeout(3000)
|
|
30169
|
+
}).catch(() => {});
|
|
30170
|
+
startFallbackTimer();
|
|
29095
30171
|
}
|
|
30172
|
+
await service.handleCommandExecuteBefore(input, output);
|
|
29096
30173
|
},
|
|
29097
30174
|
handleEvent: async (input) => {
|
|
29098
|
-
await
|
|
29099
|
-
await service.handleEvent(input);
|
|
30175
|
+
await ensureInitialized();
|
|
29100
30176
|
const { event } = input;
|
|
29101
30177
|
const properties = event.properties ?? {};
|
|
29102
|
-
const sessionID = properties.sessionID;
|
|
29103
|
-
|
|
29104
|
-
|
|
29105
|
-
}
|
|
29106
|
-
if (event.type === "session.status" && sessionID) {
|
|
30178
|
+
const sessionID = properties.sessionID ?? null;
|
|
30179
|
+
await service.handleEvent(input);
|
|
30180
|
+
if (event.type === "session.status") {
|
|
29107
30181
|
const status = properties.status;
|
|
29108
|
-
if (status?.type === "idle") {
|
|
30182
|
+
if (sessionID && status?.type === "idle") {
|
|
29109
30183
|
const interviewId = service.getActiveInterviewId(sessionID);
|
|
29110
|
-
if (!isDashboard) {
|
|
30184
|
+
if (!isDashboard && dashboardBaseUrl) {
|
|
29111
30185
|
await pollPendingAnswers(sessionID);
|
|
29112
30186
|
await pollNudgeAction(sessionID);
|
|
30187
|
+
await pollBlockComment(sessionID);
|
|
30188
|
+
await pollChat(sessionID);
|
|
29113
30189
|
} else if (interviewId && dashboard) {
|
|
29114
30190
|
const pending = dashboard.consumePendingAnswers(interviewId);
|
|
29115
30191
|
if (pending && pending.length > 0) {
|
|
@@ -29127,6 +30203,21 @@ function createInterviewManager(ctx, config) {
|
|
|
29127
30203
|
});
|
|
29128
30204
|
await service.handleNudgeAction(interviewId, nudge);
|
|
29129
30205
|
}
|
|
30206
|
+
const comment = dashboard.consumeBlockComment(interviewId);
|
|
30207
|
+
if (comment) {
|
|
30208
|
+
log("[interview] delivering block comment (in-process)", {
|
|
30209
|
+
interviewId,
|
|
30210
|
+
section: comment.section
|
|
30211
|
+
});
|
|
30212
|
+
await service.submitBlockComment(interviewId, comment.section, comment.comment);
|
|
30213
|
+
}
|
|
30214
|
+
const chat = dashboard.consumeChatMessage(interviewId);
|
|
30215
|
+
if (chat) {
|
|
30216
|
+
log("[interview] delivering chat message (in-process)", {
|
|
30217
|
+
interviewId
|
|
30218
|
+
});
|
|
30219
|
+
await service.submitChat(interviewId, chat);
|
|
30220
|
+
}
|
|
29130
30221
|
}
|
|
29131
30222
|
if (interviewId) {
|
|
29132
30223
|
service.getInterviewState(interviewId).catch((err) => {
|
|
@@ -29161,7 +30252,11 @@ function stateToEntry(interviewId, state) {
|
|
|
29161
30252
|
pendingAnswers: null,
|
|
29162
30253
|
lastUpdatedAt: Date.now(),
|
|
29163
30254
|
filePath: state.interview.markdownPath,
|
|
29164
|
-
nudgeAction: null
|
|
30255
|
+
nudgeAction: null,
|
|
30256
|
+
pendingBlockComment: null,
|
|
30257
|
+
pendingChatMessage: null,
|
|
30258
|
+
document: state.document,
|
|
30259
|
+
blocks: state.blocks
|
|
29165
30260
|
};
|
|
29166
30261
|
}
|
|
29167
30262
|
async function pushStateViaHttp(dashboardUrl, token, interviewId, state) {
|
|
@@ -29180,9 +30275,23 @@ async function registerInterviewViaHttp(dashboardUrl, token, interview) {
|
|
|
29180
30275
|
body: JSON.stringify({
|
|
29181
30276
|
interviewId: interview.id,
|
|
29182
30277
|
sessionID: interview.sessionID,
|
|
29183
|
-
idea: interview.idea
|
|
30278
|
+
idea: interview.idea,
|
|
30279
|
+
mode: "awaiting-agent",
|
|
30280
|
+
summary: "Interview created.",
|
|
30281
|
+
title: interview.idea,
|
|
30282
|
+
questions: [],
|
|
30283
|
+
pendingAnswers: null,
|
|
30284
|
+
lastUpdatedAt: Date.now(),
|
|
30285
|
+
filePath: interview.markdownPath,
|
|
30286
|
+
nudgeAction: null,
|
|
30287
|
+
pendingBlockComment: null,
|
|
30288
|
+
pendingChatMessage: null
|
|
29184
30289
|
}),
|
|
29185
30290
|
signal: AbortSignal.timeout(3000)
|
|
30291
|
+
}).catch((err) => {
|
|
30292
|
+
log("[interview] failed to register interview with dashboard via HTTP:", {
|
|
30293
|
+
error: err instanceof Error ? err.message : String(err)
|
|
30294
|
+
});
|
|
29186
30295
|
});
|
|
29187
30296
|
}
|
|
29188
30297
|
// src/mcp/context7.ts
|
|
@@ -29940,7 +31049,6 @@ function startAvailabilityCheck(config) {
|
|
|
29940
31049
|
}
|
|
29941
31050
|
}
|
|
29942
31051
|
// src/multiplexer/session-manager.ts
|
|
29943
|
-
var SESSION_MISSING_GRACE_MS = POLL_INTERVAL_BACKGROUND_MS * 3;
|
|
29944
31052
|
var SHARED_STATE_KEY = Symbol.for("oh-my-opencode-slim.multiplexer-session-manager.state");
|
|
29945
31053
|
function getSharedState() {
|
|
29946
31054
|
const globalWithState = globalThis;
|
|
@@ -29948,7 +31056,8 @@ function getSharedState() {
|
|
|
29948
31056
|
sessions: new Map,
|
|
29949
31057
|
knownSessions: new Map,
|
|
29950
31058
|
spawningSessions: new Set,
|
|
29951
|
-
closingSessions: new Map
|
|
31059
|
+
closingSessions: new Map,
|
|
31060
|
+
deferredIdleCloses: new Set
|
|
29952
31061
|
};
|
|
29953
31062
|
return globalWithState[SHARED_STATE_KEY];
|
|
29954
31063
|
}
|
|
@@ -29962,6 +31071,7 @@ class MultiplexerSessionManager {
|
|
|
29962
31071
|
knownSessions;
|
|
29963
31072
|
spawningSessions;
|
|
29964
31073
|
closingSessions;
|
|
31074
|
+
deferredIdleCloses;
|
|
29965
31075
|
pollInterval;
|
|
29966
31076
|
enabled = false;
|
|
29967
31077
|
constructor(ctx, config, backgroundJobBoard) {
|
|
@@ -29971,6 +31081,7 @@ class MultiplexerSessionManager {
|
|
|
29971
31081
|
this.knownSessions = sharedState.knownSessions;
|
|
29972
31082
|
this.spawningSessions = sharedState.spawningSessions;
|
|
29973
31083
|
this.closingSessions = sharedState.closingSessions;
|
|
31084
|
+
this.deferredIdleCloses = sharedState.deferredIdleCloses;
|
|
29974
31085
|
this.directory = ctx.directory;
|
|
29975
31086
|
const defaultPort = process.env.OPENCODE_PORT ?? "4096";
|
|
29976
31087
|
this.serverUrl = ctx.serverUrl?.toString() ?? `http://localhost:${defaultPort}`;
|
|
@@ -30052,17 +31163,13 @@ class MultiplexerSessionManager {
|
|
|
30052
31163
|
}));
|
|
30053
31164
|
return;
|
|
30054
31165
|
}
|
|
30055
|
-
const now = Date.now();
|
|
30056
31166
|
this.sessions.set(sessionId, {
|
|
30057
31167
|
sessionId,
|
|
30058
31168
|
paneId: paneResult.paneId,
|
|
30059
31169
|
parentId,
|
|
30060
31170
|
title,
|
|
30061
31171
|
directory,
|
|
30062
|
-
ownerInstanceId: this.instanceId
|
|
30063
|
-
createdAt: now,
|
|
30064
|
-
lastSeenAt: now,
|
|
30065
|
-
seenInStatus: false
|
|
31172
|
+
ownerInstanceId: this.instanceId
|
|
30066
31173
|
});
|
|
30067
31174
|
log("[multiplexer-session-manager] pane spawned", {
|
|
30068
31175
|
instanceId: this.instanceId,
|
|
@@ -30097,7 +31204,8 @@ class MultiplexerSessionManager {
|
|
|
30097
31204
|
const sessionId = event.properties?.sessionID;
|
|
30098
31205
|
if (!sessionId)
|
|
30099
31206
|
return;
|
|
30100
|
-
|
|
31207
|
+
const statusType = event.properties?.status?.type;
|
|
31208
|
+
if (statusType === "idle") {
|
|
30101
31209
|
log("[multiplexer-session-manager] session status idle received", {
|
|
30102
31210
|
instanceId: this.instanceId,
|
|
30103
31211
|
sessionId,
|
|
@@ -30109,7 +31217,10 @@ class MultiplexerSessionManager {
|
|
|
30109
31217
|
await this.closeSession(sessionId, "idle");
|
|
30110
31218
|
return;
|
|
30111
31219
|
}
|
|
30112
|
-
if (
|
|
31220
|
+
if (statusType) {
|
|
31221
|
+
this.deferredIdleCloses.delete(sessionId);
|
|
31222
|
+
if (statusType !== "busy")
|
|
31223
|
+
return;
|
|
30113
31224
|
log("[multiplexer-session-manager] session busy event received", {
|
|
30114
31225
|
instanceId: this.instanceId,
|
|
30115
31226
|
sessionId,
|
|
@@ -30137,6 +31248,7 @@ class MultiplexerSessionManager {
|
|
|
30137
31248
|
ownerInstanceId: this.sessions.get(sessionId)?.ownerInstanceId,
|
|
30138
31249
|
backgroundJobState: this.backgroundJobBoard?.get(sessionId)?.state
|
|
30139
31250
|
});
|
|
31251
|
+
this.deferredIdleCloses.delete(sessionId);
|
|
30140
31252
|
await this.closeSession(sessionId, "deleted");
|
|
30141
31253
|
}
|
|
30142
31254
|
startPolling() {
|
|
@@ -30163,7 +31275,6 @@ class MultiplexerSessionManager {
|
|
|
30163
31275
|
}
|
|
30164
31276
|
try {
|
|
30165
31277
|
const allStatuses = await this.fetchSessionStatuses();
|
|
30166
|
-
const now = Date.now();
|
|
30167
31278
|
const sessionsToClose = [];
|
|
30168
31279
|
for (const [sessionId, tracked] of this.sessions.entries()) {
|
|
30169
31280
|
if (tracked.ownerInstanceId !== this.instanceId) {
|
|
@@ -30176,34 +31287,16 @@ class MultiplexerSessionManager {
|
|
|
30176
31287
|
continue;
|
|
30177
31288
|
}
|
|
30178
31289
|
const status = allStatuses[sessionId];
|
|
30179
|
-
|
|
30180
|
-
|
|
30181
|
-
|
|
30182
|
-
|
|
30183
|
-
|
|
30184
|
-
} else if (!tracked.missingSince) {
|
|
30185
|
-
tracked.missingSince = now;
|
|
30186
|
-
}
|
|
30187
|
-
const missingTooLong = !!tracked.missingSince && now - tracked.missingSince >= SESSION_MISSING_GRACE_MS;
|
|
30188
|
-
const shouldKeepRunningBackgroundJob = (isIdle || missingTooLong) && this.isRunningBackgroundJob(sessionId);
|
|
30189
|
-
if (isIdle || missingTooLong) {
|
|
30190
|
-
if (shouldKeepRunningBackgroundJob) {
|
|
30191
|
-
log("[multiplexer-session-manager] keeping running background pane", {
|
|
30192
|
-
instanceId: this.instanceId,
|
|
30193
|
-
sessionId,
|
|
30194
|
-
paneId: tracked.paneId,
|
|
30195
|
-
seenInStatus: tracked.seenInStatus
|
|
30196
|
-
});
|
|
30197
|
-
continue;
|
|
30198
|
-
}
|
|
30199
|
-
sessionsToClose.push({
|
|
30200
|
-
sessionId,
|
|
30201
|
-
reason: isIdle ? "idle" : "missing"
|
|
30202
|
-
});
|
|
31290
|
+
if (!status)
|
|
31291
|
+
continue;
|
|
31292
|
+
if (status.type !== "idle") {
|
|
31293
|
+
this.deferredIdleCloses.delete(sessionId);
|
|
31294
|
+
continue;
|
|
30203
31295
|
}
|
|
31296
|
+
sessionsToClose.push(sessionId);
|
|
30204
31297
|
}
|
|
30205
|
-
for (const
|
|
30206
|
-
await this.closeSession(sessionId,
|
|
31298
|
+
for (const sessionId of sessionsToClose) {
|
|
31299
|
+
await this.closeSession(sessionId, "idle");
|
|
30207
31300
|
}
|
|
30208
31301
|
} catch (err) {
|
|
30209
31302
|
log("[multiplexer-session-manager] poll error", { error: String(err) });
|
|
@@ -30228,6 +31321,7 @@ class MultiplexerSessionManager {
|
|
|
30228
31321
|
async closeSession(sessionId, reason) {
|
|
30229
31322
|
if (reason === "deleted") {
|
|
30230
31323
|
this.knownSessions.delete(sessionId);
|
|
31324
|
+
this.deferredIdleCloses.delete(sessionId);
|
|
30231
31325
|
}
|
|
30232
31326
|
const existingClose = this.closingSessions.get(sessionId);
|
|
30233
31327
|
if (existingClose)
|
|
@@ -30263,6 +31357,7 @@ class MultiplexerSessionManager {
|
|
|
30263
31357
|
});
|
|
30264
31358
|
}
|
|
30265
31359
|
if (reason === "idle" && this.isRunningBackgroundJob(sessionId)) {
|
|
31360
|
+
this.deferredIdleCloses.add(sessionId);
|
|
30266
31361
|
log("[multiplexer-session-manager] close skipped; background job running", {
|
|
30267
31362
|
instanceId: this.instanceId,
|
|
30268
31363
|
sessionId,
|
|
@@ -30272,6 +31367,7 @@ class MultiplexerSessionManager {
|
|
|
30272
31367
|
});
|
|
30273
31368
|
return;
|
|
30274
31369
|
}
|
|
31370
|
+
this.deferredIdleCloses.delete(sessionId);
|
|
30275
31371
|
this.sessions.delete(sessionId);
|
|
30276
31372
|
log("[multiplexer-session-manager] closing session pane", {
|
|
30277
31373
|
instanceId: this.instanceId,
|
|
@@ -30347,18 +31443,15 @@ class MultiplexerSessionManager {
|
|
|
30347
31443
|
}));
|
|
30348
31444
|
return;
|
|
30349
31445
|
}
|
|
30350
|
-
const now = Date.now();
|
|
30351
31446
|
this.sessions.set(sessionId, {
|
|
30352
31447
|
sessionId,
|
|
30353
31448
|
paneId: paneResult.paneId,
|
|
30354
31449
|
parentId: known.parentId,
|
|
30355
31450
|
title: known.title,
|
|
30356
31451
|
directory: known.directory,
|
|
30357
|
-
ownerInstanceId: this.instanceId
|
|
30358
|
-
createdAt: now,
|
|
30359
|
-
lastSeenAt: now,
|
|
30360
|
-
seenInStatus: false
|
|
31452
|
+
ownerInstanceId: this.instanceId
|
|
30361
31453
|
});
|
|
31454
|
+
this.deferredIdleCloses.delete(sessionId);
|
|
30362
31455
|
log("[multiplexer-session-manager] pane respawned on busy", {
|
|
30363
31456
|
instanceId: this.instanceId,
|
|
30364
31457
|
sessionId,
|
|
@@ -30385,6 +31478,13 @@ class MultiplexerSessionManager {
|
|
|
30385
31478
|
isRunningBackgroundJob(sessionId) {
|
|
30386
31479
|
return this.backgroundJobBoard?.get(sessionId)?.state === "running";
|
|
30387
31480
|
}
|
|
31481
|
+
async retryDeferredIdleClose(sessionId) {
|
|
31482
|
+
if (!this.enabled)
|
|
31483
|
+
return;
|
|
31484
|
+
if (!this.deferredIdleCloses.has(sessionId))
|
|
31485
|
+
return;
|
|
31486
|
+
await this.closeSession(sessionId, "idle");
|
|
31487
|
+
}
|
|
30388
31488
|
async cleanup() {
|
|
30389
31489
|
this.stopPolling();
|
|
30390
31490
|
if (this.closingSessions.size > 0) {
|
|
@@ -30405,6 +31505,7 @@ class MultiplexerSessionManager {
|
|
|
30405
31505
|
this.knownSessions.clear();
|
|
30406
31506
|
this.spawningSessions.clear();
|
|
30407
31507
|
this.closingSessions.clear();
|
|
31508
|
+
this.deferredIdleCloses.clear();
|
|
30408
31509
|
log("[multiplexer-session-manager] cleanup complete");
|
|
30409
31510
|
}
|
|
30410
31511
|
}
|
|
@@ -30656,7 +31757,7 @@ function createAcpRunTool(agents = {}) {
|
|
|
30656
31757
|
agent: z4.string().describe("Configured ACP agent name"),
|
|
30657
31758
|
prompt: z4.string().describe("Task or question to send to the ACP agent"),
|
|
30658
31759
|
cwd: z4.string().optional().describe("Optional absolute working directory override"),
|
|
30659
|
-
timeout_ms: z4.number().int().min(
|
|
31760
|
+
timeout_ms: z4.number().int().min(0).max(MAX_ACP_TIMEOUT_MS).optional().describe("Optional timeout override in milliseconds. Set to 0 to disable the timeout.")
|
|
30660
31761
|
},
|
|
30661
31762
|
async execute(args, ctx) {
|
|
30662
31763
|
if (ctx.agent !== args.agent) {
|
|
@@ -30692,11 +31793,12 @@ function createAcpRunTool(agents = {}) {
|
|
|
30692
31793
|
});
|
|
30693
31794
|
const timeoutMs = args.timeout_ms ?? config.timeoutMs;
|
|
30694
31795
|
let timer;
|
|
30695
|
-
const timeout = new Promise((_, reject) => timer = setTimeout(() => reject(new Error(`ACP agent '${args.agent}' timed out after ${timeoutMs}ms`)), timeoutMs));
|
|
31796
|
+
const timeout = timeoutMs > 0 ? new Promise((_, reject) => timer = setTimeout(() => reject(new Error(`ACP agent '${args.agent}' timed out after ${timeoutMs}ms`)), timeoutMs)) : undefined;
|
|
30696
31797
|
const abort = () => client.close();
|
|
30697
31798
|
ctx.abort.addEventListener("abort", abort, { once: true });
|
|
30698
31799
|
try {
|
|
30699
|
-
|
|
31800
|
+
const run = client.run(args.prompt);
|
|
31801
|
+
return timeout ? await Promise.race([run, timeout]) : await run;
|
|
30700
31802
|
} finally {
|
|
30701
31803
|
if (timer)
|
|
30702
31804
|
clearTimeout(timer);
|
|
@@ -31791,7 +32893,8 @@ function emptySnapshot() {
|
|
|
31791
32893
|
return {
|
|
31792
32894
|
version: 1,
|
|
31793
32895
|
updatedAt: Date.now(),
|
|
31794
|
-
agentModels: {}
|
|
32896
|
+
agentModels: {},
|
|
32897
|
+
agentVariants: {}
|
|
31795
32898
|
};
|
|
31796
32899
|
}
|
|
31797
32900
|
function parseSnapshot(value) {
|
|
@@ -31801,7 +32904,8 @@ function parseSnapshot(value) {
|
|
|
31801
32904
|
return {
|
|
31802
32905
|
version: 1,
|
|
31803
32906
|
updatedAt: typeof parsed.updatedAt === "number" ? parsed.updatedAt : Date.now(),
|
|
31804
|
-
agentModels: parsed.agentModels ?? {}
|
|
32907
|
+
agentModels: parsed.agentModels ?? {},
|
|
32908
|
+
agentVariants: parsed.agentVariants ?? {}
|
|
31805
32909
|
};
|
|
31806
32910
|
}
|
|
31807
32911
|
function readTuiSnapshot() {
|
|
@@ -31835,11 +32939,19 @@ function updateSnapshot(mutator) {
|
|
|
31835
32939
|
function recordTuiAgentModels(input) {
|
|
31836
32940
|
updateSnapshot((snapshot) => {
|
|
31837
32941
|
snapshot.agentModels = { ...input.agentModels };
|
|
32942
|
+
snapshot.agentVariants = { ...input.agentVariants ?? {} };
|
|
31838
32943
|
});
|
|
31839
32944
|
}
|
|
31840
32945
|
function recordTuiAgentModel(input) {
|
|
31841
32946
|
updateSnapshot((snapshot) => {
|
|
31842
32947
|
snapshot.agentModels[input.agentName] = input.model;
|
|
32948
|
+
if (input.variant !== undefined) {
|
|
32949
|
+
if (input.variant === null) {
|
|
32950
|
+
delete snapshot.agentVariants[input.agentName];
|
|
32951
|
+
} else {
|
|
32952
|
+
snapshot.agentVariants[input.agentName] = input.variant;
|
|
32953
|
+
}
|
|
32954
|
+
}
|
|
31843
32955
|
});
|
|
31844
32956
|
}
|
|
31845
32957
|
|
|
@@ -31911,12 +33023,18 @@ function createPresetManager(ctx, config) {
|
|
|
31911
33023
|
} catch {}
|
|
31912
33024
|
const snapshot = readTuiSnapshot();
|
|
31913
33025
|
const agentModels = { ...snapshot.agentModels };
|
|
33026
|
+
const agentVariants = { ...snapshot.agentVariants };
|
|
31914
33027
|
for (const [agentName, agentConfig] of Object.entries(agentUpdates)) {
|
|
31915
33028
|
if (typeof agentConfig.model === "string") {
|
|
31916
33029
|
agentModels[agentName] = agentConfig.model;
|
|
31917
33030
|
}
|
|
33031
|
+
if (typeof agentConfig.variant === "string") {
|
|
33032
|
+
agentVariants[agentName] = agentConfig.variant;
|
|
33033
|
+
} else {
|
|
33034
|
+
delete agentVariants[agentName];
|
|
33035
|
+
}
|
|
31918
33036
|
}
|
|
31919
|
-
recordTuiAgentModels({ agentModels });
|
|
33037
|
+
recordTuiAgentModels({ agentModels, agentVariants });
|
|
31920
33038
|
activePreset = presetName;
|
|
31921
33039
|
const summaryParts = [];
|
|
31922
33040
|
for (const [name, cfg] of Object.entries(agentUpdates)) {
|
|
@@ -34309,6 +35427,16 @@ function createWebfetchTool(pluginCtx, options = {}) {
|
|
|
34309
35427
|
}
|
|
34310
35428
|
});
|
|
34311
35429
|
}
|
|
35430
|
+
// src/utils/env.ts
|
|
35431
|
+
var TRUTHY_ENV_VALUES = new Set(["1", "true", "yes", "on"]);
|
|
35432
|
+
var PLUGIN_DISABLE_ENV = "OH_MY_OPENCODE_SLIM_DISABLE";
|
|
35433
|
+
function isTruthyEnvValue(value) {
|
|
35434
|
+
return TRUTHY_ENV_VALUES.has(value?.trim().toLowerCase() ?? "");
|
|
35435
|
+
}
|
|
35436
|
+
function isPluginDisabledByEnv(env = process.env) {
|
|
35437
|
+
return isTruthyEnvValue(env[PLUGIN_DISABLE_ENV]);
|
|
35438
|
+
}
|
|
35439
|
+
|
|
34312
35440
|
// src/utils/subagent-depth.ts
|
|
34313
35441
|
class SubagentDepthTracker {
|
|
34314
35442
|
depthBySession = new Map;
|
|
@@ -34399,6 +35527,10 @@ async function probeJSDOM() {
|
|
|
34399
35527
|
var OhMyOpenCodeLite = async (ctx) => {
|
|
34400
35528
|
const sessionId = new Date().toISOString().replace(/[-:]/g, "").slice(0, 15);
|
|
34401
35529
|
initLogger(sessionId);
|
|
35530
|
+
if (isPluginDisabledByEnv()) {
|
|
35531
|
+
log("[plugin] disabled by OH_MY_OPENCODE_SLIM_DISABLE");
|
|
35532
|
+
return {};
|
|
35533
|
+
}
|
|
34402
35534
|
let config;
|
|
34403
35535
|
let disabledAgents;
|
|
34404
35536
|
let agentDefs;
|
|
@@ -34431,6 +35563,7 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
34431
35563
|
let cancelTaskTools;
|
|
34432
35564
|
let acpRunTools;
|
|
34433
35565
|
let webfetch;
|
|
35566
|
+
let tools;
|
|
34434
35567
|
let rewriteDisplayNameMentions;
|
|
34435
35568
|
let toolCount = 0;
|
|
34436
35569
|
try {
|
|
@@ -34482,6 +35615,9 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
34482
35615
|
readContextMaxFiles: config.backgroundJobs?.readContextMaxFiles ?? 8
|
|
34483
35616
|
});
|
|
34484
35617
|
multiplexerSessionManager = new MultiplexerSessionManager(ctx, multiplexerConfig, backgroundJobBoard);
|
|
35618
|
+
backgroundJobBoard.setTerminalStateListener((taskID) => {
|
|
35619
|
+
multiplexerSessionManager.retryDeferredIdleClose(taskID);
|
|
35620
|
+
});
|
|
34485
35621
|
autoUpdateChecker = createAutoUpdateCheckerHook(ctx, {
|
|
34486
35622
|
autoUpdate: config.autoUpdate ?? true,
|
|
34487
35623
|
companion: config.companion
|
|
@@ -34514,7 +35650,19 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
34514
35650
|
backgroundJobBoard,
|
|
34515
35651
|
shouldManageSession: (sessionID) => sessionAgentMap.get(sessionID) === "orchestrator"
|
|
34516
35652
|
});
|
|
34517
|
-
|
|
35653
|
+
tools = {
|
|
35654
|
+
...councilTools,
|
|
35655
|
+
...cancelTaskTools,
|
|
35656
|
+
...acpRunTools,
|
|
35657
|
+
webfetch,
|
|
35658
|
+
ast_grep_search,
|
|
35659
|
+
ast_grep_replace
|
|
35660
|
+
};
|
|
35661
|
+
if (config.disabled_tools && config.disabled_tools.length > 0) {
|
|
35662
|
+
const disabledTools = new Set(config.disabled_tools);
|
|
35663
|
+
tools = Object.fromEntries(Object.entries(tools).filter(([name]) => !disabledTools.has(name)));
|
|
35664
|
+
}
|
|
35665
|
+
toolCount = Object.keys(tools).length;
|
|
34518
35666
|
} catch (err) {
|
|
34519
35667
|
log("[plugin] FATAL: init failed", String(err));
|
|
34520
35668
|
await appLog(ctx, "error", `INIT FAILED: ${String(err)}. Report at github.com/alvinunreal/oh-my-opencode-slim/issues/310`);
|
|
@@ -34567,17 +35715,25 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
34567
35715
|
}
|
|
34568
35716
|
}
|
|
34569
35717
|
companionManager.onLoad();
|
|
35718
|
+
function resolveTuiVariantForModel(agentName, model) {
|
|
35719
|
+
const configEntry = config.agents?.[agentName];
|
|
35720
|
+
const defaultVariant = typeof configEntry?.variant === "string" ? configEntry.variant : undefined;
|
|
35721
|
+
const chainMatches = modelArrayMap[agentName]?.filter((entry) => entry.id === model);
|
|
35722
|
+
if (chainMatches) {
|
|
35723
|
+
if (chainMatches.length === 1) {
|
|
35724
|
+
return chainMatches[0].variant ?? defaultVariant;
|
|
35725
|
+
}
|
|
35726
|
+
return;
|
|
35727
|
+
}
|
|
35728
|
+
if (typeof configEntry?.model === "string" && configEntry.model === model && defaultVariant) {
|
|
35729
|
+
return defaultVariant;
|
|
35730
|
+
}
|
|
35731
|
+
return;
|
|
35732
|
+
}
|
|
34570
35733
|
return {
|
|
34571
35734
|
name: "oh-my-opencode-slim",
|
|
34572
35735
|
agent: agents,
|
|
34573
|
-
tool:
|
|
34574
|
-
...councilTools,
|
|
34575
|
-
...cancelTaskTools,
|
|
34576
|
-
...acpRunTools,
|
|
34577
|
-
webfetch,
|
|
34578
|
-
ast_grep_search,
|
|
34579
|
-
ast_grep_replace
|
|
34580
|
-
},
|
|
35736
|
+
tool: tools,
|
|
34581
35737
|
mcp: mcps,
|
|
34582
35738
|
config: async (opencodeConfig) => {
|
|
34583
35739
|
if (config.setDefaultAgent !== false && !opencodeConfig.default_agent) {
|
|
@@ -34703,14 +35859,22 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
34703
35859
|
}
|
|
34704
35860
|
}
|
|
34705
35861
|
const tuiAgentModels = {};
|
|
35862
|
+
const tuiAgentVariants = {};
|
|
34706
35863
|
for (const agentDef of agentDefs) {
|
|
34707
35864
|
if (agentDef.name === "councillor")
|
|
34708
35865
|
continue;
|
|
34709
35866
|
const entry = configAgent[agentDef.name];
|
|
34710
35867
|
const resolvedModel = typeof entry?.model === "string" ? entry.model : runtimeChains[agentDef.name]?.[0] ? runtimeChains[agentDef.name][0] : typeof agentDef.config.model === "string" ? agentDef.config.model : undefined;
|
|
35868
|
+
const resolvedVariant = typeof entry?.variant === "string" ? entry.variant : typeof agentDef.config.variant === "string" ? agentDef.config.variant : undefined;
|
|
34711
35869
|
tuiAgentModels[agentDef.name] = resolvedModel ?? "default";
|
|
35870
|
+
if (resolvedVariant) {
|
|
35871
|
+
tuiAgentVariants[agentDef.name] = resolvedVariant;
|
|
35872
|
+
}
|
|
34712
35873
|
}
|
|
34713
|
-
recordTuiAgentModels({
|
|
35874
|
+
recordTuiAgentModels({
|
|
35875
|
+
agentModels: tuiAgentModels,
|
|
35876
|
+
agentVariants: tuiAgentVariants
|
|
35877
|
+
});
|
|
34714
35878
|
const configMcp = opencodeConfig.mcp;
|
|
34715
35879
|
if (!configMcp) {
|
|
34716
35880
|
opencodeConfig.mcp = { ...mcps };
|
|
@@ -34748,10 +35912,16 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
34748
35912
|
const event = input.event;
|
|
34749
35913
|
if (event.type === "message.updated") {
|
|
34750
35914
|
const info = event.properties?.info;
|
|
34751
|
-
|
|
35915
|
+
const providerID = typeof info?.providerID === "string" ? info.providerID : typeof info?.model?.providerID === "string" ? info.model.providerID : undefined;
|
|
35916
|
+
const modelID = typeof info?.modelID === "string" ? info.modelID : typeof info?.model?.modelID === "string" ? info.model.modelID : undefined;
|
|
35917
|
+
if (typeof info?.agent === "string" && providerID && modelID) {
|
|
35918
|
+
const agentName = resolveRuntimeAgentName(config, info.agent);
|
|
35919
|
+
const model = `${providerID}/${modelID}`;
|
|
35920
|
+
const variant = resolveTuiVariantForModel(agentName, model);
|
|
34752
35921
|
recordTuiAgentModel({
|
|
34753
|
-
agentName
|
|
34754
|
-
model
|
|
35922
|
+
agentName,
|
|
35923
|
+
model,
|
|
35924
|
+
variant: variant ?? null
|
|
34755
35925
|
});
|
|
34756
35926
|
}
|
|
34757
35927
|
}
|