oh-my-opencode-slim 1.0.0 → 1.0.2
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.md +39 -23
- package/dist/cli/index.js +11 -4
- package/dist/cli/install.d.ts +1 -1
- package/dist/cli/providers.d.ts +4 -4
- package/dist/config/constants.d.ts +1 -1
- package/dist/config/council-schema.d.ts +3 -2
- package/dist/config/schema.d.ts +9 -0
- package/dist/council/council-manager.d.ts +3 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/phase-reminder/index.d.ts +1 -1
- package/dist/hooks/task-session-manager/index.d.ts +36 -0
- package/dist/index.js +863 -75
- package/dist/multiplexer/session-manager.d.ts +4 -0
- package/dist/tools/council.d.ts +2 -2
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/preset-manager.d.ts +27 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/session-manager.d.ts +38 -0
- package/dist/utils/session.d.ts +4 -4
- package/dist/utils/system-collapse.d.ts +6 -0
- package/dist/utils/task.d.ts +4 -0
- package/oh-my-opencode-slim.schema.json +15 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -18302,7 +18302,7 @@ var ALL_AGENT_NAMES = [ORCHESTRATOR_NAME, ...SUBAGENT_NAMES];
|
|
|
18302
18302
|
var PROTECTED_AGENTS = new Set(["orchestrator", "councillor"]);
|
|
18303
18303
|
var DEFAULT_MODELS = {
|
|
18304
18304
|
orchestrator: undefined,
|
|
18305
|
-
oracle: "openai/gpt-5.
|
|
18305
|
+
oracle: "openai/gpt-5.5",
|
|
18306
18306
|
librarian: "openai/gpt-5.4-mini",
|
|
18307
18307
|
explorer: "openai/gpt-5.4-mini",
|
|
18308
18308
|
designer: "openai/gpt-5.4-mini",
|
|
@@ -18316,7 +18316,7 @@ var DEFAULT_TIMEOUT_MS = 2 * 60 * 1000;
|
|
|
18316
18316
|
var MAX_POLL_TIME_MS = 5 * 60 * 1000;
|
|
18317
18317
|
var DEFAULT_MAX_SUBAGENT_DEPTH = 3;
|
|
18318
18318
|
var PHASE_REMINDER_TEXT = `!IMPORTANT! Recall the workflow rules:
|
|
18319
|
-
Understand → choose the best parallelized path based on your agents delegation rules →
|
|
18319
|
+
Understand → choose the best parallelized path based on your capabilities and agents delegation rules → execute → verify.
|
|
18320
18320
|
If delegating, launch the specialist in the same turn you mention it !END!`;
|
|
18321
18321
|
var TMUX_SPAWN_DELAY_MS = 500;
|
|
18322
18322
|
var COUNCILLOR_STAGGER_MS = 250;
|
|
@@ -18378,13 +18378,15 @@ var CouncilConfigSchema = z.object({
|
|
|
18378
18378
|
deprecated.push("master_timeout");
|
|
18379
18379
|
if (data.master_fallback !== undefined)
|
|
18380
18380
|
deprecated.push("master_fallback");
|
|
18381
|
+
const legacyMasterModel = typeof data.master === "object" && data.master !== null && "model" in data.master && typeof data.master.model === "string" ? data.master.model : undefined;
|
|
18381
18382
|
return {
|
|
18382
18383
|
presets: data.presets,
|
|
18383
18384
|
timeout: data.timeout,
|
|
18384
18385
|
default_preset: data.default_preset,
|
|
18385
18386
|
councillor_execution_mode: data.councillor_execution_mode,
|
|
18386
18387
|
councillor_retries: data.councillor_retries,
|
|
18387
|
-
_deprecated: deprecated.length > 0 ? deprecated : undefined
|
|
18388
|
+
_deprecated: deprecated.length > 0 ? deprecated : undefined,
|
|
18389
|
+
_legacyMasterModel: legacyMasterModel
|
|
18388
18390
|
};
|
|
18389
18391
|
});
|
|
18390
18392
|
// src/config/loader.ts
|
|
@@ -18523,6 +18525,9 @@ var InterviewConfigSchema = z2.object({
|
|
|
18523
18525
|
port: z2.number().int().min(0).max(65535).default(0),
|
|
18524
18526
|
dashboard: z2.boolean().default(false)
|
|
18525
18527
|
});
|
|
18528
|
+
var SessionManagerConfigSchema = z2.object({
|
|
18529
|
+
maxSessionsPerAgent: z2.number().int().min(1).max(10).default(2)
|
|
18530
|
+
});
|
|
18526
18531
|
var TodoContinuationConfigSchema = z2.object({
|
|
18527
18532
|
maxContinuations: z2.number().int().min(1).max(50).default(5).describe("Maximum consecutive auto-continuations before stopping to ask user"),
|
|
18528
18533
|
cooldownMs: z2.number().int().min(0).max(30000).default(3000).describe("Delay in ms before auto-continuing (gives user time to abort)"),
|
|
@@ -18564,6 +18569,7 @@ var PluginConfigSchema = z2.object({
|
|
|
18564
18569
|
scoringEngineVersion: z2.enum(["v1", "v2-shadow", "v2"]).optional(),
|
|
18565
18570
|
balanceProviderUsage: z2.boolean().optional(),
|
|
18566
18571
|
showStartupToast: z2.boolean().optional().describe("Show the startup activation toast when OpenCode starts. Defaults to true."),
|
|
18572
|
+
autoUpdate: z2.boolean().optional().describe("Disable automatic installation of plugin updates when false. Defaults to true."),
|
|
18567
18573
|
manualPlan: ManualPlanSchema.optional(),
|
|
18568
18574
|
presets: z2.record(z2.string(), PresetSchema).optional(),
|
|
18569
18575
|
agents: z2.record(z2.string(), AgentOverrideConfigSchema).optional(),
|
|
@@ -18573,6 +18579,7 @@ var PluginConfigSchema = z2.object({
|
|
|
18573
18579
|
tmux: TmuxConfigSchema.optional(),
|
|
18574
18580
|
websearch: WebsearchConfigSchema.optional(),
|
|
18575
18581
|
interview: InterviewConfigSchema.optional(),
|
|
18582
|
+
sessionManager: SessionManagerConfigSchema.optional(),
|
|
18576
18583
|
todoContinuation: TodoContinuationConfigSchema.optional(),
|
|
18577
18584
|
fallback: FailoverConfigSchema.optional(),
|
|
18578
18585
|
council: CouncilConfigSchema.optional()
|
|
@@ -18658,6 +18665,7 @@ function loadPluginConfig(directory) {
|
|
|
18658
18665
|
tmux: deepMerge(config.tmux, projectConfig.tmux),
|
|
18659
18666
|
multiplexer: deepMerge(config.multiplexer, projectConfig.multiplexer),
|
|
18660
18667
|
interview: deepMerge(config.interview, projectConfig.interview),
|
|
18668
|
+
sessionManager: deepMerge(config.sessionManager, projectConfig.sessionManager),
|
|
18661
18669
|
fallback: deepMerge(config.fallback, projectConfig.fallback),
|
|
18662
18670
|
council: deepMerge(config.council, projectConfig.council)
|
|
18663
18671
|
};
|
|
@@ -18929,6 +18937,12 @@ ${enabledParallelExamples}
|
|
|
18929
18937
|
|
|
18930
18938
|
Balance: respect dependencies, avoid parallelizing what must be sequential.
|
|
18931
18939
|
|
|
18940
|
+
### OpenCode subagent execution model
|
|
18941
|
+
- A delegated specialist runs in a separate child session.
|
|
18942
|
+
- Delegation is blocking for the parent at that point: send work out, then continue that line after results return.
|
|
18943
|
+
- Parallel delegation means launching multiple independent child-session branches.
|
|
18944
|
+
- Only parallelize branches that are truly independent; reconcile dependent steps after delegated results come back.
|
|
18945
|
+
|
|
18932
18946
|
## 5. Execute
|
|
18933
18947
|
1. Break complex tasks into todos
|
|
18934
18948
|
2. Fire parallel research/implementation
|
|
@@ -18936,6 +18950,12 @@ Balance: respect dependencies, avoid parallelizing what must be sequential.
|
|
|
18936
18950
|
4. Integrate results
|
|
18937
18951
|
5. Adjust if needed
|
|
18938
18952
|
|
|
18953
|
+
### Session Reuse
|
|
18954
|
+
- Reuse an available specialist session only for clear follow-up work on the same thread.
|
|
18955
|
+
- Prefer a fresh session for unrelated work, even with the same specialist.
|
|
18956
|
+
- If multiple remembered sessions fit, prefer the most recently used matching session.
|
|
18957
|
+
- If reuse is unclear, start a fresh session.
|
|
18958
|
+
|
|
18939
18959
|
### Auto-Continue
|
|
18940
18960
|
When working through multi-step tasks, consider enabling auto-continue to avoid stopping between batches:
|
|
18941
18961
|
- **Enable when:** User requests autonomous/batch work, or you create 4+ todos in a session
|
|
@@ -19630,6 +19650,13 @@ function createAgents(config) {
|
|
|
19630
19650
|
applyDefaultPermissions(agent, override?.skills);
|
|
19631
19651
|
return agent;
|
|
19632
19652
|
});
|
|
19653
|
+
const legacyMasterModel = config?.council?._legacyMasterModel;
|
|
19654
|
+
if (legacyMasterModel) {
|
|
19655
|
+
const councilAgent = builtInSubAgents.find((a) => a.name === "council");
|
|
19656
|
+
if (councilAgent && !getAgentOverride(config, "council")?.model && councilAgent.config.model === DEFAULT_MODELS.council) {
|
|
19657
|
+
councilAgent.config.model = legacyMasterModel;
|
|
19658
|
+
}
|
|
19659
|
+
}
|
|
19633
19660
|
const customSubAgents = protoCustomAgents.map((agent) => {
|
|
19634
19661
|
const override = getAgentOverride(config, agent.name);
|
|
19635
19662
|
if (override) {
|
|
@@ -19819,17 +19846,22 @@ class CouncilManager {
|
|
|
19819
19846
|
depthTracker;
|
|
19820
19847
|
tmuxEnabled;
|
|
19821
19848
|
deprecatedFields;
|
|
19849
|
+
legacyMasterModel;
|
|
19822
19850
|
constructor(ctx, config, depthTracker, tmuxEnabled = false) {
|
|
19823
19851
|
this.client = ctx.client;
|
|
19824
19852
|
this.directory = ctx.directory;
|
|
19825
19853
|
this.config = config;
|
|
19826
19854
|
this.deprecatedFields = config?.council?._deprecated;
|
|
19855
|
+
this.legacyMasterModel = config?.council?._legacyMasterModel;
|
|
19827
19856
|
this.depthTracker = depthTracker;
|
|
19828
19857
|
this.tmuxEnabled = tmuxEnabled;
|
|
19829
19858
|
}
|
|
19830
19859
|
getDeprecatedFields() {
|
|
19831
19860
|
return this.deprecatedFields;
|
|
19832
19861
|
}
|
|
19862
|
+
getLegacyMasterModel() {
|
|
19863
|
+
return this.legacyMasterModel;
|
|
19864
|
+
}
|
|
19833
19865
|
async runCouncil(prompt, presetName, parentSessionId) {
|
|
19834
19866
|
if (this.depthTracker) {
|
|
19835
19867
|
const parentDepth = this.depthTracker.getDepth(parentSessionId);
|
|
@@ -22043,6 +22075,185 @@ function hasInternalInitiatorMarker(part) {
|
|
|
22043
22075
|
}
|
|
22044
22076
|
return part.text.includes(SLIM_INTERNAL_INITIATOR_MARKER);
|
|
22045
22077
|
}
|
|
22078
|
+
// src/utils/session-manager.ts
|
|
22079
|
+
function aliasPrefix(agentType) {
|
|
22080
|
+
switch (agentType) {
|
|
22081
|
+
case "explorer":
|
|
22082
|
+
return "exp";
|
|
22083
|
+
case "librarian":
|
|
22084
|
+
return "lib";
|
|
22085
|
+
case "oracle":
|
|
22086
|
+
return "ora";
|
|
22087
|
+
case "designer":
|
|
22088
|
+
return "des";
|
|
22089
|
+
case "fixer":
|
|
22090
|
+
return "fix";
|
|
22091
|
+
case "observer":
|
|
22092
|
+
return "obs";
|
|
22093
|
+
case "council":
|
|
22094
|
+
return "cnc";
|
|
22095
|
+
case "councillor":
|
|
22096
|
+
return "clr";
|
|
22097
|
+
case "orchestrator":
|
|
22098
|
+
return "orc";
|
|
22099
|
+
}
|
|
22100
|
+
}
|
|
22101
|
+
function normalizeWhitespace(value) {
|
|
22102
|
+
return value.replace(/\s+/g, " ").trim();
|
|
22103
|
+
}
|
|
22104
|
+
function deriveTaskSessionLabel(input) {
|
|
22105
|
+
const preferred = normalizeWhitespace(input.description ?? "");
|
|
22106
|
+
if (preferred) {
|
|
22107
|
+
return preferred.slice(0, 48);
|
|
22108
|
+
}
|
|
22109
|
+
const firstPromptLine = (input.prompt ?? "").split(/\r?\n/).map((line) => normalizeWhitespace(line)).find(Boolean);
|
|
22110
|
+
if (firstPromptLine) {
|
|
22111
|
+
return firstPromptLine.slice(0, 48);
|
|
22112
|
+
}
|
|
22113
|
+
return `recent ${input.agentType} task`;
|
|
22114
|
+
}
|
|
22115
|
+
|
|
22116
|
+
class SessionManager {
|
|
22117
|
+
maxSessionsPerAgent;
|
|
22118
|
+
sessionsByParent = new Map;
|
|
22119
|
+
nextAliasIndexByParent = new Map;
|
|
22120
|
+
orderCounter = 0;
|
|
22121
|
+
constructor(maxSessionsPerAgent) {
|
|
22122
|
+
this.maxSessionsPerAgent = maxSessionsPerAgent;
|
|
22123
|
+
}
|
|
22124
|
+
remember(input) {
|
|
22125
|
+
const now = this.nextOrder();
|
|
22126
|
+
const group = this.getAgentGroup(input.parentSessionId, input.agentType, true);
|
|
22127
|
+
if (!group) {
|
|
22128
|
+
throw new Error("Failed to initialize session group");
|
|
22129
|
+
}
|
|
22130
|
+
const existing = group.find((entry) => entry.taskId === input.taskId);
|
|
22131
|
+
if (existing) {
|
|
22132
|
+
existing.label = input.label;
|
|
22133
|
+
existing.lastUsedAt = this.nextOrder();
|
|
22134
|
+
return existing;
|
|
22135
|
+
}
|
|
22136
|
+
const remembered = {
|
|
22137
|
+
alias: this.nextAlias(input.parentSessionId, input.agentType),
|
|
22138
|
+
taskId: input.taskId,
|
|
22139
|
+
agentType: input.agentType,
|
|
22140
|
+
label: input.label,
|
|
22141
|
+
createdAt: now,
|
|
22142
|
+
lastUsedAt: now
|
|
22143
|
+
};
|
|
22144
|
+
group.push(remembered);
|
|
22145
|
+
this.trimGroup(group);
|
|
22146
|
+
return remembered;
|
|
22147
|
+
}
|
|
22148
|
+
markUsed(parentSessionId, agentType, key) {
|
|
22149
|
+
const group = this.getAgentGroup(parentSessionId, agentType, false);
|
|
22150
|
+
const match = group?.find((entry) => entry.alias === key || entry.taskId === key);
|
|
22151
|
+
if (match) {
|
|
22152
|
+
match.lastUsedAt = this.nextOrder();
|
|
22153
|
+
}
|
|
22154
|
+
}
|
|
22155
|
+
resolve(parentSessionId, agentType, key) {
|
|
22156
|
+
const group = this.getAgentGroup(parentSessionId, agentType, false);
|
|
22157
|
+
return group?.find((entry) => entry.alias === key || entry.taskId === key);
|
|
22158
|
+
}
|
|
22159
|
+
drop(parentSessionId, agentType, key) {
|
|
22160
|
+
const group = this.getAgentGroup(parentSessionId, agentType, false);
|
|
22161
|
+
if (!group)
|
|
22162
|
+
return;
|
|
22163
|
+
const next = group.filter((entry) => entry.alias !== key && entry.taskId !== key);
|
|
22164
|
+
this.setAgentGroup(parentSessionId, agentType, next);
|
|
22165
|
+
}
|
|
22166
|
+
dropTask(taskId) {
|
|
22167
|
+
for (const [parentSessionId, groups] of this.sessionsByParent.entries()) {
|
|
22168
|
+
for (const [agentType, group] of groups.entries()) {
|
|
22169
|
+
const next = group.filter((entry) => entry.taskId !== taskId);
|
|
22170
|
+
this.setAgentGroup(parentSessionId, agentType, next);
|
|
22171
|
+
}
|
|
22172
|
+
}
|
|
22173
|
+
}
|
|
22174
|
+
clearParent(parentSessionId) {
|
|
22175
|
+
this.sessionsByParent.delete(parentSessionId);
|
|
22176
|
+
this.nextAliasIndexByParent.delete(parentSessionId);
|
|
22177
|
+
}
|
|
22178
|
+
formatForPrompt(parentSessionId) {
|
|
22179
|
+
const groups = this.sessionsByParent.get(parentSessionId);
|
|
22180
|
+
if (!groups || groups.size === 0)
|
|
22181
|
+
return;
|
|
22182
|
+
const lines = [...groups.entries()].map(([agentType, entries]) => [
|
|
22183
|
+
agentType,
|
|
22184
|
+
[...entries].sort((a, b) => b.lastUsedAt - a.lastUsedAt)
|
|
22185
|
+
]).filter(([, entries]) => entries.length > 0).sort((a, b) => b[1][0].lastUsedAt - a[1][0].lastUsedAt).map(([agentType, entries]) => `- ${agentType}: ${entries.map((entry) => `${entry.alias} ${entry.label}`).join("; ")}`);
|
|
22186
|
+
if (lines.length === 0)
|
|
22187
|
+
return;
|
|
22188
|
+
return [
|
|
22189
|
+
"### Resumable Sessions",
|
|
22190
|
+
"Reuse only for clear continuation of the same thread. Otherwise start fresh.",
|
|
22191
|
+
"",
|
|
22192
|
+
...lines
|
|
22193
|
+
].join(`
|
|
22194
|
+
`);
|
|
22195
|
+
}
|
|
22196
|
+
getAgentGroup(parentSessionId, agentType, create) {
|
|
22197
|
+
let groups = this.sessionsByParent.get(parentSessionId);
|
|
22198
|
+
if (!groups && create) {
|
|
22199
|
+
groups = new Map;
|
|
22200
|
+
this.sessionsByParent.set(parentSessionId, groups);
|
|
22201
|
+
}
|
|
22202
|
+
let group = groups?.get(agentType);
|
|
22203
|
+
if (!group && create && groups) {
|
|
22204
|
+
group = [];
|
|
22205
|
+
groups.set(agentType, group);
|
|
22206
|
+
}
|
|
22207
|
+
return group;
|
|
22208
|
+
}
|
|
22209
|
+
setAgentGroup(parentSessionId, agentType, entries) {
|
|
22210
|
+
const groups = this.sessionsByParent.get(parentSessionId);
|
|
22211
|
+
if (!groups)
|
|
22212
|
+
return;
|
|
22213
|
+
if (entries.length === 0) {
|
|
22214
|
+
groups.delete(agentType);
|
|
22215
|
+
if (groups.size === 0) {
|
|
22216
|
+
this.sessionsByParent.delete(parentSessionId);
|
|
22217
|
+
this.nextAliasIndexByParent.delete(parentSessionId);
|
|
22218
|
+
}
|
|
22219
|
+
return;
|
|
22220
|
+
}
|
|
22221
|
+
groups.set(agentType, entries);
|
|
22222
|
+
}
|
|
22223
|
+
nextAlias(parentSessionId, agentType) {
|
|
22224
|
+
let counters = this.nextAliasIndexByParent.get(parentSessionId);
|
|
22225
|
+
if (!counters) {
|
|
22226
|
+
counters = new Map;
|
|
22227
|
+
this.nextAliasIndexByParent.set(parentSessionId, counters);
|
|
22228
|
+
}
|
|
22229
|
+
const next = (counters.get(agentType) ?? 0) + 1;
|
|
22230
|
+
counters.set(agentType, next);
|
|
22231
|
+
return `${aliasPrefix(agentType)}-${next}`;
|
|
22232
|
+
}
|
|
22233
|
+
trimGroup(group) {
|
|
22234
|
+
group.sort((a, b) => b.lastUsedAt - a.lastUsedAt);
|
|
22235
|
+
if (group.length > this.maxSessionsPerAgent) {
|
|
22236
|
+
group.length = this.maxSessionsPerAgent;
|
|
22237
|
+
}
|
|
22238
|
+
}
|
|
22239
|
+
nextOrder() {
|
|
22240
|
+
this.orderCounter += 1;
|
|
22241
|
+
return this.orderCounter;
|
|
22242
|
+
}
|
|
22243
|
+
}
|
|
22244
|
+
// src/utils/task.ts
|
|
22245
|
+
function parseTaskIdFromTaskOutput(output) {
|
|
22246
|
+
const lines = output.split(/\r?\n/);
|
|
22247
|
+
for (const line of lines) {
|
|
22248
|
+
const trimmed = line.trim();
|
|
22249
|
+
const match = /^task_id:\s*([^\s()]+)(?:\s*\(.*)?$/.exec(trimmed);
|
|
22250
|
+
if (!match) {
|
|
22251
|
+
continue;
|
|
22252
|
+
}
|
|
22253
|
+
return match[1];
|
|
22254
|
+
}
|
|
22255
|
+
return;
|
|
22256
|
+
}
|
|
22046
22257
|
// src/utils/zip-extractor.ts
|
|
22047
22258
|
import { spawnSync } from "node:child_process";
|
|
22048
22259
|
import { release } from "node:os";
|
|
@@ -22358,6 +22569,8 @@ var RATE_LIMIT_PATTERNS = [
|
|
|
22358
22569
|
/too many requests/i,
|
|
22359
22570
|
/quota.?exceeded/i,
|
|
22360
22571
|
/usage.?exceeded/i,
|
|
22572
|
+
/ExceededBudget/i,
|
|
22573
|
+
/over.?budget/i,
|
|
22361
22574
|
/usage limit/i,
|
|
22362
22575
|
/overloaded/i,
|
|
22363
22576
|
/resource.?exhausted/i,
|
|
@@ -22436,7 +22649,7 @@ class ForegroundFallbackManager {
|
|
|
22436
22649
|
if (!props?.sessionID || props.status?.type !== "retry")
|
|
22437
22650
|
break;
|
|
22438
22651
|
const msg = props.status.message?.toLowerCase() ?? "";
|
|
22439
|
-
if (msg.includes("rate limit") || msg.includes("usage limit") || msg.includes("usage exceeded") || msg.includes("quota exceeded") || msg.includes("high concurrency") || msg.includes("reduce concurrency")) {
|
|
22652
|
+
if (msg.includes("rate limit") || msg.includes("usage limit") || msg.includes("usage exceeded") || msg.includes("quota exceeded") || msg.includes("exceededbudget") || msg.includes("over budget") || msg.includes("high concurrency") || msg.includes("reduce concurrency")) {
|
|
22440
22653
|
await this.tryFallback(props.sessionID);
|
|
22441
22654
|
}
|
|
22442
22655
|
break;
|
|
@@ -22613,31 +22826,51 @@ function extFromMime(mime) {
|
|
|
22613
22826
|
function sanitizeFilename(name) {
|
|
22614
22827
|
return name.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
22615
22828
|
}
|
|
22616
|
-
function
|
|
22829
|
+
function cleanupAllSessions(saveDir) {
|
|
22617
22830
|
const now = Date.now();
|
|
22618
|
-
|
|
22619
|
-
lastCleanupByDir.set(dir, now);
|
|
22620
|
-
}
|
|
22621
|
-
const lastCleanup = lastCleanupByDir.get(dir) ?? 0;
|
|
22831
|
+
const lastCleanup = lastCleanupByDir.get(saveDir) ?? 0;
|
|
22622
22832
|
if (now - lastCleanup < CLEANUP_INTERVAL)
|
|
22623
22833
|
return;
|
|
22624
|
-
lastCleanupByDir.set(
|
|
22834
|
+
lastCleanupByDir.set(saveDir, now);
|
|
22835
|
+
const maxAge = 60 * 60 * 1000;
|
|
22836
|
+
const dirsToScan = [];
|
|
22625
22837
|
try {
|
|
22626
|
-
const
|
|
22627
|
-
|
|
22628
|
-
|
|
22629
|
-
|
|
22630
|
-
|
|
22631
|
-
|
|
22632
|
-
|
|
22633
|
-
|
|
22634
|
-
|
|
22635
|
-
|
|
22636
|
-
rmdirSync(dir);
|
|
22637
|
-
lastCleanupByDir.delete(dir);
|
|
22638
|
-
} catch {}
|
|
22838
|
+
for (const entry of readdirSync2(saveDir, { withFileTypes: true })) {
|
|
22839
|
+
const fp = join7(saveDir, entry.name);
|
|
22840
|
+
if (entry.isDirectory()) {
|
|
22841
|
+
dirsToScan.push(fp);
|
|
22842
|
+
} else {
|
|
22843
|
+
try {
|
|
22844
|
+
if (now - statSync3(fp).mtimeMs > maxAge)
|
|
22845
|
+
unlinkSync2(fp);
|
|
22846
|
+
} catch {}
|
|
22847
|
+
}
|
|
22639
22848
|
}
|
|
22640
22849
|
} catch {}
|
|
22850
|
+
for (const dir of dirsToScan) {
|
|
22851
|
+
try {
|
|
22852
|
+
let isEmpty = true;
|
|
22853
|
+
let allRemoved = true;
|
|
22854
|
+
for (const f of readdirSync2(dir)) {
|
|
22855
|
+
isEmpty = false;
|
|
22856
|
+
const fp = join7(dir, f);
|
|
22857
|
+
try {
|
|
22858
|
+
if (now - statSync3(fp).mtimeMs > maxAge) {
|
|
22859
|
+
unlinkSync2(fp);
|
|
22860
|
+
} else {
|
|
22861
|
+
allRemoved = false;
|
|
22862
|
+
}
|
|
22863
|
+
} catch {
|
|
22864
|
+
allRemoved = false;
|
|
22865
|
+
}
|
|
22866
|
+
}
|
|
22867
|
+
if (!isEmpty && allRemoved) {
|
|
22868
|
+
try {
|
|
22869
|
+
rmdirSync(dir);
|
|
22870
|
+
} catch {}
|
|
22871
|
+
}
|
|
22872
|
+
} catch {}
|
|
22873
|
+
}
|
|
22641
22874
|
}
|
|
22642
22875
|
function writeUniqueFile(dir, name, data, log2) {
|
|
22643
22876
|
const ext = extname(name);
|
|
@@ -22680,6 +22913,7 @@ function processImageAttachments(args) {
|
|
|
22680
22913
|
} catch (e) {
|
|
22681
22914
|
log2(`[image-hook] failed to create image directory: ${e}`);
|
|
22682
22915
|
}
|
|
22916
|
+
cleanupAllSessions(saveDir);
|
|
22683
22917
|
for (const msg of messages) {
|
|
22684
22918
|
if (msg.info.role !== "user")
|
|
22685
22919
|
continue;
|
|
@@ -22693,7 +22927,6 @@ function processImageAttachments(args) {
|
|
|
22693
22927
|
} catch (e) {
|
|
22694
22928
|
log2(`[image-hook] failed to create target image directory: ${e}`);
|
|
22695
22929
|
}
|
|
22696
|
-
cleanupOldImages(targetDir, saveDir);
|
|
22697
22930
|
const savedPaths = [];
|
|
22698
22931
|
for (const p of imageParts) {
|
|
22699
22932
|
const url = p.url;
|
|
@@ -22846,6 +23079,154 @@ function createPostFileToolNudgeHook(options = {}) {
|
|
|
22846
23079
|
}
|
|
22847
23080
|
};
|
|
22848
23081
|
}
|
|
23082
|
+
// src/hooks/task-session-manager/index.ts
|
|
23083
|
+
var AGENT_NAME_SET = new Set([
|
|
23084
|
+
"orchestrator",
|
|
23085
|
+
"oracle",
|
|
23086
|
+
"designer",
|
|
23087
|
+
"explorer",
|
|
23088
|
+
"librarian",
|
|
23089
|
+
"fixer",
|
|
23090
|
+
"observer",
|
|
23091
|
+
"council",
|
|
23092
|
+
"councillor"
|
|
23093
|
+
]);
|
|
23094
|
+
var MAX_PENDING_TASK_CALLS = 100;
|
|
23095
|
+
function isAgentName(value) {
|
|
23096
|
+
return typeof value === "string" && AGENT_NAME_SET.has(value);
|
|
23097
|
+
}
|
|
23098
|
+
function isObjectRecord(value) {
|
|
23099
|
+
return typeof value === "object" && value !== null;
|
|
23100
|
+
}
|
|
23101
|
+
function createTaskSessionManagerHook(_ctx, options) {
|
|
23102
|
+
const sessionManager = new SessionManager(options.maxSessionsPerAgent);
|
|
23103
|
+
const pendingCalls = new Map;
|
|
23104
|
+
const pendingCallOrder = [];
|
|
23105
|
+
function isMissingRememberedSessionError(output) {
|
|
23106
|
+
const firstLine = output.split(/\r?\n/, 1)[0]?.trim().toLowerCase() ?? "";
|
|
23107
|
+
return firstLine.startsWith("[error]") && firstLine.includes("session") && (firstLine.includes("not found") || firstLine.includes("no session"));
|
|
23108
|
+
}
|
|
23109
|
+
function rememberPendingCall(call) {
|
|
23110
|
+
const existingIndex = pendingCallOrder.indexOf(call.callId);
|
|
23111
|
+
if (existingIndex >= 0) {
|
|
23112
|
+
pendingCallOrder.splice(existingIndex, 1);
|
|
23113
|
+
}
|
|
23114
|
+
pendingCalls.set(call.callId, call);
|
|
23115
|
+
pendingCallOrder.push(call.callId);
|
|
23116
|
+
while (pendingCallOrder.length > MAX_PENDING_TASK_CALLS) {
|
|
23117
|
+
const evictedCallId = pendingCallOrder.shift();
|
|
23118
|
+
if (!evictedCallId) {
|
|
23119
|
+
break;
|
|
23120
|
+
}
|
|
23121
|
+
pendingCalls.delete(evictedCallId);
|
|
23122
|
+
}
|
|
23123
|
+
}
|
|
23124
|
+
function takePendingCall(callId) {
|
|
23125
|
+
if (!callId)
|
|
23126
|
+
return;
|
|
23127
|
+
const pending = pendingCalls.get(callId);
|
|
23128
|
+
pendingCalls.delete(callId);
|
|
23129
|
+
const orderIndex = pendingCallOrder.indexOf(callId);
|
|
23130
|
+
if (orderIndex >= 0) {
|
|
23131
|
+
pendingCallOrder.splice(orderIndex, 1);
|
|
23132
|
+
}
|
|
23133
|
+
return pending;
|
|
23134
|
+
}
|
|
23135
|
+
return {
|
|
23136
|
+
"tool.execute.before": async (input, output) => {
|
|
23137
|
+
if (input.tool.toLowerCase() !== "task")
|
|
23138
|
+
return;
|
|
23139
|
+
if (!input.sessionID || !options.shouldManageSession(input.sessionID)) {
|
|
23140
|
+
return;
|
|
23141
|
+
}
|
|
23142
|
+
if (!isObjectRecord(output.args))
|
|
23143
|
+
return;
|
|
23144
|
+
const args = output.args;
|
|
23145
|
+
if (!isAgentName(args.subagent_type))
|
|
23146
|
+
return;
|
|
23147
|
+
const label = deriveTaskSessionLabel({
|
|
23148
|
+
description: typeof args.description === "string" ? args.description : undefined,
|
|
23149
|
+
prompt: typeof args.prompt === "string" ? args.prompt : undefined,
|
|
23150
|
+
agentType: args.subagent_type
|
|
23151
|
+
});
|
|
23152
|
+
if (input.callID) {
|
|
23153
|
+
rememberPendingCall({
|
|
23154
|
+
callId: input.callID,
|
|
23155
|
+
parentSessionId: input.sessionID,
|
|
23156
|
+
agentType: args.subagent_type,
|
|
23157
|
+
label
|
|
23158
|
+
});
|
|
23159
|
+
}
|
|
23160
|
+
if (typeof args.task_id !== "string" || args.task_id.trim() === "") {
|
|
23161
|
+
return;
|
|
23162
|
+
}
|
|
23163
|
+
const requested = args.task_id.trim();
|
|
23164
|
+
const remembered = sessionManager.resolve(input.sessionID, args.subagent_type, requested);
|
|
23165
|
+
if (!remembered) {
|
|
23166
|
+
delete args.task_id;
|
|
23167
|
+
return;
|
|
23168
|
+
}
|
|
23169
|
+
args.task_id = remembered.taskId;
|
|
23170
|
+
sessionManager.markUsed(input.sessionID, args.subagent_type, remembered.taskId);
|
|
23171
|
+
if (input.callID) {
|
|
23172
|
+
rememberPendingCall({
|
|
23173
|
+
callId: input.callID,
|
|
23174
|
+
parentSessionId: input.sessionID,
|
|
23175
|
+
agentType: args.subagent_type,
|
|
23176
|
+
label,
|
|
23177
|
+
resumedTaskId: remembered.taskId
|
|
23178
|
+
});
|
|
23179
|
+
}
|
|
23180
|
+
},
|
|
23181
|
+
"tool.execute.after": async (input, output) => {
|
|
23182
|
+
if (input.tool.toLowerCase() !== "task")
|
|
23183
|
+
return;
|
|
23184
|
+
const pending = takePendingCall(input.callID);
|
|
23185
|
+
if (!pending || typeof output.output !== "string")
|
|
23186
|
+
return;
|
|
23187
|
+
const taskId = parseTaskIdFromTaskOutput(output.output);
|
|
23188
|
+
if (!taskId) {
|
|
23189
|
+
if (pending.resumedTaskId && isMissingRememberedSessionError(output.output)) {
|
|
23190
|
+
sessionManager.drop(pending.parentSessionId, pending.agentType, pending.resumedTaskId);
|
|
23191
|
+
}
|
|
23192
|
+
return;
|
|
23193
|
+
}
|
|
23194
|
+
if (pending.resumedTaskId && pending.resumedTaskId !== taskId) {
|
|
23195
|
+
sessionManager.drop(pending.parentSessionId, pending.agentType, pending.resumedTaskId);
|
|
23196
|
+
}
|
|
23197
|
+
sessionManager.remember({
|
|
23198
|
+
parentSessionId: pending.parentSessionId,
|
|
23199
|
+
taskId,
|
|
23200
|
+
agentType: pending.agentType,
|
|
23201
|
+
label: pending.label
|
|
23202
|
+
});
|
|
23203
|
+
},
|
|
23204
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
23205
|
+
if (!input.sessionID || !options.shouldManageSession(input.sessionID)) {
|
|
23206
|
+
return;
|
|
23207
|
+
}
|
|
23208
|
+
const reminder = sessionManager.formatForPrompt(input.sessionID);
|
|
23209
|
+
if (!reminder)
|
|
23210
|
+
return;
|
|
23211
|
+
output.system.push(reminder);
|
|
23212
|
+
},
|
|
23213
|
+
event: async (input) => {
|
|
23214
|
+
if (input.event.type !== "session.deleted")
|
|
23215
|
+
return;
|
|
23216
|
+
const sessionId = input.event.properties?.info?.id ?? input.event.properties?.sessionID;
|
|
23217
|
+
if (!sessionId)
|
|
23218
|
+
return;
|
|
23219
|
+
sessionManager.clearParent(sessionId);
|
|
23220
|
+
sessionManager.dropTask(sessionId);
|
|
23221
|
+
for (const [callId, pending] of pendingCalls.entries()) {
|
|
23222
|
+
if (pending.parentSessionId !== sessionId) {
|
|
23223
|
+
continue;
|
|
23224
|
+
}
|
|
23225
|
+
takePendingCall(callId);
|
|
23226
|
+
}
|
|
23227
|
+
}
|
|
23228
|
+
};
|
|
23229
|
+
}
|
|
22849
23230
|
// src/hooks/todo-continuation/index.ts
|
|
22850
23231
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
22851
23232
|
|
|
@@ -23730,6 +24111,7 @@ async function readJsonBody(request) {
|
|
|
23730
24111
|
}
|
|
23731
24112
|
|
|
23732
24113
|
// src/interview/ui.ts
|
|
24114
|
+
var BRAND_LOGO_URL = "https://ohmyopencodeslim.com/android-chrome-512x512.png";
|
|
23733
24115
|
function escapeHtml(value) {
|
|
23734
24116
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
23735
24117
|
}
|
|
@@ -23827,12 +24209,8 @@ function sharedStyles() {
|
|
|
23827
24209
|
}
|
|
23828
24210
|
.footer { margin-top: 32px; text-align: center; font-size: 13px; color: rgba(255,255,255,0.4); }`;
|
|
23829
24211
|
}
|
|
23830
|
-
function
|
|
23831
|
-
return `<
|
|
23832
|
-
<rect x="12" y="12" width="120" height="120" rx="32" fill="rgba(255,255,255,0.08)" stroke="rgba(255,255,255,0.18)" stroke-width="2"/>
|
|
23833
|
-
<path d="M50 48h18c16 0 26 10 26 24s-10 24-26 24H50z" fill="none" stroke="white" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
|
23834
|
-
<path d="M74 48h20c10 0 18 8 18 18v12c0 10-8 18-18 18H74" fill="none" stroke="white" stroke-width="8" stroke-linecap="round" stroke-linejoin="round" opacity="0.65"/>
|
|
23835
|
-
</svg>`;
|
|
24212
|
+
function brandImage(size) {
|
|
24213
|
+
return `<img class="brand-mark" src="${BRAND_LOGO_URL}" alt="Oh My Opencode Slim" width="${size}" height="${size}" />`;
|
|
23836
24214
|
}
|
|
23837
24215
|
function renderDashboardPage(interviews, files, outputFolder) {
|
|
23838
24216
|
const activeHtml = interviews.length === 0 ? "" : interviews.map((item) => {
|
|
@@ -24041,7 +24419,7 @@ function renderDashboardPage(interviews, files, outputFolder) {
|
|
|
24041
24419
|
<body>
|
|
24042
24420
|
<div class="wrap">
|
|
24043
24421
|
<div class="brand-header">
|
|
24044
|
-
${
|
|
24422
|
+
${brandImage(96)}
|
|
24045
24423
|
<h1>Interviews</h1>
|
|
24046
24424
|
<p class="muted">${totalCount} item${totalCount === 1 ? "" : "s"}</p>
|
|
24047
24425
|
</div>
|
|
@@ -24242,6 +24620,37 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
24242
24620
|
}
|
|
24243
24621
|
|
|
24244
24622
|
.options { display: flex; flex-direction: column; gap: 10px; margin-bottom: 20px; }
|
|
24623
|
+
.question-hint {
|
|
24624
|
+
display: flex;
|
|
24625
|
+
flex-wrap: wrap;
|
|
24626
|
+
gap: 8px;
|
|
24627
|
+
margin: -4px 0 16px;
|
|
24628
|
+
color: rgba(255,255,255,0.5);
|
|
24629
|
+
font-size: 13px;
|
|
24630
|
+
line-height: 1.5;
|
|
24631
|
+
transition: color 0.2s ease;
|
|
24632
|
+
}
|
|
24633
|
+
.active-question .question-hint {
|
|
24634
|
+
color: rgba(255,255,255,0.78);
|
|
24635
|
+
}
|
|
24636
|
+
.hint-chip {
|
|
24637
|
+
display: inline-flex;
|
|
24638
|
+
align-items: center;
|
|
24639
|
+
gap: 6px;
|
|
24640
|
+
padding: 4px 10px;
|
|
24641
|
+
border-radius: 999px;
|
|
24642
|
+
background: rgba(255,255,255,0.06);
|
|
24643
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
24644
|
+
}
|
|
24645
|
+
.hint-chip kbd {
|
|
24646
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
24647
|
+
font-size: 12px;
|
|
24648
|
+
padding: 2px 6px;
|
|
24649
|
+
border-radius: 6px;
|
|
24650
|
+
background: rgba(255,255,255,0.12);
|
|
24651
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
24652
|
+
color: rgba(255,255,255,0.95);
|
|
24653
|
+
}
|
|
24245
24654
|
|
|
24246
24655
|
.option {
|
|
24247
24656
|
border: 1px solid rgba(255,255,255,0.1);
|
|
@@ -24521,7 +24930,7 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
24521
24930
|
<div class="wrap">
|
|
24522
24931
|
<a href="/" class="back-link">← All Interviews</a>
|
|
24523
24932
|
<div class="brand-header">
|
|
24524
|
-
${
|
|
24933
|
+
${brandImage(144)}
|
|
24525
24934
|
</div>
|
|
24526
24935
|
<h1 id="idea">Connecting...</h1>
|
|
24527
24936
|
<p class="muted" id="summary">Preparing interview session</p>
|
|
@@ -24562,7 +24971,15 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
24562
24971
|
${clipboardHelperJs()}
|
|
24563
24972
|
const interviewId = ${JSON.stringify(interviewId).replace(/</g, "\\u003c")};
|
|
24564
24973
|
const resumeSlug = ${JSON.stringify(resumeSlug).replace(/</g, "\\u003c")};
|
|
24565
|
-
const state = {
|
|
24974
|
+
const state = {
|
|
24975
|
+
data: null,
|
|
24976
|
+
answers: {},
|
|
24977
|
+
activeQuestionIndex: 0,
|
|
24978
|
+
lastQuestionIds: [],
|
|
24979
|
+
lastSig: null,
|
|
24980
|
+
customMode: {},
|
|
24981
|
+
isSubmitting: false,
|
|
24982
|
+
};
|
|
24566
24983
|
|
|
24567
24984
|
function updateSubmitButton() {
|
|
24568
24985
|
const button = document.getElementById('submitButton');
|
|
@@ -24575,7 +24992,11 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
24575
24992
|
const allAnswered = questions.every((question) =>
|
|
24576
24993
|
(state.answers[question.id] || '').trim().length > 0,
|
|
24577
24994
|
);
|
|
24578
|
-
button.disabled =
|
|
24995
|
+
button.disabled =
|
|
24996
|
+
state.data.isBusy ||
|
|
24997
|
+
state.isSubmitting ||
|
|
24998
|
+
!questions.length ||
|
|
24999
|
+
!allAnswered;
|
|
24579
25000
|
const hideSubmit = ['completed', 'session-disconnected'];
|
|
24580
25001
|
button.style.display = hideSubmit.includes(state.data.mode) ? 'none' : '';
|
|
24581
25002
|
|
|
@@ -24726,6 +25147,54 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
24726
25147
|
});
|
|
24727
25148
|
}
|
|
24728
25149
|
|
|
25150
|
+
function scrollToActiveQuestion(behavior) {
|
|
25151
|
+
const questions = state.data?.questions || [];
|
|
25152
|
+
const activeQ = questions[state.activeQuestionIndex];
|
|
25153
|
+
if (!activeQ) return;
|
|
25154
|
+
|
|
25155
|
+
const wrapper = document.getElementById('question-' + activeQ.id);
|
|
25156
|
+
if (wrapper) {
|
|
25157
|
+
wrapper.scrollIntoView({ behavior, block: 'center' });
|
|
25158
|
+
}
|
|
25159
|
+
}
|
|
25160
|
+
|
|
25161
|
+
function syncActiveQuestionIndex(questions) {
|
|
25162
|
+
if (!questions.length) {
|
|
25163
|
+
state.activeQuestionIndex = 0;
|
|
25164
|
+
state.lastQuestionIds = [];
|
|
25165
|
+
return;
|
|
25166
|
+
}
|
|
25167
|
+
|
|
25168
|
+
const nextQuestionIds = questions.map((question) => question.id);
|
|
25169
|
+
const previousQuestionIds = state.lastQuestionIds || [];
|
|
25170
|
+
const activeQuestionId = previousQuestionIds[state.activeQuestionIndex];
|
|
25171
|
+
const nextActiveIndex = activeQuestionId
|
|
25172
|
+
? nextQuestionIds.indexOf(activeQuestionId)
|
|
25173
|
+
: -1;
|
|
25174
|
+
|
|
25175
|
+
if (nextActiveIndex >= 0) {
|
|
25176
|
+
state.activeQuestionIndex = nextActiveIndex;
|
|
25177
|
+
} else {
|
|
25178
|
+
state.activeQuestionIndex = 0;
|
|
25179
|
+
}
|
|
25180
|
+
|
|
25181
|
+
state.lastQuestionIds = nextQuestionIds;
|
|
25182
|
+
}
|
|
25183
|
+
|
|
25184
|
+
function isTextEntryTarget(target) {
|
|
25185
|
+
return target &&
|
|
25186
|
+
(target.tagName === 'TEXTAREA' ||
|
|
25187
|
+
target.tagName === 'INPUT' ||
|
|
25188
|
+
target.isContentEditable);
|
|
25189
|
+
}
|
|
25190
|
+
|
|
25191
|
+
function isShortcutBlockedTarget(target) {
|
|
25192
|
+
if (!target) return false;
|
|
25193
|
+
return !!target.closest(
|
|
25194
|
+
'button, a, select, summary, textarea, input, [contenteditable="true"]',
|
|
25195
|
+
);
|
|
25196
|
+
}
|
|
25197
|
+
|
|
24729
25198
|
document.addEventListener('keydown', (e) => {
|
|
24730
25199
|
const isSubmitShortcut =
|
|
24731
25200
|
(e.key === 'Enter' && (e.metaKey || e.ctrlKey)) ||
|
|
@@ -24739,12 +25208,41 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
24739
25208
|
return;
|
|
24740
25209
|
}
|
|
24741
25210
|
|
|
24742
|
-
if (e.target
|
|
25211
|
+
if (isTextEntryTarget(e.target)) return;
|
|
24743
25212
|
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
|
24744
25213
|
|
|
24745
25214
|
const questions = state.data?.questions || [];
|
|
24746
25215
|
if (!questions.length) return;
|
|
24747
25216
|
|
|
25217
|
+
if (e.key === 'Enter') {
|
|
25218
|
+
if (e.repeat) {
|
|
25219
|
+
e.preventDefault();
|
|
25220
|
+
return;
|
|
25221
|
+
}
|
|
25222
|
+
if (isShortcutBlockedTarget(e.target)) return;
|
|
25223
|
+
if (state.data.isBusy || state.isSubmitting) return;
|
|
25224
|
+
|
|
25225
|
+
const activeQ = questions[state.activeQuestionIndex];
|
|
25226
|
+
if (!activeQ) return;
|
|
25227
|
+
|
|
25228
|
+
const answer = (state.answers[activeQ.id] || '').trim();
|
|
25229
|
+
if (!answer) return;
|
|
25230
|
+
|
|
25231
|
+
const isLastQuestion =
|
|
25232
|
+
state.activeQuestionIndex === questions.length - 1;
|
|
25233
|
+
if (isLastQuestion) {
|
|
25234
|
+
const submitBtn = document.getElementById('submitButton');
|
|
25235
|
+
if (submitBtn && !submitBtn.disabled) {
|
|
25236
|
+
submitBtn.click();
|
|
25237
|
+
}
|
|
25238
|
+
} else {
|
|
25239
|
+
advanceToNextQuestion(activeQ.id);
|
|
25240
|
+
}
|
|
25241
|
+
|
|
25242
|
+
e.preventDefault();
|
|
25243
|
+
return;
|
|
25244
|
+
}
|
|
25245
|
+
|
|
24748
25246
|
const num = parseInt(e.key, 10);
|
|
24749
25247
|
if (num >= 1 && num <= 9) {
|
|
24750
25248
|
const activeQ = questions[state.activeQuestionIndex];
|
|
@@ -24912,6 +25410,10 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
24912
25410
|
function renderQuestions(questions) {
|
|
24913
25411
|
const sig = JSON.stringify([questions, state.data?.mode]);
|
|
24914
25412
|
const container = document.getElementById('questions');
|
|
25413
|
+
const previousActiveQuestionId =
|
|
25414
|
+
state.lastQuestionIds[state.activeQuestionIndex];
|
|
25415
|
+
|
|
25416
|
+
syncActiveQuestionIndex(questions);
|
|
24915
25417
|
|
|
24916
25418
|
if (state.lastSig === sig) {
|
|
24917
25419
|
questions.forEach((q) => updateOptionsDOM(q.id));
|
|
@@ -24952,6 +25454,15 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
24952
25454
|
wrapper.appendChild(title);
|
|
24953
25455
|
|
|
24954
25456
|
const predefined = question.options || [];
|
|
25457
|
+
if (predefined.length) {
|
|
25458
|
+
const hint = document.createElement('div');
|
|
25459
|
+
hint.className = 'question-hint';
|
|
25460
|
+
hint.innerHTML =
|
|
25461
|
+
'<span class="hint-chip"><kbd>1-9</kbd><span>Choose an option</span></span>' +
|
|
25462
|
+
'<span class="hint-chip"><kbd>Enter</kbd><span>Accept selected answer</span></span>';
|
|
25463
|
+
wrapper.appendChild(hint);
|
|
25464
|
+
}
|
|
25465
|
+
|
|
24955
25466
|
if (predefined.length) {
|
|
24956
25467
|
const options = document.createElement('div');
|
|
24957
25468
|
options.className = 'options';
|
|
@@ -24987,6 +25498,13 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
24987
25498
|
|
|
24988
25499
|
updateActiveQuestionFocus();
|
|
24989
25500
|
questions.forEach(q => updateOptionsDOM(q.id));
|
|
25501
|
+
const currentActiveQuestionId = questions[state.activeQuestionIndex]?.id;
|
|
25502
|
+
if (
|
|
25503
|
+
questions.length > 0 &&
|
|
25504
|
+
previousActiveQuestionId !== currentActiveQuestionId
|
|
25505
|
+
) {
|
|
25506
|
+
scrollToActiveQuestion('smooth');
|
|
25507
|
+
}
|
|
24990
25508
|
}
|
|
24991
25509
|
|
|
24992
25510
|
function render(data) {
|
|
@@ -25055,8 +25573,13 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
25055
25573
|
render(data);
|
|
25056
25574
|
}
|
|
25057
25575
|
|
|
25576
|
+
function scrollToTop() {
|
|
25577
|
+
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
|
|
25578
|
+
}
|
|
25579
|
+
|
|
25058
25580
|
document.getElementById('submitButton').addEventListener('click', async () => {
|
|
25059
|
-
if (!state.data) return;
|
|
25581
|
+
if (!state.data || state.isSubmitting) return;
|
|
25582
|
+
document.getElementById('submitButton').blur();
|
|
25060
25583
|
const answers = (state.data.questions || []).map((question) => {
|
|
25061
25584
|
return {
|
|
25062
25585
|
questionId: question.id,
|
|
@@ -25064,10 +25587,14 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
25064
25587
|
};
|
|
25065
25588
|
});
|
|
25066
25589
|
|
|
25590
|
+
state.isSubmitting = true;
|
|
25591
|
+
updateSubmitButton();
|
|
25592
|
+
|
|
25067
25593
|
const overlay = document.getElementById('loadingOverlay');
|
|
25068
25594
|
const overlayText = document.getElementById('loadingText');
|
|
25069
25595
|
overlay.classList.add('active');
|
|
25070
25596
|
overlayText.textContent = "Submitting Answers...";
|
|
25597
|
+
scrollToTop();
|
|
25071
25598
|
|
|
25072
25599
|
try {
|
|
25073
25600
|
const response = await fetch('/api/interviews/' + encodeURIComponent(interviewId) + '/answers', {
|
|
@@ -25080,6 +25607,8 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
25080
25607
|
} catch (err) {
|
|
25081
25608
|
document.getElementById('submitStatus').textContent = 'Error submitting answers.';
|
|
25082
25609
|
}
|
|
25610
|
+
state.isSubmitting = false;
|
|
25611
|
+
updateSubmitButton();
|
|
25083
25612
|
try {
|
|
25084
25613
|
await refresh();
|
|
25085
25614
|
} catch (_error) {
|
|
@@ -26399,6 +26928,7 @@ function createInterviewService(ctx, config, deps) {
|
|
|
26399
26928
|
const activeInterviewIds = new Map;
|
|
26400
26929
|
const interviewsById = new Map;
|
|
26401
26930
|
const sessionBusy = new Map;
|
|
26931
|
+
const sessionModel = new Map;
|
|
26402
26932
|
const browserOpened = new Set;
|
|
26403
26933
|
let resolveBaseUrl = null;
|
|
26404
26934
|
let onStateChange = null;
|
|
@@ -26654,10 +27184,12 @@ function createInterviewService(ctx, config, deps) {
|
|
|
26654
27184
|
}
|
|
26655
27185
|
await appendInterviewAnswers(interview, state.questions, answers);
|
|
26656
27186
|
const prompt = buildAnswerPrompt(answers, state.questions, maxQuestions);
|
|
27187
|
+
const model = sessionModel.get(interview.sessionID);
|
|
26657
27188
|
await ctx.client.session.promptAsync({
|
|
26658
27189
|
path: { id: interview.sessionID },
|
|
26659
27190
|
body: {
|
|
26660
|
-
parts: [createInternalAgentTextPart(prompt)]
|
|
27191
|
+
parts: [createInternalAgentTextPart(prompt)],
|
|
27192
|
+
...model ? { model: parseModelReference(model) ?? undefined } : {}
|
|
26661
27193
|
}
|
|
26662
27194
|
});
|
|
26663
27195
|
promptSent = true;
|
|
@@ -26707,12 +27239,23 @@ function createInterviewService(ctx, config, deps) {
|
|
|
26707
27239
|
}
|
|
26708
27240
|
return;
|
|
26709
27241
|
}
|
|
27242
|
+
if (event.type === "message.updated") {
|
|
27243
|
+
const info = properties;
|
|
27244
|
+
const sessionID = info?.info?.sessionID;
|
|
27245
|
+
const providerID = info?.info?.providerID;
|
|
27246
|
+
const modelID = info?.info?.modelID;
|
|
27247
|
+
if (sessionID && providerID && modelID) {
|
|
27248
|
+
sessionModel.set(sessionID, `${providerID}/${modelID}`);
|
|
27249
|
+
}
|
|
27250
|
+
return;
|
|
27251
|
+
}
|
|
26710
27252
|
if (event.type === "session.deleted") {
|
|
26711
27253
|
const deletedSessionId = (properties.info?.id ?? properties.sessionID) || null;
|
|
26712
27254
|
if (!deletedSessionId) {
|
|
26713
27255
|
return;
|
|
26714
27256
|
}
|
|
26715
27257
|
sessionBusy.delete(deletedSessionId);
|
|
27258
|
+
sessionModel.delete(deletedSessionId);
|
|
26716
27259
|
const interviewId = activeInterviewIds.get(deletedSessionId);
|
|
26717
27260
|
if (!interviewId) {
|
|
26718
27261
|
return;
|
|
@@ -26809,10 +27352,12 @@ function createInterviewService(ctx, config, deps) {
|
|
|
26809
27352
|
].join(`
|
|
26810
27353
|
`);
|
|
26811
27354
|
}
|
|
27355
|
+
const model = sessionModel.get(interview.sessionID);
|
|
26812
27356
|
await ctx.client.session.promptAsync({
|
|
26813
27357
|
path: { id: interview.sessionID },
|
|
26814
27358
|
body: {
|
|
26815
|
-
parts: [createInternalAgentTextPart(prompt)]
|
|
27359
|
+
parts: [createInternalAgentTextPart(prompt)],
|
|
27360
|
+
...model ? { model: parseModelReference(model) ?? undefined } : {}
|
|
26816
27361
|
}
|
|
26817
27362
|
});
|
|
26818
27363
|
promptSent = true;
|
|
@@ -27786,6 +28331,8 @@ class MultiplexerSessionManager {
|
|
|
27786
28331
|
directory;
|
|
27787
28332
|
multiplexer = null;
|
|
27788
28333
|
sessions = new Map;
|
|
28334
|
+
knownSessions = new Map;
|
|
28335
|
+
spawningSessions = new Set;
|
|
27789
28336
|
pollInterval;
|
|
27790
28337
|
enabled = false;
|
|
27791
28338
|
constructor(ctx, config) {
|
|
@@ -27814,45 +28361,59 @@ class MultiplexerSessionManager {
|
|
|
27814
28361
|
const parentId = info.parentID;
|
|
27815
28362
|
const title = info.title ?? "Subagent";
|
|
27816
28363
|
const directory = info.directory ?? this.directory;
|
|
27817
|
-
|
|
27818
|
-
|
|
28364
|
+
this.knownSessions.set(sessionId, {
|
|
28365
|
+
parentId,
|
|
28366
|
+
title,
|
|
28367
|
+
directory
|
|
28368
|
+
});
|
|
28369
|
+
if (this.isTrackedOrSpawning(sessionId)) {
|
|
28370
|
+
log("[multiplexer-session-manager] session already tracked or spawning", {
|
|
27819
28371
|
sessionId
|
|
27820
28372
|
});
|
|
27821
28373
|
return;
|
|
27822
28374
|
}
|
|
27823
|
-
|
|
27824
|
-
|
|
27825
|
-
|
|
27826
|
-
|
|
27827
|
-
|
|
27828
|
-
|
|
27829
|
-
|
|
27830
|
-
|
|
27831
|
-
|
|
27832
|
-
|
|
27833
|
-
|
|
27834
|
-
|
|
27835
|
-
|
|
27836
|
-
log("[multiplexer-session-manager] failed to spawn pane", {
|
|
27837
|
-
error: String(err)
|
|
27838
|
-
});
|
|
27839
|
-
return { success: false, paneId: undefined };
|
|
27840
|
-
});
|
|
27841
|
-
if (paneResult.success && paneResult.paneId) {
|
|
27842
|
-
const now = Date.now();
|
|
27843
|
-
this.sessions.set(sessionId, {
|
|
28375
|
+
this.spawningSessions.add(sessionId);
|
|
28376
|
+
try {
|
|
28377
|
+
const serverRunning = await isServerRunning(this.serverUrl);
|
|
28378
|
+
if (!serverRunning) {
|
|
28379
|
+
log("[multiplexer-session-manager] server not running, skipping", {
|
|
28380
|
+
serverUrl: this.serverUrl
|
|
28381
|
+
});
|
|
28382
|
+
return;
|
|
28383
|
+
}
|
|
28384
|
+
if (this.sessions.has(sessionId)) {
|
|
28385
|
+
return;
|
|
28386
|
+
}
|
|
28387
|
+
log("[multiplexer-session-manager] child session created, spawning pane", {
|
|
27844
28388
|
sessionId,
|
|
27845
|
-
paneId: paneResult.paneId,
|
|
27846
28389
|
parentId,
|
|
27847
|
-
title
|
|
27848
|
-
createdAt: now,
|
|
27849
|
-
lastSeenAt: now
|
|
28390
|
+
title
|
|
27850
28391
|
});
|
|
27851
|
-
|
|
27852
|
-
|
|
27853
|
-
|
|
28392
|
+
const paneResult = await this.multiplexer.spawnPane(sessionId, title, this.serverUrl, directory).catch((err) => {
|
|
28393
|
+
log("[multiplexer-session-manager] failed to spawn pane", {
|
|
28394
|
+
error: String(err)
|
|
28395
|
+
});
|
|
28396
|
+
return { success: false, paneId: undefined };
|
|
27854
28397
|
});
|
|
27855
|
-
|
|
28398
|
+
if (paneResult.success && paneResult.paneId) {
|
|
28399
|
+
const now = Date.now();
|
|
28400
|
+
this.sessions.set(sessionId, {
|
|
28401
|
+
sessionId,
|
|
28402
|
+
paneId: paneResult.paneId,
|
|
28403
|
+
parentId,
|
|
28404
|
+
title,
|
|
28405
|
+
directory,
|
|
28406
|
+
createdAt: now,
|
|
28407
|
+
lastSeenAt: now
|
|
28408
|
+
});
|
|
28409
|
+
log("[multiplexer-session-manager] pane spawned", {
|
|
28410
|
+
sessionId,
|
|
28411
|
+
paneId: paneResult.paneId
|
|
28412
|
+
});
|
|
28413
|
+
this.startPolling();
|
|
28414
|
+
}
|
|
28415
|
+
} finally {
|
|
28416
|
+
this.spawningSessions.delete(sessionId);
|
|
27856
28417
|
}
|
|
27857
28418
|
}
|
|
27858
28419
|
async onSessionStatus(event) {
|
|
@@ -27865,6 +28426,10 @@ class MultiplexerSessionManager {
|
|
|
27865
28426
|
return;
|
|
27866
28427
|
if (event.properties?.status?.type === "idle") {
|
|
27867
28428
|
await this.closeSession(sessionId);
|
|
28429
|
+
return;
|
|
28430
|
+
}
|
|
28431
|
+
if (event.properties?.status?.type === "busy") {
|
|
28432
|
+
await this.respawnIfKnown(sessionId);
|
|
27868
28433
|
}
|
|
27869
28434
|
}
|
|
27870
28435
|
async onSessionDeleted(event) {
|
|
@@ -27879,6 +28444,7 @@ class MultiplexerSessionManager {
|
|
|
27879
28444
|
sessionId
|
|
27880
28445
|
});
|
|
27881
28446
|
await this.closeSession(sessionId);
|
|
28447
|
+
this.knownSessions.delete(sessionId);
|
|
27882
28448
|
}
|
|
27883
28449
|
startPolling() {
|
|
27884
28450
|
if (this.pollInterval)
|
|
@@ -27939,6 +28505,61 @@ class MultiplexerSessionManager {
|
|
|
27939
28505
|
this.stopPolling();
|
|
27940
28506
|
}
|
|
27941
28507
|
}
|
|
28508
|
+
async respawnIfKnown(sessionId) {
|
|
28509
|
+
if (!this.enabled || !this.multiplexer)
|
|
28510
|
+
return;
|
|
28511
|
+
if (this.isTrackedOrSpawning(sessionId))
|
|
28512
|
+
return;
|
|
28513
|
+
const known = this.knownSessions.get(sessionId);
|
|
28514
|
+
if (!known)
|
|
28515
|
+
return;
|
|
28516
|
+
this.spawningSessions.add(sessionId);
|
|
28517
|
+
try {
|
|
28518
|
+
const serverRunning = await isServerRunning(this.serverUrl);
|
|
28519
|
+
if (!serverRunning) {
|
|
28520
|
+
log("[multiplexer-session-manager] server not running, skipping busy respawn", {
|
|
28521
|
+
serverUrl: this.serverUrl,
|
|
28522
|
+
sessionId
|
|
28523
|
+
});
|
|
28524
|
+
return;
|
|
28525
|
+
}
|
|
28526
|
+
if (this.sessions.has(sessionId))
|
|
28527
|
+
return;
|
|
28528
|
+
log("[multiplexer-session-manager] child session busy again, respawning pane", {
|
|
28529
|
+
sessionId,
|
|
28530
|
+
parentId: known.parentId,
|
|
28531
|
+
title: known.title
|
|
28532
|
+
});
|
|
28533
|
+
const paneResult = await this.multiplexer.spawnPane(sessionId, known.title, this.serverUrl, known.directory).catch((err) => {
|
|
28534
|
+
log("[multiplexer-session-manager] failed to respawn pane", {
|
|
28535
|
+
error: String(err)
|
|
28536
|
+
});
|
|
28537
|
+
return { success: false, paneId: undefined };
|
|
28538
|
+
});
|
|
28539
|
+
if (!paneResult.success || !paneResult.paneId)
|
|
28540
|
+
return;
|
|
28541
|
+
const now = Date.now();
|
|
28542
|
+
this.sessions.set(sessionId, {
|
|
28543
|
+
sessionId,
|
|
28544
|
+
paneId: paneResult.paneId,
|
|
28545
|
+
parentId: known.parentId,
|
|
28546
|
+
title: known.title,
|
|
28547
|
+
directory: known.directory,
|
|
28548
|
+
createdAt: now,
|
|
28549
|
+
lastSeenAt: now
|
|
28550
|
+
});
|
|
28551
|
+
log("[multiplexer-session-manager] pane respawned on busy", {
|
|
28552
|
+
sessionId,
|
|
28553
|
+
paneId: paneResult.paneId
|
|
28554
|
+
});
|
|
28555
|
+
this.startPolling();
|
|
28556
|
+
} finally {
|
|
28557
|
+
this.spawningSessions.delete(sessionId);
|
|
28558
|
+
}
|
|
28559
|
+
}
|
|
28560
|
+
isTrackedOrSpawning(sessionId) {
|
|
28561
|
+
return this.sessions.has(sessionId) || this.spawningSessions.has(sessionId);
|
|
28562
|
+
}
|
|
27942
28563
|
async cleanup() {
|
|
27943
28564
|
this.stopPolling();
|
|
27944
28565
|
if (this.sessions.size > 0 && this.multiplexer) {
|
|
@@ -27953,6 +28574,8 @@ class MultiplexerSessionManager {
|
|
|
27953
28574
|
await Promise.all(closePromises);
|
|
27954
28575
|
this.sessions.clear();
|
|
27955
28576
|
}
|
|
28577
|
+
this.knownSessions.clear();
|
|
28578
|
+
this.spawningSessions.clear();
|
|
27956
28579
|
log("[multiplexer-session-manager] cleanup complete");
|
|
27957
28580
|
}
|
|
27958
28581
|
}
|
|
@@ -28571,14 +29194,158 @@ Returns the councillor responses with a summary footer.`,
|
|
|
28571
29194
|
*Council: ${completed}/${total} councillors responded (${composition})*`;
|
|
28572
29195
|
const deprecated = councilManager.getDeprecatedFields();
|
|
28573
29196
|
if (deprecated && deprecated.length > 0) {
|
|
29197
|
+
const legacyMasterModel = councilManager.getLegacyMasterModel();
|
|
29198
|
+
const hasMaster = deprecated.includes("master");
|
|
29199
|
+
const trulyIgnored = hasMaster && !legacyMasterModel ? deprecated : deprecated.filter((f) => f !== "master");
|
|
29200
|
+
const parts = [];
|
|
29201
|
+
if (hasMaster && legacyMasterModel) {
|
|
29202
|
+
parts.push(`\`council.master\` is deprecated and will be removed in a future version. Its \`model\` is currently used as a fallback for the council agent — add a \`council\` entry to your preset to make this explicit.`);
|
|
29203
|
+
}
|
|
29204
|
+
if (trulyIgnored.length > 0) {
|
|
29205
|
+
parts.push(`${trulyIgnored.map((f) => `\`council.${f}\``).join(", ")} ${trulyIgnored.length === 1 ? "is" : "are"} deprecated and ignored — remove ${trulyIgnored.length === 1 ? "it" : "them"} from your config.`);
|
|
29206
|
+
}
|
|
28574
29207
|
output += `
|
|
28575
|
-
⚠ Config warning: ${
|
|
29208
|
+
⚠ Config warning: ${parts.join(" ")}`;
|
|
28576
29209
|
}
|
|
28577
29210
|
return output;
|
|
28578
29211
|
}
|
|
28579
29212
|
});
|
|
28580
29213
|
return { council_session };
|
|
28581
29214
|
}
|
|
29215
|
+
// src/tools/preset-manager.ts
|
|
29216
|
+
var COMMAND_NAME3 = "preset";
|
|
29217
|
+
function createPresetManager(ctx, config) {
|
|
29218
|
+
let activePreset = config.preset ?? null;
|
|
29219
|
+
async function handleCommandExecuteBefore(input, output) {
|
|
29220
|
+
if (input.command !== COMMAND_NAME3) {
|
|
29221
|
+
return;
|
|
29222
|
+
}
|
|
29223
|
+
output.parts.length = 0;
|
|
29224
|
+
const arg = input.arguments.trim();
|
|
29225
|
+
const presets = config.presets ?? {};
|
|
29226
|
+
if (!arg) {
|
|
29227
|
+
output.parts.push(createInternalAgentTextPart(formatPresetList(presets)));
|
|
29228
|
+
return;
|
|
29229
|
+
}
|
|
29230
|
+
if (/\s/.test(arg)) {
|
|
29231
|
+
const suggestion = arg.split(/\s+/)[0];
|
|
29232
|
+
output.parts.push(createInternalAgentTextPart(`Preset names cannot contain spaces. Did you mean: /preset ${suggestion}?`));
|
|
29233
|
+
return;
|
|
29234
|
+
}
|
|
29235
|
+
await switchPreset(arg, presets, output);
|
|
29236
|
+
}
|
|
29237
|
+
function registerCommand(opencodeConfig) {
|
|
29238
|
+
const configCommand = opencodeConfig.command;
|
|
29239
|
+
if (!configCommand?.[COMMAND_NAME3]) {
|
|
29240
|
+
if (!opencodeConfig.command) {
|
|
29241
|
+
opencodeConfig.command = {};
|
|
29242
|
+
}
|
|
29243
|
+
opencodeConfig.command[COMMAND_NAME3] = {
|
|
29244
|
+
template: "List available presets and switch between them",
|
|
29245
|
+
description: "Switch agent presets at runtime (e.g., /preset cheap, /preset powerful)"
|
|
29246
|
+
};
|
|
29247
|
+
}
|
|
29248
|
+
}
|
|
29249
|
+
async function switchPreset(presetName, presets, output) {
|
|
29250
|
+
const preset = presets[presetName];
|
|
29251
|
+
if (!preset) {
|
|
29252
|
+
const available = Object.keys(presets);
|
|
29253
|
+
const hint = available.length > 0 ? `Available presets: ${available.join(", ")}` : "No presets configured. Define presets in oh-my-opencode-slim.jsonc.";
|
|
29254
|
+
output.parts.push(createInternalAgentTextPart(`Preset "${presetName}" not found. ${hint}`));
|
|
29255
|
+
return;
|
|
29256
|
+
}
|
|
29257
|
+
const agentUpdates = {};
|
|
29258
|
+
for (const [agentName, override] of Object.entries(preset)) {
|
|
29259
|
+
const agentConfig = mapOverrideToAgentConfig(override);
|
|
29260
|
+
if (Object.keys(agentConfig).length > 0) {
|
|
29261
|
+
agentUpdates[agentName] = agentConfig;
|
|
29262
|
+
}
|
|
29263
|
+
}
|
|
29264
|
+
if (Object.keys(agentUpdates).length === 0) {
|
|
29265
|
+
output.parts.push(createInternalAgentTextPart(`Preset "${presetName}" is empty (no agent overrides defined).`));
|
|
29266
|
+
return;
|
|
29267
|
+
}
|
|
29268
|
+
try {
|
|
29269
|
+
await ctx.client.config.update({
|
|
29270
|
+
body: { agent: agentUpdates }
|
|
29271
|
+
});
|
|
29272
|
+
activePreset = presetName;
|
|
29273
|
+
const summary = Object.entries(agentUpdates).map(([name, cfg]) => {
|
|
29274
|
+
const parts = [name];
|
|
29275
|
+
if (cfg.model)
|
|
29276
|
+
parts.push(`model: ${cfg.model}`);
|
|
29277
|
+
if (cfg.variant)
|
|
29278
|
+
parts.push(`variant: ${cfg.variant}`);
|
|
29279
|
+
if (cfg.temperature !== undefined)
|
|
29280
|
+
parts.push(`temp: ${cfg.temperature}`);
|
|
29281
|
+
if (cfg.options)
|
|
29282
|
+
parts.push("options: yes");
|
|
29283
|
+
return parts.join(" → ");
|
|
29284
|
+
}).join(`
|
|
29285
|
+
`);
|
|
29286
|
+
output.parts.push(createInternalAgentTextPart(`Switched to preset "${presetName}":
|
|
29287
|
+
${summary}`));
|
|
29288
|
+
} catch (err) {
|
|
29289
|
+
output.parts.push(createInternalAgentTextPart(`Failed to switch preset "${presetName}": ${String(err)}`));
|
|
29290
|
+
}
|
|
29291
|
+
}
|
|
29292
|
+
function mapOverrideToAgentConfig(override) {
|
|
29293
|
+
const agentConfig = {};
|
|
29294
|
+
if (typeof override.model === "string") {
|
|
29295
|
+
agentConfig.model = override.model;
|
|
29296
|
+
} else if (Array.isArray(override.model) && override.model.length > 0) {
|
|
29297
|
+
const first = override.model[0];
|
|
29298
|
+
agentConfig.model = typeof first === "string" ? first : first.id;
|
|
29299
|
+
if (typeof first !== "string" && first.variant) {
|
|
29300
|
+
agentConfig.variant = first.variant;
|
|
29301
|
+
}
|
|
29302
|
+
}
|
|
29303
|
+
if (typeof override.temperature === "number") {
|
|
29304
|
+
agentConfig.temperature = override.temperature;
|
|
29305
|
+
}
|
|
29306
|
+
if (typeof override.variant === "string") {
|
|
29307
|
+
agentConfig.variant = override.variant;
|
|
29308
|
+
}
|
|
29309
|
+
if (override.options && typeof override.options === "object" && !Array.isArray(override.options)) {
|
|
29310
|
+
agentConfig.options = override.options;
|
|
29311
|
+
}
|
|
29312
|
+
return agentConfig;
|
|
29313
|
+
}
|
|
29314
|
+
function formatPresetList(presets) {
|
|
29315
|
+
const names = Object.keys(presets);
|
|
29316
|
+
if (names.length === 0) {
|
|
29317
|
+
return 'No presets configured. Define presets in oh-my-opencode-slim.jsonc under the "presets" field.';
|
|
29318
|
+
}
|
|
29319
|
+
const lines = ["Available presets:"];
|
|
29320
|
+
for (const name of names) {
|
|
29321
|
+
const marker = name === activePreset ? " ← active" : "";
|
|
29322
|
+
const preset = presets[name];
|
|
29323
|
+
const agentNames = Object.keys(preset);
|
|
29324
|
+
const models = agentNames.map((a) => {
|
|
29325
|
+
const cfg = preset[a];
|
|
29326
|
+
const modelStr = typeof cfg.model === "string" ? cfg.model : Array.isArray(cfg.model) && cfg.model.length > 0 ? resolveFirstModel(cfg.model) : undefined;
|
|
29327
|
+
return modelStr ? ` ${a} → ${modelStr}` : ` ${a}`;
|
|
29328
|
+
}).join(`
|
|
29329
|
+
`);
|
|
29330
|
+
lines.push(` ${name}${marker}`);
|
|
29331
|
+
lines.push(models);
|
|
29332
|
+
}
|
|
29333
|
+
lines.push(`
|
|
29334
|
+
Usage: /preset <name> to switch.`);
|
|
29335
|
+
return lines.join(`
|
|
29336
|
+
`);
|
|
29337
|
+
}
|
|
29338
|
+
function resolveFirstModel(models) {
|
|
29339
|
+
if (models.length === 0)
|
|
29340
|
+
return;
|
|
29341
|
+
const first = models[0];
|
|
29342
|
+
return typeof first === "string" ? first : first.id;
|
|
29343
|
+
}
|
|
29344
|
+
return {
|
|
29345
|
+
handleCommandExecuteBefore,
|
|
29346
|
+
registerCommand
|
|
29347
|
+
};
|
|
29348
|
+
}
|
|
28582
29349
|
// src/tools/smartfetch/constants.ts
|
|
28583
29350
|
var DOCS_HOST_SUFFIXES = [
|
|
28584
29351
|
".readthedocs.io",
|
|
@@ -30916,6 +31683,17 @@ class SubagentDepthTracker {
|
|
|
30916
31683
|
}
|
|
30917
31684
|
}
|
|
30918
31685
|
|
|
31686
|
+
// src/utils/system-collapse.ts
|
|
31687
|
+
function collapseSystemInPlace(system2) {
|
|
31688
|
+
const joined = system2.join(`
|
|
31689
|
+
|
|
31690
|
+
`);
|
|
31691
|
+
system2.length = 0;
|
|
31692
|
+
if (joined) {
|
|
31693
|
+
system2.push(joined);
|
|
31694
|
+
}
|
|
31695
|
+
}
|
|
31696
|
+
|
|
30919
31697
|
// src/index.ts
|
|
30920
31698
|
async function appLog(ctx, level, message) {
|
|
30921
31699
|
try {
|
|
@@ -30965,7 +31743,9 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
30965
31743
|
let jsonErrorRecoveryHook;
|
|
30966
31744
|
let foregroundFallback;
|
|
30967
31745
|
let todoContinuationHook;
|
|
31746
|
+
let taskSessionManagerHook;
|
|
30968
31747
|
let interviewManager;
|
|
31748
|
+
let presetManager;
|
|
30969
31749
|
let councilTools;
|
|
30970
31750
|
let webfetch;
|
|
30971
31751
|
let toolCount = 0;
|
|
@@ -31023,7 +31803,7 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
31023
31803
|
multiplexerSessionManager = new MultiplexerSessionManager(ctx, multiplexerConfig);
|
|
31024
31804
|
autoUpdateChecker = createAutoUpdateCheckerHook(ctx, {
|
|
31025
31805
|
showStartupToast: config.showStartupToast ?? true,
|
|
31026
|
-
autoUpdate: true
|
|
31806
|
+
autoUpdate: config.autoUpdate ?? true
|
|
31027
31807
|
});
|
|
31028
31808
|
phaseReminderHook = createPhaseReminderHook();
|
|
31029
31809
|
filterAvailableSkillsHook = createFilterAvailableSkillsHook(ctx, config);
|
|
@@ -31042,7 +31822,12 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
31042
31822
|
autoEnable: config.todoContinuation?.autoEnable ?? false,
|
|
31043
31823
|
autoEnableThreshold: config.todoContinuation?.autoEnableThreshold ?? 4
|
|
31044
31824
|
});
|
|
31825
|
+
taskSessionManagerHook = createTaskSessionManagerHook(ctx, {
|
|
31826
|
+
maxSessionsPerAgent: config.sessionManager?.maxSessionsPerAgent ?? 2,
|
|
31827
|
+
shouldManageSession: (sessionID) => sessionAgentMap.get(sessionID) === "orchestrator"
|
|
31828
|
+
});
|
|
31045
31829
|
interviewManager = createInterviewManager(ctx, config);
|
|
31830
|
+
presetManager = createPresetManager(ctx, config);
|
|
31046
31831
|
toolCount = Object.keys(councilTools).length + Object.keys(todoContinuationHook.tool).length + 1 + 2;
|
|
31047
31832
|
} catch (err) {
|
|
31048
31833
|
log("[plugin] FATAL: init failed", String(err));
|
|
@@ -31197,6 +31982,7 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
31197
31982
|
};
|
|
31198
31983
|
}
|
|
31199
31984
|
interviewManager.registerCommand(opencodeConfig);
|
|
31985
|
+
presetManager.registerCommand(opencodeConfig);
|
|
31200
31986
|
},
|
|
31201
31987
|
event: async (input) => {
|
|
31202
31988
|
const event = input.event;
|
|
@@ -31215,6 +32001,7 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
31215
32001
|
await multiplexerSessionManager.onSessionDeleted(event);
|
|
31216
32002
|
await interviewManager.handleEvent(input);
|
|
31217
32003
|
await postFileToolNudgeHook.event(input);
|
|
32004
|
+
await taskSessionManagerHook.event(input);
|
|
31218
32005
|
if (input.event.type === "session.deleted") {
|
|
31219
32006
|
const props = input.event.properties;
|
|
31220
32007
|
const sessionID = props?.info?.id ?? props?.sessionID;
|
|
@@ -31228,10 +32015,12 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
31228
32015
|
},
|
|
31229
32016
|
"tool.execute.before": async (input, output) => {
|
|
31230
32017
|
await applyPatchHook["tool.execute.before"](input, output);
|
|
32018
|
+
await taskSessionManagerHook["tool.execute.before"](input, output);
|
|
31231
32019
|
},
|
|
31232
32020
|
"command.execute.before": async (input, output) => {
|
|
31233
32021
|
await todoContinuationHook.handleCommandExecuteBefore(input, output);
|
|
31234
32022
|
await interviewManager.handleCommandExecuteBefore(input, output);
|
|
32023
|
+
await presetManager.handleCommandExecuteBefore(input, output);
|
|
31235
32024
|
},
|
|
31236
32025
|
"chat.headers": chatHeadersHook["chat.headers"],
|
|
31237
32026
|
"chat.message": async (input, output) => {
|
|
@@ -31262,10 +32051,8 @@ ${output.system[0]}` : "");
|
|
|
31262
32051
|
}
|
|
31263
32052
|
await todoContinuationHook.handleChatSystemTransform(input, output);
|
|
31264
32053
|
await postFileToolNudgeHook["experimental.chat.system.transform"](input, output);
|
|
31265
|
-
|
|
31266
|
-
|
|
31267
|
-
`);
|
|
31268
|
-
output.system = joined ? [joined] : [];
|
|
32054
|
+
await taskSessionManagerHook["experimental.chat.system.transform"](input, output);
|
|
32055
|
+
collapseSystemInPlace(output.system);
|
|
31269
32056
|
},
|
|
31270
32057
|
"experimental.chat.messages.transform": async (input, output) => {
|
|
31271
32058
|
const typedOutput = output;
|
|
@@ -31297,6 +32084,7 @@ ${output.system[0]}` : "");
|
|
|
31297
32084
|
await jsonErrorRecoveryHook["tool.execute.after"](input, output);
|
|
31298
32085
|
await todoContinuationHook.handleToolExecuteAfter(input);
|
|
31299
32086
|
await postFileToolNudgeHook["tool.execute.after"](input, output);
|
|
32087
|
+
await taskSessionManagerHook["tool.execute.after"](input, output);
|
|
31300
32088
|
}
|
|
31301
32089
|
};
|
|
31302
32090
|
};
|