oh-my-opencode-slim 2.0.4 → 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 +30 -1
- package/README.zh-CN.md +14 -0
- package/dist/agents/permissions.d.ts +10 -0
- package/dist/cli/index.js +4 -1
- package/dist/cli/skills.d.ts +1 -1
- package/dist/config/constants.d.ts +3 -2
- package/dist/config/schema.d.ts +3 -0
- package/dist/index.js +1418 -249
- 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(),
|
|
@@ -19352,10 +19362,31 @@ function createOrchestratorAgent(model, customPrompt, customAppendPrompt, disabl
|
|
|
19352
19362
|
return definition;
|
|
19353
19363
|
}
|
|
19354
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
|
+
|
|
19355
19386
|
// src/agents/council.ts
|
|
19356
19387
|
var COUNCIL_AGENT_PROMPT = `You are the Council agent — a multi-LLM orchestration system that runs consensus across multiple models.
|
|
19357
19388
|
|
|
19358
|
-
**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.
|
|
19359
19390
|
|
|
19360
19391
|
**When to use**:
|
|
19361
19392
|
- When invoked by a user with a request
|
|
@@ -19416,7 +19447,11 @@ function createCouncilAgent(model, customPrompt, customAppendPrompt) {
|
|
|
19416
19447
|
description: "Multi-LLM council agent that synthesizes responses from multiple models for higher-quality outputs",
|
|
19417
19448
|
config: {
|
|
19418
19449
|
temperature: 0.1,
|
|
19419
|
-
prompt
|
|
19450
|
+
prompt,
|
|
19451
|
+
permission: {
|
|
19452
|
+
...createReadOnlyAgentPermission(),
|
|
19453
|
+
council_session: "allow"
|
|
19454
|
+
}
|
|
19420
19455
|
}
|
|
19421
19456
|
};
|
|
19422
19457
|
if (model) {
|
|
@@ -19525,17 +19560,7 @@ function createCouncillorAgent(model, customPrompt, customAppendPrompt) {
|
|
|
19525
19560
|
model,
|
|
19526
19561
|
temperature: 0.2,
|
|
19527
19562
|
prompt,
|
|
19528
|
-
permission:
|
|
19529
|
-
"*": "deny",
|
|
19530
|
-
question: "deny",
|
|
19531
|
-
read: "allow",
|
|
19532
|
-
glob: "allow",
|
|
19533
|
-
grep: "allow",
|
|
19534
|
-
lsp: "allow",
|
|
19535
|
-
list: "allow",
|
|
19536
|
-
codesearch: "allow",
|
|
19537
|
-
ast_grep_search: "allow"
|
|
19538
|
-
}
|
|
19563
|
+
permission: createReadOnlyAgentPermission()
|
|
19539
19564
|
}
|
|
19540
19565
|
};
|
|
19541
19566
|
}
|
|
@@ -19875,7 +19900,38 @@ function normalizeDisplayName(displayName) {
|
|
|
19875
19900
|
const trimmed = displayName.trim();
|
|
19876
19901
|
return trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
|
|
19877
19902
|
}
|
|
19878
|
-
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) {
|
|
19879
19935
|
const description = config.description ?? `External ACP agent '${name}' via ${config.command}`;
|
|
19880
19936
|
const prompt = config.prompt ?? [
|
|
19881
19937
|
`You are the ${name} ACP wrapper agent.`,
|
|
@@ -19889,7 +19945,7 @@ function buildAcpAgentDefinition(name, config) {
|
|
|
19889
19945
|
name,
|
|
19890
19946
|
description,
|
|
19891
19947
|
config: {
|
|
19892
|
-
model: config.wrapperModel ??
|
|
19948
|
+
model: config.wrapperModel ?? fallbackModel ?? DEFAULT_MODELS.oracle,
|
|
19893
19949
|
temperature: 0,
|
|
19894
19950
|
prompt,
|
|
19895
19951
|
permission: {
|
|
@@ -19954,10 +20010,11 @@ function hasCustomAgentModel(override) {
|
|
|
19954
20010
|
}
|
|
19955
20011
|
function buildCustomAgentDefinition(name, override, filePrompt, fileAppendPrompt) {
|
|
19956
20012
|
const basePrompt = override.prompt ?? `You are the ${name} specialist.`;
|
|
20013
|
+
const primaryModel = getPrimaryModelFromOverride(override);
|
|
19957
20014
|
return {
|
|
19958
20015
|
name,
|
|
19959
20016
|
config: {
|
|
19960
|
-
model:
|
|
20017
|
+
model: primaryModel ?? DEFAULT_MODELS.oracle,
|
|
19961
20018
|
temperature: 0.2,
|
|
19962
20019
|
prompt: resolvePrompt(basePrompt, filePrompt, fileAppendPrompt)
|
|
19963
20020
|
}
|
|
@@ -19974,9 +20031,9 @@ function injectDisplayNames(orchestrator, nameMap) {
|
|
|
19974
20031
|
}
|
|
19975
20032
|
orchestrator.config.prompt = prompt;
|
|
19976
20033
|
}
|
|
19977
|
-
function applyDefaultPermissions(agent, configuredSkills) {
|
|
20034
|
+
function applyDefaultPermissions(agent, configuredSkills, disabledSkills) {
|
|
19978
20035
|
const existing = agent.config.permission ?? {};
|
|
19979
|
-
const skillPermissions = getSkillPermissionsForAgent(agent.name, configuredSkills);
|
|
20036
|
+
const skillPermissions = getSkillPermissionsForAgent(agent.name, configuredSkills, disabledSkills);
|
|
19980
20037
|
const questionPerm = existing.question === "deny" ? "deny" : "allow";
|
|
19981
20038
|
const councilSessionPerm = COUNCIL_TOOL_ALLOWED_AGENTS.has(agent.name) ? existing.council_session ?? "allow" : "deny";
|
|
19982
20039
|
const cancelTaskPerm = CANCEL_TASK_ALLOWED_AGENTS.has(agent.name) ? existing.cancel_task ?? "allow" : "deny";
|
|
@@ -20009,6 +20066,7 @@ function createAgents(config) {
|
|
|
20009
20066
|
if (!config?.council) {
|
|
20010
20067
|
disabled.add("council");
|
|
20011
20068
|
}
|
|
20069
|
+
const primaryModel = getConfigPrimaryModel(config);
|
|
20012
20070
|
const getModelForAgent = (name) => {
|
|
20013
20071
|
if (name === "fixer" && !getAgentOverride(config, "fixer")?.model) {
|
|
20014
20072
|
const librarianOverride = getAgentOverride(config, "librarian")?.model;
|
|
@@ -20019,9 +20077,9 @@ function createAgents(config) {
|
|
|
20019
20077
|
} else {
|
|
20020
20078
|
librarianModel = librarianOverride;
|
|
20021
20079
|
}
|
|
20022
|
-
return librarianModel ?? DEFAULT_MODELS.librarian;
|
|
20080
|
+
return librarianModel ?? primaryModel ?? DEFAULT_MODELS.librarian;
|
|
20023
20081
|
}
|
|
20024
|
-
return DEFAULT_MODELS[name];
|
|
20082
|
+
return primaryModel ?? DEFAULT_MODELS[name];
|
|
20025
20083
|
};
|
|
20026
20084
|
const protoSubAgents = Object.entries(SUBAGENT_FACTORIES).filter(([name]) => !disabled.has(name)).map(([name, factory]) => {
|
|
20027
20085
|
const customPrompts = loadAgentPrompt(name, config?.preset);
|
|
@@ -20063,14 +20121,14 @@ function createAgents(config) {
|
|
|
20063
20121
|
const acp = config?.acpAgents?.[name];
|
|
20064
20122
|
if (!acp)
|
|
20065
20123
|
throw new Error(`ACP agent '${name}' is missing config`);
|
|
20066
|
-
return buildAcpAgentDefinition(name, acp);
|
|
20124
|
+
return buildAcpAgentDefinition(name, acp, primaryModel);
|
|
20067
20125
|
});
|
|
20068
20126
|
const builtInSubAgents = protoSubAgents.map((agent) => {
|
|
20069
20127
|
const override = getAgentOverride(config, agent.name);
|
|
20070
20128
|
if (override) {
|
|
20071
20129
|
applyOverrides(agent, override);
|
|
20072
20130
|
}
|
|
20073
|
-
applyDefaultPermissions(agent, override?.skills);
|
|
20131
|
+
applyDefaultPermissions(agent, override?.skills, config?.disabled_skills);
|
|
20074
20132
|
return agent;
|
|
20075
20133
|
});
|
|
20076
20134
|
const legacyMasterModel = config?.council?._legacyMasterModel;
|
|
@@ -20085,11 +20143,11 @@ function createAgents(config) {
|
|
|
20085
20143
|
if (override) {
|
|
20086
20144
|
applyOverrides(agent, override);
|
|
20087
20145
|
}
|
|
20088
|
-
applyDefaultPermissions(agent, override?.skills);
|
|
20146
|
+
applyDefaultPermissions(agent, override?.skills, config?.disabled_skills);
|
|
20089
20147
|
return agent;
|
|
20090
20148
|
});
|
|
20091
20149
|
const acpSubAgents = protoAcpAgents.map((agent) => {
|
|
20092
|
-
applyDefaultPermissions(agent);
|
|
20150
|
+
applyDefaultPermissions(agent, undefined, config?.disabled_skills);
|
|
20093
20151
|
return agent;
|
|
20094
20152
|
});
|
|
20095
20153
|
const allSubAgents = [
|
|
@@ -20101,7 +20159,7 @@ function createAgents(config) {
|
|
|
20101
20159
|
const orchestratorModel = orchestratorOverride?.model ?? DEFAULT_MODELS.orchestrator;
|
|
20102
20160
|
const orchestratorPrompts = loadAgentPrompt("orchestrator", config?.preset);
|
|
20103
20161
|
const orchestrator = createOrchestratorAgent(orchestratorModel, orchestratorPrompts.prompt, orchestratorPrompts.appendPrompt, disabled);
|
|
20104
|
-
applyDefaultPermissions(orchestrator, orchestratorOverride?.skills);
|
|
20162
|
+
applyDefaultPermissions(orchestrator, orchestratorOverride?.skills, config?.disabled_skills);
|
|
20105
20163
|
if (orchestratorOverride) {
|
|
20106
20164
|
applyOverrides(orchestrator, orchestratorOverride);
|
|
20107
20165
|
}
|
|
@@ -21024,7 +21082,15 @@ class CouncilManager {
|
|
|
21024
21082
|
const body = {
|
|
21025
21083
|
agent: options.agent,
|
|
21026
21084
|
model: modelRef,
|
|
21027
|
-
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
|
+
},
|
|
21028
21094
|
parts: [{ type: "text", text: options.promptText }]
|
|
21029
21095
|
};
|
|
21030
21096
|
if (options.variant) {
|
|
@@ -23551,6 +23617,7 @@ var AGENT_PREFIX = {
|
|
|
23551
23617
|
class BackgroundJobBoard {
|
|
23552
23618
|
jobs = new Map;
|
|
23553
23619
|
counters = new Map;
|
|
23620
|
+
terminalStateListener;
|
|
23554
23621
|
maxReusablePerAgent;
|
|
23555
23622
|
readContextMinLines;
|
|
23556
23623
|
readContextMaxFiles;
|
|
@@ -23559,6 +23626,9 @@ class BackgroundJobBoard {
|
|
|
23559
23626
|
this.readContextMinLines = options.readContextMinLines ?? 10;
|
|
23560
23627
|
this.readContextMaxFiles = options.readContextMaxFiles ?? 8;
|
|
23561
23628
|
}
|
|
23629
|
+
setTerminalStateListener(listener) {
|
|
23630
|
+
this.terminalStateListener = listener;
|
|
23631
|
+
}
|
|
23562
23632
|
registerLaunch(input) {
|
|
23563
23633
|
const now = input.now ?? Date.now();
|
|
23564
23634
|
const existing = this.jobs.get(input.taskID);
|
|
@@ -23616,6 +23686,7 @@ class BackgroundJobBoard {
|
|
|
23616
23686
|
}
|
|
23617
23687
|
const now = input.now ?? Date.now();
|
|
23618
23688
|
const terminal = TERMINAL_STATES.has(input.state);
|
|
23689
|
+
const notifyTerminal = terminal && !TERMINAL_STATES.has(existing.state);
|
|
23619
23690
|
const updated = {
|
|
23620
23691
|
...existing,
|
|
23621
23692
|
state: input.state,
|
|
@@ -23630,6 +23701,8 @@ class BackgroundJobBoard {
|
|
|
23630
23701
|
};
|
|
23631
23702
|
this.jobs.set(input.taskID, updated);
|
|
23632
23703
|
this.trimReusable(input.taskID);
|
|
23704
|
+
if (notifyTerminal)
|
|
23705
|
+
this.terminalStateListener?.(input.taskID);
|
|
23633
23706
|
return updated;
|
|
23634
23707
|
}
|
|
23635
23708
|
updateFromStatusOutput(output) {
|
|
@@ -23694,6 +23767,7 @@ class BackgroundJobBoard {
|
|
|
23694
23767
|
if (TERMINAL_STATES.has(existing.state))
|
|
23695
23768
|
return existing;
|
|
23696
23769
|
}
|
|
23770
|
+
const notifyTerminal = !TERMINAL_STATES.has(existing.state) && existing.state !== "reconciled";
|
|
23697
23771
|
const summary = normalizeCancelReason(reason);
|
|
23698
23772
|
const updated = {
|
|
23699
23773
|
...existing,
|
|
@@ -23709,6 +23783,8 @@ class BackgroundJobBoard {
|
|
|
23709
23783
|
lastStatusError: undefined
|
|
23710
23784
|
};
|
|
23711
23785
|
this.jobs.set(taskID, updated);
|
|
23786
|
+
if (notifyTerminal)
|
|
23787
|
+
this.terminalStateListener?.(taskID);
|
|
23712
23788
|
return updated;
|
|
23713
23789
|
}
|
|
23714
23790
|
get(taskID) {
|
|
@@ -24161,7 +24237,7 @@ function createFilterAvailableSkillsHook(_ctx, config) {
|
|
|
24161
24237
|
return cached;
|
|
24162
24238
|
}
|
|
24163
24239
|
const configuredSkills = getAgentOverride(config, agentName)?.skills;
|
|
24164
|
-
const permissionRules = getSkillPermissionsForAgent(agentName, configuredSkills);
|
|
24240
|
+
const permissionRules = getSkillPermissionsForAgent(agentName, configuredSkills, config.disabled_skills);
|
|
24165
24241
|
permissionRulesByAgent.set(agentName, permissionRules);
|
|
24166
24242
|
return permissionRules;
|
|
24167
24243
|
};
|
|
@@ -25435,26 +25511,25 @@ function slugify(value) {
|
|
|
25435
25511
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
|
|
25436
25512
|
}
|
|
25437
25513
|
function extractHistorySection(document) {
|
|
25438
|
-
const marker =
|
|
25439
|
-
|
|
25440
|
-
|
|
25441
|
-
|
|
25442
|
-
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();
|
|
25443
25519
|
}
|
|
25444
25520
|
function extractSummarySection(document) {
|
|
25445
25521
|
const marker = `## Current spec
|
|
25446
25522
|
|
|
25447
25523
|
`;
|
|
25448
|
-
const historyMarker = `
|
|
25449
|
-
|
|
25450
|
-
## Q&A history`;
|
|
25451
25524
|
const start = document.indexOf(marker);
|
|
25452
25525
|
if (start < 0) {
|
|
25453
25526
|
return "";
|
|
25454
25527
|
}
|
|
25455
25528
|
const summaryStart = start + marker.length;
|
|
25456
|
-
const
|
|
25457
|
-
|
|
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();
|
|
25458
25533
|
}
|
|
25459
25534
|
function extractTitle(document) {
|
|
25460
25535
|
const match = document.match(/^#\s+(.+)$/m);
|
|
@@ -25463,11 +25538,19 @@ function extractTitle(document) {
|
|
|
25463
25538
|
function buildInterviewDocument(idea, summary, history, meta) {
|
|
25464
25539
|
const normalizedSummary = summary.trim() || "Waiting for interview answers.";
|
|
25465
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"];
|
|
25466
25545
|
const frontmatter = meta?.sessionID ? [
|
|
25467
25546
|
"---",
|
|
25468
25547
|
`sessionID: ${meta.sessionID}`,
|
|
25469
25548
|
`baseMessageCount: ${meta.baseMessageCount ?? 0}`,
|
|
25470
|
-
`updatedAt: ${
|
|
25549
|
+
`updatedAt: ${now.toISOString()}`,
|
|
25550
|
+
`version: 1.0`,
|
|
25551
|
+
`date_created: ${dateStr}`,
|
|
25552
|
+
`owner: ${owner}`,
|
|
25553
|
+
`tags: [${tags.join(", ")}]`,
|
|
25471
25554
|
"---",
|
|
25472
25555
|
""
|
|
25473
25556
|
].join(`
|
|
@@ -25549,6 +25632,55 @@ A: ${answer.answer.trim()}` : null;
|
|
|
25549
25632
|
baseMessageCount: record.baseMessageCount
|
|
25550
25633
|
}), "utf8");
|
|
25551
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
|
+
}
|
|
25552
25684
|
|
|
25553
25685
|
// src/interview/helpers.ts
|
|
25554
25686
|
function sendJson(response, status, value) {
|
|
@@ -26038,6 +26170,65 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
26038
26170
|
<style>
|
|
26039
26171
|
${sharedStyles()}
|
|
26040
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
|
+
}
|
|
26041
26232
|
h1 { font-size: 32px; font-weight: 600; letter-spacing: -0.02em; margin-bottom: 12px; line-height: 1.2; }
|
|
26042
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; }
|
|
26043
26234
|
h3 { font-size: 18px; font-weight: 500; margin-bottom: 16px; line-height: 1.4; }
|
|
@@ -26403,9 +26594,102 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
26403
26594
|
background: rgba(52,211,153,0.05);
|
|
26404
26595
|
}
|
|
26405
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; }
|
|
26406
26689
|
</style>
|
|
26407
26690
|
</head>
|
|
26408
26691
|
<body>
|
|
26692
|
+
<nav id="tocSidebar" class="toc-sidebar"></nav>
|
|
26409
26693
|
<div class="wrap">
|
|
26410
26694
|
<a href="/" class="back-link">← All Interviews</a>
|
|
26411
26695
|
<div class="brand-header">
|
|
@@ -26446,6 +26730,11 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
26446
26730
|
<div class="status-text" id="loadingText">Processing...</div>
|
|
26447
26731
|
</div>
|
|
26448
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
|
+
|
|
26449
26738
|
<script>
|
|
26450
26739
|
${clipboardHelperJs()}
|
|
26451
26740
|
const interviewId = ${JSON.stringify(interviewId).replace(/</g, "\\u003c")};
|
|
@@ -26815,21 +27104,94 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
26815
27104
|
return paragraphs.map(p => \`<p>\${p.replace(/\\n/g, '<br>')}</p>\`).join('');
|
|
26816
27105
|
}
|
|
26817
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
|
+
|
|
26818
27135
|
function renderCompletedView(data) {
|
|
26819
27136
|
const container = document.getElementById('questions');
|
|
26820
|
-
const { spec, qaPairs } = parseDocument(data.document);
|
|
26821
27137
|
const frag = document.createDocumentFragment();
|
|
26822
27138
|
|
|
26823
|
-
|
|
26824
|
-
if (
|
|
27139
|
+
const blocks = data.blocks || [];
|
|
27140
|
+
if (blocks.length > 0) {
|
|
26825
27141
|
const specLabel = document.createElement('div');
|
|
26826
27142
|
specLabel.className = 'section-label';
|
|
26827
|
-
specLabel.textContent = '
|
|
27143
|
+
specLabel.textContent = 'Interactive Specifications (11 Sections)';
|
|
26828
27144
|
frag.appendChild(specLabel);
|
|
26829
|
-
|
|
26830
|
-
|
|
26831
|
-
|
|
26832
|
-
|
|
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
|
+
}
|
|
26833
27195
|
}
|
|
26834
27196
|
|
|
26835
27197
|
// Q&A section
|
|
@@ -26838,6 +27200,7 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
26838
27200
|
qaLabel.textContent = 'Q&A History';
|
|
26839
27201
|
frag.appendChild(qaLabel);
|
|
26840
27202
|
|
|
27203
|
+
const { qaPairs } = parseDocument(data.document);
|
|
26841
27204
|
if (!qaPairs.length) {
|
|
26842
27205
|
const empty = document.createElement('p');
|
|
26843
27206
|
empty.className = 'qa-empty';
|
|
@@ -26886,6 +27249,103 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
26886
27249
|
container.replaceChildren(frag);
|
|
26887
27250
|
}
|
|
26888
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
|
+
|
|
26889
27349
|
function renderQuestions(questions) {
|
|
26890
27350
|
const sig = JSON.stringify([questions, state.data?.mode]);
|
|
26891
27351
|
const container = document.getElementById('questions');
|
|
@@ -27042,6 +27502,19 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
27042
27502
|
|
|
27043
27503
|
renderQuestions(data.questions || []);
|
|
27044
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
|
+
}
|
|
27045
27518
|
}
|
|
27046
27519
|
|
|
27047
27520
|
async function refresh() {
|
|
@@ -27152,19 +27625,65 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
27152
27625
|
document.getElementById('moreQuestionsBtn').addEventListener('click', () => sendNudge('more-questions'));
|
|
27153
27626
|
document.getElementById('confirmCompleteBtn').addEventListener('click', () => sendNudge('confirm-complete'));
|
|
27154
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
|
+
|
|
27155
27667
|
function schedulePoll() {
|
|
27156
|
-
|
|
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 () => {
|
|
27157
27675
|
try { await refresh(); } catch (_) {}
|
|
27158
|
-
|
|
27159
|
-
|
|
27160
|
-
|
|
27676
|
+
pollFallbackTimer = null;
|
|
27677
|
+
if (!sseConnected) {
|
|
27678
|
+
schedulePoll();
|
|
27679
|
+
}
|
|
27161
27680
|
}, 2500);
|
|
27162
27681
|
}
|
|
27163
27682
|
|
|
27164
27683
|
refresh().catch((error) => {
|
|
27165
27684
|
document.getElementById('submitStatus').textContent = error.message || 'Failed to load interview.';
|
|
27166
27685
|
});
|
|
27167
|
-
|
|
27686
|
+
connectSse();
|
|
27168
27687
|
</script>
|
|
27169
27688
|
</body>
|
|
27170
27689
|
</html>`;
|
|
@@ -27233,6 +27752,51 @@ function createDashboardServer(config) {
|
|
|
27233
27752
|
let baseUrl = null;
|
|
27234
27753
|
const sessions = new Map;
|
|
27235
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
|
+
}
|
|
27236
27800
|
const TERMINAL_MODES = new Set([
|
|
27237
27801
|
"abandoned",
|
|
27238
27802
|
"completed",
|
|
@@ -27391,7 +27955,9 @@ function createDashboardServer(config) {
|
|
|
27391
27955
|
pendingAnswers: null,
|
|
27392
27956
|
lastUpdatedAt: fm.updatedAt ? new Date(fm.updatedAt).getTime() : Date.now(),
|
|
27393
27957
|
filePath: path14.join(interviewDir, entry),
|
|
27394
|
-
nudgeAction: null
|
|
27958
|
+
nudgeAction: null,
|
|
27959
|
+
pendingBlockComment: null,
|
|
27960
|
+
pendingChatMessage: null
|
|
27395
27961
|
});
|
|
27396
27962
|
if (!sessions.has(fm.sessionID)) {
|
|
27397
27963
|
sessions.set(fm.sessionID, {
|
|
@@ -27572,7 +28138,9 @@ function createDashboardServer(config) {
|
|
|
27572
28138
|
pendingAnswers: null,
|
|
27573
28139
|
lastUpdatedAt: Date.now(),
|
|
27574
28140
|
filePath: "",
|
|
27575
|
-
nudgeAction: null
|
|
28141
|
+
nudgeAction: null,
|
|
28142
|
+
pendingBlockComment: null,
|
|
28143
|
+
pendingChatMessage: null
|
|
27576
28144
|
});
|
|
27577
28145
|
dedupRecovered(interviewId, stateCache);
|
|
27578
28146
|
fileCache = null;
|
|
@@ -27583,6 +28151,48 @@ function createDashboardServer(config) {
|
|
|
27583
28151
|
});
|
|
27584
28152
|
return;
|
|
27585
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
|
+
}
|
|
27586
28196
|
if (request.method === "POST" && pathname.startsWith("/api/interviews/") && pathname.endsWith("/state")) {
|
|
27587
28197
|
const interviewId = pathname.replace("/api/interviews/", "").replace("/state", "");
|
|
27588
28198
|
if (!interviewId || !isValidId(interviewId)) {
|
|
@@ -27609,10 +28219,15 @@ function createDashboardServer(config) {
|
|
|
27609
28219
|
existing.questions = state.questions;
|
|
27610
28220
|
if (state.filePath)
|
|
27611
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;
|
|
27612
28226
|
existing.lastUpdatedAt = Date.now();
|
|
27613
28227
|
dedupRecovered(interviewId, stateCache);
|
|
28228
|
+
broadcastSse(interviewId, existing);
|
|
27614
28229
|
} else {
|
|
27615
|
-
|
|
28230
|
+
const entry = {
|
|
27616
28231
|
interviewId,
|
|
27617
28232
|
sessionID: state.sessionID ?? "",
|
|
27618
28233
|
idea: state.idea ?? "",
|
|
@@ -27623,8 +28238,14 @@ function createDashboardServer(config) {
|
|
|
27623
28238
|
pendingAnswers: null,
|
|
27624
28239
|
lastUpdatedAt: Date.now(),
|
|
27625
28240
|
filePath: state.filePath ?? "",
|
|
27626
|
-
nudgeAction: null
|
|
27627
|
-
|
|
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);
|
|
27628
28249
|
}
|
|
27629
28250
|
sendJson(response, 200, { status: "ok" });
|
|
27630
28251
|
return;
|
|
@@ -27682,7 +28303,8 @@ function createDashboardServer(config) {
|
|
|
27682
28303
|
questions: entry.questions,
|
|
27683
28304
|
document,
|
|
27684
28305
|
lastUpdatedAt: entry.lastUpdatedAt,
|
|
27685
|
-
nudgeAction: entry.nudgeAction
|
|
28306
|
+
nudgeAction: entry.nudgeAction,
|
|
28307
|
+
blocks: parseSpecBlocks(document)
|
|
27686
28308
|
});
|
|
27687
28309
|
return;
|
|
27688
28310
|
}
|
|
@@ -27717,12 +28339,12 @@ function createDashboardServer(config) {
|
|
|
27717
28339
|
sendJson(response, 200, { status: "ok" });
|
|
27718
28340
|
return;
|
|
27719
28341
|
}
|
|
27720
|
-
if (request.method === "
|
|
28342
|
+
if (request.method === "POST" && pathname.startsWith("/api/interviews/") && pathname.endsWith("/block-comment")) {
|
|
27721
28343
|
if (!isAuthenticated(request)) {
|
|
27722
28344
|
sendJson(response, 401, { error: "Unauthorized" });
|
|
27723
28345
|
return;
|
|
27724
28346
|
}
|
|
27725
|
-
const interviewId = pathname.replace("/api/interviews/", "").replace("/
|
|
28347
|
+
const interviewId = pathname.replace("/api/interviews/", "").replace("/block-comment", "");
|
|
27726
28348
|
if (!isValidId(interviewId)) {
|
|
27727
28349
|
sendJson(response, 400, { error: "Invalid interview ID" });
|
|
27728
28350
|
return;
|
|
@@ -27732,21 +28354,32 @@ function createDashboardServer(config) {
|
|
|
27732
28354
|
sendJson(response, 404, { error: "Interview not found" });
|
|
27733
28355
|
return;
|
|
27734
28356
|
}
|
|
27735
|
-
|
|
27736
|
-
|
|
27737
|
-
|
|
28357
|
+
let body;
|
|
28358
|
+
try {
|
|
28359
|
+
body = await readJsonBody(request);
|
|
28360
|
+
} catch {
|
|
28361
|
+
sendJson(response, 400, { error: "Invalid JSON" });
|
|
28362
|
+
return;
|
|
27738
28363
|
}
|
|
27739
|
-
|
|
27740
|
-
|
|
27741
|
-
|
|
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" });
|
|
27742
28375
|
return;
|
|
27743
28376
|
}
|
|
27744
|
-
if (request.method === "
|
|
28377
|
+
if (request.method === "GET" && pathname.startsWith("/api/interviews/") && pathname.endsWith("/block-comment")) {
|
|
27745
28378
|
if (!isAuthenticated(request)) {
|
|
27746
28379
|
sendJson(response, 401, { error: "Unauthorized" });
|
|
27747
28380
|
return;
|
|
27748
28381
|
}
|
|
27749
|
-
const interviewId = pathname.replace("/api/interviews/", "").replace("/
|
|
28382
|
+
const interviewId = pathname.replace("/api/interviews/", "").replace("/block-comment", "");
|
|
27750
28383
|
if (!isValidId(interviewId)) {
|
|
27751
28384
|
sendJson(response, 400, { error: "Invalid interview ID" });
|
|
27752
28385
|
return;
|
|
@@ -27756,10 +28389,113 @@ function createDashboardServer(config) {
|
|
|
27756
28389
|
sendJson(response, 404, { error: "Interview not found" });
|
|
27757
28390
|
return;
|
|
27758
28391
|
}
|
|
27759
|
-
|
|
27760
|
-
|
|
27761
|
-
|
|
27762
|
-
}
|
|
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
|
|
28477
|
+
});
|
|
28478
|
+
return;
|
|
28479
|
+
}
|
|
28480
|
+
if (request.method === "POST" && pathname.startsWith("/api/interviews/") && pathname.endsWith("/nudge")) {
|
|
28481
|
+
if (!isAuthenticated(request)) {
|
|
28482
|
+
sendJson(response, 401, { error: "Unauthorized" });
|
|
28483
|
+
return;
|
|
28484
|
+
}
|
|
28485
|
+
const interviewId = pathname.replace("/api/interviews/", "").replace("/nudge", "");
|
|
28486
|
+
if (!isValidId(interviewId)) {
|
|
28487
|
+
sendJson(response, 400, { error: "Invalid interview ID" });
|
|
28488
|
+
return;
|
|
28489
|
+
}
|
|
28490
|
+
const entry = stateCache.get(interviewId);
|
|
28491
|
+
if (!entry) {
|
|
28492
|
+
sendJson(response, 404, { error: "Interview not found" });
|
|
28493
|
+
return;
|
|
28494
|
+
}
|
|
28495
|
+
let body;
|
|
28496
|
+
try {
|
|
28497
|
+
body = await readJsonBody(request);
|
|
28498
|
+
} catch {
|
|
27763
28499
|
sendJson(response, 400, { error: "Invalid JSON" });
|
|
27764
28500
|
return;
|
|
27765
28501
|
}
|
|
@@ -27886,9 +28622,20 @@ function createDashboardServer(config) {
|
|
|
27886
28622
|
entry.pendingAnswers ??= existing.pendingAnswers;
|
|
27887
28623
|
if (existing.nudgeAction)
|
|
27888
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
|
+
}
|
|
27889
28635
|
}
|
|
27890
28636
|
stateCache.set(entry.interviewId, entry);
|
|
27891
28637
|
dedupRecovered(entry.interviewId, stateCache);
|
|
28638
|
+
broadcastSse(entry.interviewId, entry);
|
|
27892
28639
|
},
|
|
27893
28640
|
getState: (id) => stateCache.get(id),
|
|
27894
28641
|
storeAnswers: (id, answers) => {
|
|
@@ -27916,6 +28663,22 @@ function createDashboardServer(config) {
|
|
|
27916
28663
|
entry.nudgeAction = null;
|
|
27917
28664
|
return action;
|
|
27918
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
|
+
},
|
|
27919
28682
|
authToken,
|
|
27920
28683
|
discoverSessionDirectories,
|
|
27921
28684
|
addManualFolder: (dir) => {
|
|
@@ -28103,6 +28866,50 @@ function createInterviewServer(deps) {
|
|
|
28103
28866
|
}
|
|
28104
28867
|
return;
|
|
28105
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
|
+
}
|
|
28106
28913
|
const nudgeMatch = pathname.match(/^\/api\/interviews\/([^/]+)\/nudge$/);
|
|
28107
28914
|
if (request.method === "POST" && nudgeMatch) {
|
|
28108
28915
|
try {
|
|
@@ -28217,6 +29024,39 @@ function normalizeQuestion(value, index) {
|
|
|
28217
29024
|
suggested: typeof result.data.suggested === "string" && result.data.suggested.trim().length > 0 ? result.data.suggested.trim() : undefined
|
|
28218
29025
|
};
|
|
28219
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
|
+
}
|
|
28220
29060
|
function flattenMessage(message) {
|
|
28221
29061
|
return (message.parts ?? []).map((part) => part.text ?? "").join(`
|
|
28222
29062
|
`).trim();
|
|
@@ -28233,8 +29073,14 @@ function parseAssistantState(text, maxQuestions = 2) {
|
|
|
28233
29073
|
if (!match) {
|
|
28234
29074
|
return { state: null };
|
|
28235
29075
|
}
|
|
29076
|
+
let rawJson = match[1].trim();
|
|
28236
29077
|
try {
|
|
28237
|
-
|
|
29078
|
+
JSON.parse(rawJson);
|
|
29079
|
+
} catch {
|
|
29080
|
+
rawJson = repairJsonNewlines(rawJson);
|
|
29081
|
+
}
|
|
29082
|
+
try {
|
|
29083
|
+
const raw = JSON.parse(rawJson);
|
|
28238
29084
|
const parsed = RawInterviewStateSchema.parse(raw);
|
|
28239
29085
|
const summary = typeof parsed.summary === "string" ? parsed.summary.trim() : "";
|
|
28240
29086
|
const title = typeof parsed.title === "string" && parsed.title.trim().length > 0 ? parsed.title.trim() : undefined;
|
|
@@ -28292,17 +29138,67 @@ ${suggested}`;
|
|
|
28292
29138
|
|
|
28293
29139
|
`);
|
|
28294
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
|
+
`;
|
|
28295
29189
|
function buildKickoffPrompt(idea, maxQuestions) {
|
|
28296
29190
|
return [
|
|
28297
29191
|
"You are running an interview q&a session for the user inside their repository.",
|
|
28298
29192
|
`Initial idea: ${idea}`,
|
|
29193
|
+
`Goal: Iteratively generate and populate a highly structured Specification document.`,
|
|
29194
|
+
SPECIFICATION_TEMPLATE_GUIDELINE,
|
|
28299
29195
|
`Clarify the idea through short rounds of at most ${maxQuestions} questions at a time.`,
|
|
28300
29196
|
"When useful, each question may include 2 to 4 answer options and one suggested option.",
|
|
28301
29197
|
"Be practical. Focus on the highest-ambiguity and highest-risk decisions first.",
|
|
28302
29198
|
"After any short human-friendly preface, you MUST include a machine-readable block in this exact format:",
|
|
28303
29199
|
"<interview_state>",
|
|
28304
29200
|
"{",
|
|
28305
|
-
' "summary": "
|
|
29201
|
+
' "summary": "Full specification markdown (strictly matching the 11 section titles above)",',
|
|
28306
29202
|
' "title": "concise-kebab-case-title-for-filename",',
|
|
28307
29203
|
' "questions": [',
|
|
28308
29204
|
" {",
|
|
@@ -28316,7 +29212,7 @@ function buildKickoffPrompt(idea, maxQuestions) {
|
|
|
28316
29212
|
"</interview_state>",
|
|
28317
29213
|
"Rules:",
|
|
28318
29214
|
`- Return 0 to ${maxQuestions} questions.`,
|
|
28319
|
-
"- 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.",
|
|
28320
29216
|
`- Do not ask more than ${maxQuestions} questions in one round.`,
|
|
28321
29217
|
'- Provide a concise "title" field (kebab-case, 3-6 words) suitable for a filename.'
|
|
28322
29218
|
].join(`
|
|
@@ -28326,12 +29222,13 @@ function buildResumePrompt(document, maxQuestions) {
|
|
|
28326
29222
|
return [
|
|
28327
29223
|
"Resume the interview from this existing markdown document.",
|
|
28328
29224
|
"Use the current spec and Q&A history as ground truth so far.",
|
|
29225
|
+
SPECIFICATION_TEMPLATE_GUIDELINE,
|
|
28329
29226
|
"Do not restart from scratch.",
|
|
28330
29227
|
"",
|
|
28331
29228
|
document,
|
|
28332
29229
|
"",
|
|
28333
29230
|
`Ask the next highest-value clarifying questions, up to ${maxQuestions} at a time.`,
|
|
28334
|
-
"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.",
|
|
28335
29232
|
"Return the same <interview_state> JSON block format as before."
|
|
28336
29233
|
].join(`
|
|
28337
29234
|
`);
|
|
@@ -28341,12 +29238,13 @@ function buildAnswerPrompt(answers, questions, maxQuestions) {
|
|
|
28341
29238
|
`);
|
|
28342
29239
|
return [
|
|
28343
29240
|
"Continue the same interview.",
|
|
29241
|
+
SPECIFICATION_TEMPLATE_GUIDELINE,
|
|
28344
29242
|
"These were the active questions:",
|
|
28345
29243
|
formatQuestionContext(questions),
|
|
28346
29244
|
"The user answered:",
|
|
28347
29245
|
answerText,
|
|
28348
|
-
"Now update
|
|
28349
|
-
`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.`,
|
|
28350
29248
|
"Return the same <interview_state> JSON block format as before."
|
|
28351
29249
|
].join(`
|
|
28352
29250
|
|
|
@@ -28406,6 +29304,7 @@ function createInterviewService(ctx, config, deps) {
|
|
|
28406
29304
|
const browserOpener = deps?.openBrowser ?? openBrowser;
|
|
28407
29305
|
const activeInterviewIds = new Map;
|
|
28408
29306
|
const interviewsById = new Map;
|
|
29307
|
+
const activeSyncs = new Map;
|
|
28409
29308
|
const sessionBusy = new Map;
|
|
28410
29309
|
const sessionModel = new Map;
|
|
28411
29310
|
const browserOpened = new Set;
|
|
@@ -28478,6 +29377,19 @@ function createInterviewService(ctx, config, deps) {
|
|
|
28478
29377
|
});
|
|
28479
29378
|
return result.data;
|
|
28480
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
|
+
}
|
|
28481
29393
|
function isUserVisibleMessage(message) {
|
|
28482
29394
|
return !(message.parts ?? []).some((part) => hasInternalInitiatorMarker(part));
|
|
28483
29395
|
}
|
|
@@ -28546,8 +29458,19 @@ function createInterviewService(ctx, config, deps) {
|
|
|
28546
29458
|
}
|
|
28547
29459
|
return record;
|
|
28548
29460
|
}
|
|
28549
|
-
|
|
28550
|
-
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);
|
|
28551
29474
|
const interviewMessages = allMessages.slice(interview.baseMessageCount).filter(isUserVisibleMessage);
|
|
28552
29475
|
const parsed = findLatestAssistantState(interviewMessages, maxQuestions);
|
|
28553
29476
|
const existingDocument = await readInterviewDocument(interview);
|
|
@@ -28557,17 +29480,24 @@ function createInterviewService(ctx, config, deps) {
|
|
|
28557
29480
|
summary: extractSummarySection(existingDocument) || fallbackState.summary
|
|
28558
29481
|
};
|
|
28559
29482
|
await maybeRenameWithTitle(interview, state.title);
|
|
28560
|
-
|
|
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);
|
|
28561
29490
|
const interviewState = {
|
|
28562
29491
|
interview,
|
|
28563
29492
|
url: `${await ensureServer()}/interview/${interview.id}`,
|
|
28564
29493
|
markdownPath: relativeInterviewPath(ctx.directory, interview.markdownPath),
|
|
28565
|
-
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",
|
|
28566
29495
|
lastParseError: parsed.latestAssistantError,
|
|
28567
29496
|
isBusy: sessionBusy.get(interview.sessionID) === true,
|
|
28568
29497
|
summary: state.summary,
|
|
28569
29498
|
questions: state.questions,
|
|
28570
|
-
document
|
|
29499
|
+
document,
|
|
29500
|
+
blocks
|
|
28571
29501
|
};
|
|
28572
29502
|
if (onStateChange) {
|
|
28573
29503
|
onStateChange(interview.id, interviewState);
|
|
@@ -28793,6 +29723,105 @@ function createInterviewService(ctx, config, deps) {
|
|
|
28793
29723
|
fileCache = { items: sorted, at: Date.now() };
|
|
28794
29724
|
return sorted;
|
|
28795
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
|
+
}
|
|
28796
29825
|
async function handleNudgeAction(interviewId, action) {
|
|
28797
29826
|
const interview = getInterviewById(interviewId);
|
|
28798
29827
|
if (!interview) {
|
|
@@ -28808,12 +29837,17 @@ function createInterviewService(ctx, config, deps) {
|
|
|
28808
29837
|
let promptSent = false;
|
|
28809
29838
|
try {
|
|
28810
29839
|
const state = await getInterviewState(interviewId);
|
|
29840
|
+
const relativePath = relativeInterviewPath(ctx.directory, interview.markdownPath);
|
|
28811
29841
|
let prompt;
|
|
28812
29842
|
if (action === "more-questions") {
|
|
28813
29843
|
prompt = [
|
|
28814
|
-
`
|
|
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
|
+
`\`\`\``,
|
|
28815
29849
|
``,
|
|
28816
|
-
`
|
|
29850
|
+
`The user reviewed the completed interview spec and wants you to continue.`,
|
|
28817
29851
|
``,
|
|
28818
29852
|
`Ask up to ${maxQuestions} new clarifying questions about aspects that are still unclear or underspecified.`,
|
|
28819
29853
|
`Include the structured <interview_state> block with new questions.`
|
|
@@ -28821,9 +29855,13 @@ function createInterviewService(ctx, config, deps) {
|
|
|
28821
29855
|
`);
|
|
28822
29856
|
} else {
|
|
28823
29857
|
prompt = [
|
|
28824
|
-
`
|
|
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
|
+
`\`\`\``,
|
|
28825
29863
|
``,
|
|
28826
|
-
`
|
|
29864
|
+
`The user confirmed the interview spec is complete.`,
|
|
28827
29865
|
``,
|
|
28828
29866
|
`Produce a final, polished version of the full spec document.`,
|
|
28829
29867
|
`Do NOT include any <interview_state> block — just output the final spec as clean markdown.`,
|
|
@@ -28858,6 +29896,8 @@ function createInterviewService(ctx, config, deps) {
|
|
|
28858
29896
|
listInterviewFiles,
|
|
28859
29897
|
listInterviews,
|
|
28860
29898
|
submitAnswers,
|
|
29899
|
+
submitBlockComment,
|
|
29900
|
+
submitChat,
|
|
28861
29901
|
handleNudgeAction
|
|
28862
29902
|
};
|
|
28863
29903
|
}
|
|
@@ -28876,6 +29916,8 @@ function createInterviewManager(ctx, config) {
|
|
|
28876
29916
|
listInterviewFiles: async () => service2.listInterviewFiles(),
|
|
28877
29917
|
listInterviews: () => service2.listInterviews(),
|
|
28878
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),
|
|
28879
29921
|
handleNudgeAction: async (interviewId, action) => service2.handleNudgeAction(interviewId, action),
|
|
28880
29922
|
outputFolder: resolvedOutputPath,
|
|
28881
29923
|
port: 0
|
|
@@ -28909,6 +29951,8 @@ function createInterviewManager(ctx, config) {
|
|
|
28909
29951
|
continue;
|
|
28910
29952
|
pollPendingAnswers(sessionID).catch(() => {});
|
|
28911
29953
|
pollNudgeAction(sessionID).catch(() => {});
|
|
29954
|
+
pollBlockComment(sessionID).catch(() => {});
|
|
29955
|
+
pollChat(sessionID).catch(() => {});
|
|
28912
29956
|
}
|
|
28913
29957
|
}, FALLBACK_POLL_INTERVAL);
|
|
28914
29958
|
fallbackTimer?.unref();
|
|
@@ -28940,7 +29984,9 @@ function createInterviewManager(ctx, config) {
|
|
|
28940
29984
|
pendingAnswers: null,
|
|
28941
29985
|
lastUpdatedAt: Date.now(),
|
|
28942
29986
|
filePath: interview.markdownPath,
|
|
28943
|
-
nudgeAction: null
|
|
29987
|
+
nudgeAction: null,
|
|
29988
|
+
pendingBlockComment: null,
|
|
29989
|
+
pendingChatMessage: null
|
|
28944
29990
|
});
|
|
28945
29991
|
dashboard?.registerSession({
|
|
28946
29992
|
sessionID: interview.sessionID,
|
|
@@ -28966,122 +30012,140 @@ function createInterviewManager(ctx, config) {
|
|
|
28966
30012
|
await new Promise((r) => setTimeout(r, 500));
|
|
28967
30013
|
const retry = await probeDashboard(dashboardPort);
|
|
28968
30014
|
if (!retry.alive) {
|
|
28969
|
-
log("[interview] dashboard
|
|
28970
|
-
|
|
28971
|
-
});
|
|
28972
|
-
const perSessionServer = createInterviewServer({
|
|
28973
|
-
getState: async (interviewId) => service.getInterviewState(interviewId),
|
|
28974
|
-
listInterviewFiles: async () => service.listInterviewFiles(),
|
|
28975
|
-
listInterviews: () => service.listInterviews(),
|
|
28976
|
-
submitAnswers: async (interviewId, answers) => service.submitAnswers(interviewId, answers),
|
|
28977
|
-
handleNudgeAction: async (interviewId, action) => service.handleNudgeAction(interviewId, action),
|
|
28978
|
-
outputFolder: path16.join(ctx.directory, outputFolder),
|
|
28979
|
-
port: 0
|
|
28980
|
-
});
|
|
28981
|
-
service.setBaseUrlResolver(() => perSessionServer.ensureStarted());
|
|
28982
|
-
isDashboard = false;
|
|
28983
|
-
initDone = true;
|
|
28984
|
-
return;
|
|
30015
|
+
log("[interview] dashboard probe failed twice, falling back to local server");
|
|
30016
|
+
throw new Error("Dashboard not reachable");
|
|
28985
30017
|
}
|
|
28986
30018
|
}
|
|
30019
|
+
const creds = await readDashboardAuthFile(dashboardPort);
|
|
30020
|
+
if (!creds) {
|
|
30021
|
+
throw new Error("Dashboard credentials file missing");
|
|
30022
|
+
}
|
|
28987
30023
|
dashboardBaseUrl = `http://127.0.0.1:${dashboardPort}`;
|
|
28988
|
-
|
|
28989
|
-
authToken = auth?.token ?? "";
|
|
30024
|
+
authToken = creds.token;
|
|
28990
30025
|
service.setBaseUrlResolver(() => Promise.resolve(dashboardBaseUrl));
|
|
28991
30026
|
service.setStatePushCallback((id, state) => {
|
|
28992
|
-
|
|
28993
|
-
|
|
28994
|
-
|
|
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
|
+
});
|
|
28995
30032
|
});
|
|
28996
|
-
}
|
|
30033
|
+
}
|
|
28997
30034
|
});
|
|
28998
30035
|
service.setOnInterviewCreated((interview) => {
|
|
28999
|
-
|
|
29000
|
-
|
|
29001
|
-
|
|
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
|
+
});
|
|
29002
30041
|
});
|
|
29003
|
-
}
|
|
30042
|
+
}
|
|
29004
30043
|
});
|
|
29005
|
-
log("[interview] dashboard mode:
|
|
29006
|
-
|
|
30044
|
+
log("[interview] dashboard mode: registered as session client", {
|
|
30045
|
+
dashboardUrl: dashboardBaseUrl
|
|
29007
30046
|
});
|
|
29008
|
-
startFallbackTimer();
|
|
29009
30047
|
}
|
|
29010
30048
|
} catch (err) {
|
|
29011
|
-
log("[interview] dashboard
|
|
29012
|
-
|
|
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
|
|
29013
30062
|
});
|
|
30063
|
+
service.setBaseUrlResolver(() => server.ensureStarted());
|
|
30064
|
+
service.setStatePushCallback(() => {});
|
|
29014
30065
|
} finally {
|
|
29015
30066
|
initDone = true;
|
|
29016
30067
|
}
|
|
29017
30068
|
})();
|
|
29018
|
-
async function
|
|
29019
|
-
if (!initDone)
|
|
30069
|
+
async function ensureInitialized() {
|
|
30070
|
+
if (!initDone) {
|
|
29020
30071
|
await initPromise;
|
|
30072
|
+
}
|
|
29021
30073
|
}
|
|
29022
|
-
async function
|
|
29023
|
-
|
|
29024
|
-
|
|
29025
|
-
registeredSessions.add(sessionID);
|
|
29026
|
-
if (isDashboard)
|
|
30074
|
+
async function pollPendingAnswers(sessionID) {
|
|
30075
|
+
const interviewId = service.getActiveInterviewId(sessionID);
|
|
30076
|
+
if (!interviewId)
|
|
29027
30077
|
return;
|
|
29028
30078
|
try {
|
|
29029
|
-
await fetch(`${dashboardBaseUrl}/api/
|
|
29030
|
-
|
|
29031
|
-
|
|
29032
|
-
|
|
29033
|
-
|
|
29034
|
-
|
|
29035
|
-
|
|
29036
|
-
|
|
29037
|
-
|
|
29038
|
-
});
|
|
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
|
+
}
|
|
29039
30088
|
} catch (err) {
|
|
29040
|
-
log("[interview] failed
|
|
30089
|
+
log("[interview] failed polling pending answers:", {
|
|
29041
30090
|
error: err instanceof Error ? err.message : String(err)
|
|
29042
30091
|
});
|
|
29043
30092
|
}
|
|
29044
30093
|
}
|
|
29045
|
-
async function
|
|
30094
|
+
async function pollNudgeAction(sessionID) {
|
|
29046
30095
|
const interviewId = service.getActiveInterviewId(sessionID);
|
|
29047
30096
|
if (!interviewId)
|
|
29048
30097
|
return;
|
|
29049
30098
|
try {
|
|
29050
|
-
const
|
|
29051
|
-
|
|
29052
|
-
|
|
29053
|
-
|
|
29054
|
-
|
|
29055
|
-
|
|
29056
|
-
|
|
29057
|
-
interviewId,
|
|
29058
|
-
|
|
29059
|
-
});
|
|
29060
|
-
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
|
+
}
|
|
29061
30108
|
} catch (err) {
|
|
29062
|
-
log("[interview] failed
|
|
30109
|
+
log("[interview] failed polling nudge action:", {
|
|
29063
30110
|
error: err instanceof Error ? err.message : String(err)
|
|
29064
30111
|
});
|
|
29065
30112
|
}
|
|
29066
30113
|
}
|
|
29067
|
-
async function
|
|
30114
|
+
async function pollBlockComment(sessionID) {
|
|
29068
30115
|
const interviewId = service.getActiveInterviewId(sessionID);
|
|
29069
30116
|
if (!interviewId)
|
|
29070
30117
|
return;
|
|
29071
30118
|
try {
|
|
29072
|
-
const
|
|
29073
|
-
|
|
29074
|
-
|
|
29075
|
-
|
|
29076
|
-
|
|
29077
|
-
|
|
29078
|
-
|
|
29079
|
-
interviewId,
|
|
29080
|
-
|
|
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)
|
|
29081
30131
|
});
|
|
29082
|
-
|
|
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
|
+
}
|
|
29083
30147
|
} catch (err) {
|
|
29084
|
-
log("[interview] failed
|
|
30148
|
+
log("[interview] failed polling chat message:", {
|
|
29085
30149
|
error: err instanceof Error ? err.message : String(err)
|
|
29086
30150
|
});
|
|
29087
30151
|
}
|
|
@@ -29089,28 +30153,39 @@ function createInterviewManager(ctx, config) {
|
|
|
29089
30153
|
return {
|
|
29090
30154
|
registerCommand: (c) => service.registerCommand(c),
|
|
29091
30155
|
handleCommandExecuteBefore: async (input, output) => {
|
|
29092
|
-
await
|
|
29093
|
-
|
|
29094
|
-
|
|
29095
|
-
|
|
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();
|
|
29096
30171
|
}
|
|
30172
|
+
await service.handleCommandExecuteBefore(input, output);
|
|
29097
30173
|
},
|
|
29098
30174
|
handleEvent: async (input) => {
|
|
29099
|
-
await
|
|
29100
|
-
await service.handleEvent(input);
|
|
30175
|
+
await ensureInitialized();
|
|
29101
30176
|
const { event } = input;
|
|
29102
30177
|
const properties = event.properties ?? {};
|
|
29103
|
-
const sessionID = properties.sessionID;
|
|
29104
|
-
|
|
29105
|
-
|
|
29106
|
-
}
|
|
29107
|
-
if (event.type === "session.status" && sessionID) {
|
|
30178
|
+
const sessionID = properties.sessionID ?? null;
|
|
30179
|
+
await service.handleEvent(input);
|
|
30180
|
+
if (event.type === "session.status") {
|
|
29108
30181
|
const status = properties.status;
|
|
29109
|
-
if (status?.type === "idle") {
|
|
30182
|
+
if (sessionID && status?.type === "idle") {
|
|
29110
30183
|
const interviewId = service.getActiveInterviewId(sessionID);
|
|
29111
|
-
if (!isDashboard) {
|
|
30184
|
+
if (!isDashboard && dashboardBaseUrl) {
|
|
29112
30185
|
await pollPendingAnswers(sessionID);
|
|
29113
30186
|
await pollNudgeAction(sessionID);
|
|
30187
|
+
await pollBlockComment(sessionID);
|
|
30188
|
+
await pollChat(sessionID);
|
|
29114
30189
|
} else if (interviewId && dashboard) {
|
|
29115
30190
|
const pending = dashboard.consumePendingAnswers(interviewId);
|
|
29116
30191
|
if (pending && pending.length > 0) {
|
|
@@ -29128,6 +30203,21 @@ function createInterviewManager(ctx, config) {
|
|
|
29128
30203
|
});
|
|
29129
30204
|
await service.handleNudgeAction(interviewId, nudge);
|
|
29130
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
|
+
}
|
|
29131
30221
|
}
|
|
29132
30222
|
if (interviewId) {
|
|
29133
30223
|
service.getInterviewState(interviewId).catch((err) => {
|
|
@@ -29162,7 +30252,11 @@ function stateToEntry(interviewId, state) {
|
|
|
29162
30252
|
pendingAnswers: null,
|
|
29163
30253
|
lastUpdatedAt: Date.now(),
|
|
29164
30254
|
filePath: state.interview.markdownPath,
|
|
29165
|
-
nudgeAction: null
|
|
30255
|
+
nudgeAction: null,
|
|
30256
|
+
pendingBlockComment: null,
|
|
30257
|
+
pendingChatMessage: null,
|
|
30258
|
+
document: state.document,
|
|
30259
|
+
blocks: state.blocks
|
|
29166
30260
|
};
|
|
29167
30261
|
}
|
|
29168
30262
|
async function pushStateViaHttp(dashboardUrl, token, interviewId, state) {
|
|
@@ -29181,9 +30275,23 @@ async function registerInterviewViaHttp(dashboardUrl, token, interview) {
|
|
|
29181
30275
|
body: JSON.stringify({
|
|
29182
30276
|
interviewId: interview.id,
|
|
29183
30277
|
sessionID: interview.sessionID,
|
|
29184
|
-
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
|
|
29185
30289
|
}),
|
|
29186
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
|
+
});
|
|
29187
30295
|
});
|
|
29188
30296
|
}
|
|
29189
30297
|
// src/mcp/context7.ts
|
|
@@ -29941,7 +31049,6 @@ function startAvailabilityCheck(config) {
|
|
|
29941
31049
|
}
|
|
29942
31050
|
}
|
|
29943
31051
|
// src/multiplexer/session-manager.ts
|
|
29944
|
-
var SESSION_MISSING_GRACE_MS = POLL_INTERVAL_BACKGROUND_MS * 3;
|
|
29945
31052
|
var SHARED_STATE_KEY = Symbol.for("oh-my-opencode-slim.multiplexer-session-manager.state");
|
|
29946
31053
|
function getSharedState() {
|
|
29947
31054
|
const globalWithState = globalThis;
|
|
@@ -29949,7 +31056,8 @@ function getSharedState() {
|
|
|
29949
31056
|
sessions: new Map,
|
|
29950
31057
|
knownSessions: new Map,
|
|
29951
31058
|
spawningSessions: new Set,
|
|
29952
|
-
closingSessions: new Map
|
|
31059
|
+
closingSessions: new Map,
|
|
31060
|
+
deferredIdleCloses: new Set
|
|
29953
31061
|
};
|
|
29954
31062
|
return globalWithState[SHARED_STATE_KEY];
|
|
29955
31063
|
}
|
|
@@ -29963,6 +31071,7 @@ class MultiplexerSessionManager {
|
|
|
29963
31071
|
knownSessions;
|
|
29964
31072
|
spawningSessions;
|
|
29965
31073
|
closingSessions;
|
|
31074
|
+
deferredIdleCloses;
|
|
29966
31075
|
pollInterval;
|
|
29967
31076
|
enabled = false;
|
|
29968
31077
|
constructor(ctx, config, backgroundJobBoard) {
|
|
@@ -29972,6 +31081,7 @@ class MultiplexerSessionManager {
|
|
|
29972
31081
|
this.knownSessions = sharedState.knownSessions;
|
|
29973
31082
|
this.spawningSessions = sharedState.spawningSessions;
|
|
29974
31083
|
this.closingSessions = sharedState.closingSessions;
|
|
31084
|
+
this.deferredIdleCloses = sharedState.deferredIdleCloses;
|
|
29975
31085
|
this.directory = ctx.directory;
|
|
29976
31086
|
const defaultPort = process.env.OPENCODE_PORT ?? "4096";
|
|
29977
31087
|
this.serverUrl = ctx.serverUrl?.toString() ?? `http://localhost:${defaultPort}`;
|
|
@@ -30053,17 +31163,13 @@ class MultiplexerSessionManager {
|
|
|
30053
31163
|
}));
|
|
30054
31164
|
return;
|
|
30055
31165
|
}
|
|
30056
|
-
const now = Date.now();
|
|
30057
31166
|
this.sessions.set(sessionId, {
|
|
30058
31167
|
sessionId,
|
|
30059
31168
|
paneId: paneResult.paneId,
|
|
30060
31169
|
parentId,
|
|
30061
31170
|
title,
|
|
30062
31171
|
directory,
|
|
30063
|
-
ownerInstanceId: this.instanceId
|
|
30064
|
-
createdAt: now,
|
|
30065
|
-
lastSeenAt: now,
|
|
30066
|
-
seenInStatus: false
|
|
31172
|
+
ownerInstanceId: this.instanceId
|
|
30067
31173
|
});
|
|
30068
31174
|
log("[multiplexer-session-manager] pane spawned", {
|
|
30069
31175
|
instanceId: this.instanceId,
|
|
@@ -30098,7 +31204,8 @@ class MultiplexerSessionManager {
|
|
|
30098
31204
|
const sessionId = event.properties?.sessionID;
|
|
30099
31205
|
if (!sessionId)
|
|
30100
31206
|
return;
|
|
30101
|
-
|
|
31207
|
+
const statusType = event.properties?.status?.type;
|
|
31208
|
+
if (statusType === "idle") {
|
|
30102
31209
|
log("[multiplexer-session-manager] session status idle received", {
|
|
30103
31210
|
instanceId: this.instanceId,
|
|
30104
31211
|
sessionId,
|
|
@@ -30110,7 +31217,10 @@ class MultiplexerSessionManager {
|
|
|
30110
31217
|
await this.closeSession(sessionId, "idle");
|
|
30111
31218
|
return;
|
|
30112
31219
|
}
|
|
30113
|
-
if (
|
|
31220
|
+
if (statusType) {
|
|
31221
|
+
this.deferredIdleCloses.delete(sessionId);
|
|
31222
|
+
if (statusType !== "busy")
|
|
31223
|
+
return;
|
|
30114
31224
|
log("[multiplexer-session-manager] session busy event received", {
|
|
30115
31225
|
instanceId: this.instanceId,
|
|
30116
31226
|
sessionId,
|
|
@@ -30138,6 +31248,7 @@ class MultiplexerSessionManager {
|
|
|
30138
31248
|
ownerInstanceId: this.sessions.get(sessionId)?.ownerInstanceId,
|
|
30139
31249
|
backgroundJobState: this.backgroundJobBoard?.get(sessionId)?.state
|
|
30140
31250
|
});
|
|
31251
|
+
this.deferredIdleCloses.delete(sessionId);
|
|
30141
31252
|
await this.closeSession(sessionId, "deleted");
|
|
30142
31253
|
}
|
|
30143
31254
|
startPolling() {
|
|
@@ -30164,7 +31275,6 @@ class MultiplexerSessionManager {
|
|
|
30164
31275
|
}
|
|
30165
31276
|
try {
|
|
30166
31277
|
const allStatuses = await this.fetchSessionStatuses();
|
|
30167
|
-
const now = Date.now();
|
|
30168
31278
|
const sessionsToClose = [];
|
|
30169
31279
|
for (const [sessionId, tracked] of this.sessions.entries()) {
|
|
30170
31280
|
if (tracked.ownerInstanceId !== this.instanceId) {
|
|
@@ -30177,34 +31287,16 @@ class MultiplexerSessionManager {
|
|
|
30177
31287
|
continue;
|
|
30178
31288
|
}
|
|
30179
31289
|
const status = allStatuses[sessionId];
|
|
30180
|
-
|
|
30181
|
-
|
|
30182
|
-
|
|
30183
|
-
|
|
30184
|
-
|
|
30185
|
-
} else if (!tracked.missingSince) {
|
|
30186
|
-
tracked.missingSince = now;
|
|
30187
|
-
}
|
|
30188
|
-
const missingTooLong = !!tracked.missingSince && now - tracked.missingSince >= SESSION_MISSING_GRACE_MS;
|
|
30189
|
-
const shouldKeepRunningBackgroundJob = (isIdle || missingTooLong) && this.isRunningBackgroundJob(sessionId);
|
|
30190
|
-
if (isIdle || missingTooLong) {
|
|
30191
|
-
if (shouldKeepRunningBackgroundJob) {
|
|
30192
|
-
log("[multiplexer-session-manager] keeping running background pane", {
|
|
30193
|
-
instanceId: this.instanceId,
|
|
30194
|
-
sessionId,
|
|
30195
|
-
paneId: tracked.paneId,
|
|
30196
|
-
seenInStatus: tracked.seenInStatus
|
|
30197
|
-
});
|
|
30198
|
-
continue;
|
|
30199
|
-
}
|
|
30200
|
-
sessionsToClose.push({
|
|
30201
|
-
sessionId,
|
|
30202
|
-
reason: isIdle ? "idle" : "missing"
|
|
30203
|
-
});
|
|
31290
|
+
if (!status)
|
|
31291
|
+
continue;
|
|
31292
|
+
if (status.type !== "idle") {
|
|
31293
|
+
this.deferredIdleCloses.delete(sessionId);
|
|
31294
|
+
continue;
|
|
30204
31295
|
}
|
|
31296
|
+
sessionsToClose.push(sessionId);
|
|
30205
31297
|
}
|
|
30206
|
-
for (const
|
|
30207
|
-
await this.closeSession(sessionId,
|
|
31298
|
+
for (const sessionId of sessionsToClose) {
|
|
31299
|
+
await this.closeSession(sessionId, "idle");
|
|
30208
31300
|
}
|
|
30209
31301
|
} catch (err) {
|
|
30210
31302
|
log("[multiplexer-session-manager] poll error", { error: String(err) });
|
|
@@ -30229,6 +31321,7 @@ class MultiplexerSessionManager {
|
|
|
30229
31321
|
async closeSession(sessionId, reason) {
|
|
30230
31322
|
if (reason === "deleted") {
|
|
30231
31323
|
this.knownSessions.delete(sessionId);
|
|
31324
|
+
this.deferredIdleCloses.delete(sessionId);
|
|
30232
31325
|
}
|
|
30233
31326
|
const existingClose = this.closingSessions.get(sessionId);
|
|
30234
31327
|
if (existingClose)
|
|
@@ -30264,6 +31357,7 @@ class MultiplexerSessionManager {
|
|
|
30264
31357
|
});
|
|
30265
31358
|
}
|
|
30266
31359
|
if (reason === "idle" && this.isRunningBackgroundJob(sessionId)) {
|
|
31360
|
+
this.deferredIdleCloses.add(sessionId);
|
|
30267
31361
|
log("[multiplexer-session-manager] close skipped; background job running", {
|
|
30268
31362
|
instanceId: this.instanceId,
|
|
30269
31363
|
sessionId,
|
|
@@ -30273,6 +31367,7 @@ class MultiplexerSessionManager {
|
|
|
30273
31367
|
});
|
|
30274
31368
|
return;
|
|
30275
31369
|
}
|
|
31370
|
+
this.deferredIdleCloses.delete(sessionId);
|
|
30276
31371
|
this.sessions.delete(sessionId);
|
|
30277
31372
|
log("[multiplexer-session-manager] closing session pane", {
|
|
30278
31373
|
instanceId: this.instanceId,
|
|
@@ -30348,18 +31443,15 @@ class MultiplexerSessionManager {
|
|
|
30348
31443
|
}));
|
|
30349
31444
|
return;
|
|
30350
31445
|
}
|
|
30351
|
-
const now = Date.now();
|
|
30352
31446
|
this.sessions.set(sessionId, {
|
|
30353
31447
|
sessionId,
|
|
30354
31448
|
paneId: paneResult.paneId,
|
|
30355
31449
|
parentId: known.parentId,
|
|
30356
31450
|
title: known.title,
|
|
30357
31451
|
directory: known.directory,
|
|
30358
|
-
ownerInstanceId: this.instanceId
|
|
30359
|
-
createdAt: now,
|
|
30360
|
-
lastSeenAt: now,
|
|
30361
|
-
seenInStatus: false
|
|
31452
|
+
ownerInstanceId: this.instanceId
|
|
30362
31453
|
});
|
|
31454
|
+
this.deferredIdleCloses.delete(sessionId);
|
|
30363
31455
|
log("[multiplexer-session-manager] pane respawned on busy", {
|
|
30364
31456
|
instanceId: this.instanceId,
|
|
30365
31457
|
sessionId,
|
|
@@ -30386,6 +31478,13 @@ class MultiplexerSessionManager {
|
|
|
30386
31478
|
isRunningBackgroundJob(sessionId) {
|
|
30387
31479
|
return this.backgroundJobBoard?.get(sessionId)?.state === "running";
|
|
30388
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
|
+
}
|
|
30389
31488
|
async cleanup() {
|
|
30390
31489
|
this.stopPolling();
|
|
30391
31490
|
if (this.closingSessions.size > 0) {
|
|
@@ -30406,6 +31505,7 @@ class MultiplexerSessionManager {
|
|
|
30406
31505
|
this.knownSessions.clear();
|
|
30407
31506
|
this.spawningSessions.clear();
|
|
30408
31507
|
this.closingSessions.clear();
|
|
31508
|
+
this.deferredIdleCloses.clear();
|
|
30409
31509
|
log("[multiplexer-session-manager] cleanup complete");
|
|
30410
31510
|
}
|
|
30411
31511
|
}
|
|
@@ -30657,7 +31757,7 @@ function createAcpRunTool(agents = {}) {
|
|
|
30657
31757
|
agent: z4.string().describe("Configured ACP agent name"),
|
|
30658
31758
|
prompt: z4.string().describe("Task or question to send to the ACP agent"),
|
|
30659
31759
|
cwd: z4.string().optional().describe("Optional absolute working directory override"),
|
|
30660
|
-
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.")
|
|
30661
31761
|
},
|
|
30662
31762
|
async execute(args, ctx) {
|
|
30663
31763
|
if (ctx.agent !== args.agent) {
|
|
@@ -30693,11 +31793,12 @@ function createAcpRunTool(agents = {}) {
|
|
|
30693
31793
|
});
|
|
30694
31794
|
const timeoutMs = args.timeout_ms ?? config.timeoutMs;
|
|
30695
31795
|
let timer;
|
|
30696
|
-
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;
|
|
30697
31797
|
const abort = () => client.close();
|
|
30698
31798
|
ctx.abort.addEventListener("abort", abort, { once: true });
|
|
30699
31799
|
try {
|
|
30700
|
-
|
|
31800
|
+
const run = client.run(args.prompt);
|
|
31801
|
+
return timeout ? await Promise.race([run, timeout]) : await run;
|
|
30701
31802
|
} finally {
|
|
30702
31803
|
if (timer)
|
|
30703
31804
|
clearTimeout(timer);
|
|
@@ -31792,7 +32893,8 @@ function emptySnapshot() {
|
|
|
31792
32893
|
return {
|
|
31793
32894
|
version: 1,
|
|
31794
32895
|
updatedAt: Date.now(),
|
|
31795
|
-
agentModels: {}
|
|
32896
|
+
agentModels: {},
|
|
32897
|
+
agentVariants: {}
|
|
31796
32898
|
};
|
|
31797
32899
|
}
|
|
31798
32900
|
function parseSnapshot(value) {
|
|
@@ -31802,7 +32904,8 @@ function parseSnapshot(value) {
|
|
|
31802
32904
|
return {
|
|
31803
32905
|
version: 1,
|
|
31804
32906
|
updatedAt: typeof parsed.updatedAt === "number" ? parsed.updatedAt : Date.now(),
|
|
31805
|
-
agentModels: parsed.agentModels ?? {}
|
|
32907
|
+
agentModels: parsed.agentModels ?? {},
|
|
32908
|
+
agentVariants: parsed.agentVariants ?? {}
|
|
31806
32909
|
};
|
|
31807
32910
|
}
|
|
31808
32911
|
function readTuiSnapshot() {
|
|
@@ -31836,11 +32939,19 @@ function updateSnapshot(mutator) {
|
|
|
31836
32939
|
function recordTuiAgentModels(input) {
|
|
31837
32940
|
updateSnapshot((snapshot) => {
|
|
31838
32941
|
snapshot.agentModels = { ...input.agentModels };
|
|
32942
|
+
snapshot.agentVariants = { ...input.agentVariants ?? {} };
|
|
31839
32943
|
});
|
|
31840
32944
|
}
|
|
31841
32945
|
function recordTuiAgentModel(input) {
|
|
31842
32946
|
updateSnapshot((snapshot) => {
|
|
31843
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
|
+
}
|
|
31844
32955
|
});
|
|
31845
32956
|
}
|
|
31846
32957
|
|
|
@@ -31912,12 +33023,18 @@ function createPresetManager(ctx, config) {
|
|
|
31912
33023
|
} catch {}
|
|
31913
33024
|
const snapshot = readTuiSnapshot();
|
|
31914
33025
|
const agentModels = { ...snapshot.agentModels };
|
|
33026
|
+
const agentVariants = { ...snapshot.agentVariants };
|
|
31915
33027
|
for (const [agentName, agentConfig] of Object.entries(agentUpdates)) {
|
|
31916
33028
|
if (typeof agentConfig.model === "string") {
|
|
31917
33029
|
agentModels[agentName] = agentConfig.model;
|
|
31918
33030
|
}
|
|
33031
|
+
if (typeof agentConfig.variant === "string") {
|
|
33032
|
+
agentVariants[agentName] = agentConfig.variant;
|
|
33033
|
+
} else {
|
|
33034
|
+
delete agentVariants[agentName];
|
|
33035
|
+
}
|
|
31919
33036
|
}
|
|
31920
|
-
recordTuiAgentModels({ agentModels });
|
|
33037
|
+
recordTuiAgentModels({ agentModels, agentVariants });
|
|
31921
33038
|
activePreset = presetName;
|
|
31922
33039
|
const summaryParts = [];
|
|
31923
33040
|
for (const [name, cfg] of Object.entries(agentUpdates)) {
|
|
@@ -34310,6 +35427,16 @@ function createWebfetchTool(pluginCtx, options = {}) {
|
|
|
34310
35427
|
}
|
|
34311
35428
|
});
|
|
34312
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
|
+
|
|
34313
35440
|
// src/utils/subagent-depth.ts
|
|
34314
35441
|
class SubagentDepthTracker {
|
|
34315
35442
|
depthBySession = new Map;
|
|
@@ -34400,6 +35527,10 @@ async function probeJSDOM() {
|
|
|
34400
35527
|
var OhMyOpenCodeLite = async (ctx) => {
|
|
34401
35528
|
const sessionId = new Date().toISOString().replace(/[-:]/g, "").slice(0, 15);
|
|
34402
35529
|
initLogger(sessionId);
|
|
35530
|
+
if (isPluginDisabledByEnv()) {
|
|
35531
|
+
log("[plugin] disabled by OH_MY_OPENCODE_SLIM_DISABLE");
|
|
35532
|
+
return {};
|
|
35533
|
+
}
|
|
34403
35534
|
let config;
|
|
34404
35535
|
let disabledAgents;
|
|
34405
35536
|
let agentDefs;
|
|
@@ -34432,6 +35563,7 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
34432
35563
|
let cancelTaskTools;
|
|
34433
35564
|
let acpRunTools;
|
|
34434
35565
|
let webfetch;
|
|
35566
|
+
let tools;
|
|
34435
35567
|
let rewriteDisplayNameMentions;
|
|
34436
35568
|
let toolCount = 0;
|
|
34437
35569
|
try {
|
|
@@ -34483,6 +35615,9 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
34483
35615
|
readContextMaxFiles: config.backgroundJobs?.readContextMaxFiles ?? 8
|
|
34484
35616
|
});
|
|
34485
35617
|
multiplexerSessionManager = new MultiplexerSessionManager(ctx, multiplexerConfig, backgroundJobBoard);
|
|
35618
|
+
backgroundJobBoard.setTerminalStateListener((taskID) => {
|
|
35619
|
+
multiplexerSessionManager.retryDeferredIdleClose(taskID);
|
|
35620
|
+
});
|
|
34486
35621
|
autoUpdateChecker = createAutoUpdateCheckerHook(ctx, {
|
|
34487
35622
|
autoUpdate: config.autoUpdate ?? true,
|
|
34488
35623
|
companion: config.companion
|
|
@@ -34515,7 +35650,19 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
34515
35650
|
backgroundJobBoard,
|
|
34516
35651
|
shouldManageSession: (sessionID) => sessionAgentMap.get(sessionID) === "orchestrator"
|
|
34517
35652
|
});
|
|
34518
|
-
|
|
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;
|
|
34519
35666
|
} catch (err) {
|
|
34520
35667
|
log("[plugin] FATAL: init failed", String(err));
|
|
34521
35668
|
await appLog(ctx, "error", `INIT FAILED: ${String(err)}. Report at github.com/alvinunreal/oh-my-opencode-slim/issues/310`);
|
|
@@ -34568,17 +35715,25 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
34568
35715
|
}
|
|
34569
35716
|
}
|
|
34570
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
|
+
}
|
|
34571
35733
|
return {
|
|
34572
35734
|
name: "oh-my-opencode-slim",
|
|
34573
35735
|
agent: agents,
|
|
34574
|
-
tool:
|
|
34575
|
-
...councilTools,
|
|
34576
|
-
...cancelTaskTools,
|
|
34577
|
-
...acpRunTools,
|
|
34578
|
-
webfetch,
|
|
34579
|
-
ast_grep_search,
|
|
34580
|
-
ast_grep_replace
|
|
34581
|
-
},
|
|
35736
|
+
tool: tools,
|
|
34582
35737
|
mcp: mcps,
|
|
34583
35738
|
config: async (opencodeConfig) => {
|
|
34584
35739
|
if (config.setDefaultAgent !== false && !opencodeConfig.default_agent) {
|
|
@@ -34704,14 +35859,22 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
34704
35859
|
}
|
|
34705
35860
|
}
|
|
34706
35861
|
const tuiAgentModels = {};
|
|
35862
|
+
const tuiAgentVariants = {};
|
|
34707
35863
|
for (const agentDef of agentDefs) {
|
|
34708
35864
|
if (agentDef.name === "councillor")
|
|
34709
35865
|
continue;
|
|
34710
35866
|
const entry = configAgent[agentDef.name];
|
|
34711
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;
|
|
34712
35869
|
tuiAgentModels[agentDef.name] = resolvedModel ?? "default";
|
|
35870
|
+
if (resolvedVariant) {
|
|
35871
|
+
tuiAgentVariants[agentDef.name] = resolvedVariant;
|
|
35872
|
+
}
|
|
34713
35873
|
}
|
|
34714
|
-
recordTuiAgentModels({
|
|
35874
|
+
recordTuiAgentModels({
|
|
35875
|
+
agentModels: tuiAgentModels,
|
|
35876
|
+
agentVariants: tuiAgentVariants
|
|
35877
|
+
});
|
|
34715
35878
|
const configMcp = opencodeConfig.mcp;
|
|
34716
35879
|
if (!configMcp) {
|
|
34717
35880
|
opencodeConfig.mcp = { ...mcps };
|
|
@@ -34749,10 +35912,16 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
34749
35912
|
const event = input.event;
|
|
34750
35913
|
if (event.type === "message.updated") {
|
|
34751
35914
|
const info = event.properties?.info;
|
|
34752
|
-
|
|
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);
|
|
34753
35921
|
recordTuiAgentModel({
|
|
34754
|
-
agentName
|
|
34755
|
-
model
|
|
35922
|
+
agentName,
|
|
35923
|
+
model,
|
|
35924
|
+
variant: variant ?? null
|
|
34756
35925
|
});
|
|
34757
35926
|
}
|
|
34758
35927
|
}
|