oh-my-opencode-slim 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -13
- package/dist/cli/index.js +10 -3
- 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 +2 -2
- package/dist/config/schema.d.ts +12 -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 +40 -0
- package/dist/index.js +970 -149
- package/dist/multiplexer/session-manager.d.ts +4 -0
- package/dist/tools/council.d.ts +2 -2
- package/dist/utils/agent-variant.d.ts +2 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/logger.d.ts +2 -0
- package/dist/utils/session-manager.d.ts +55 -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 +27 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6246,33 +6246,33 @@ var require_URL = __commonJS((exports, module) => {
|
|
|
6246
6246
|
else
|
|
6247
6247
|
return basepath.substring(0, lastslash + 1) + refpath;
|
|
6248
6248
|
}
|
|
6249
|
-
function remove_dot_segments(
|
|
6250
|
-
if (!
|
|
6251
|
-
return
|
|
6249
|
+
function remove_dot_segments(path14) {
|
|
6250
|
+
if (!path14)
|
|
6251
|
+
return path14;
|
|
6252
6252
|
var output = "";
|
|
6253
|
-
while (
|
|
6254
|
-
if (
|
|
6255
|
-
|
|
6253
|
+
while (path14.length > 0) {
|
|
6254
|
+
if (path14 === "." || path14 === "..") {
|
|
6255
|
+
path14 = "";
|
|
6256
6256
|
break;
|
|
6257
6257
|
}
|
|
6258
|
-
var twochars =
|
|
6259
|
-
var threechars =
|
|
6260
|
-
var fourchars =
|
|
6258
|
+
var twochars = path14.substring(0, 2);
|
|
6259
|
+
var threechars = path14.substring(0, 3);
|
|
6260
|
+
var fourchars = path14.substring(0, 4);
|
|
6261
6261
|
if (threechars === "../") {
|
|
6262
|
-
|
|
6262
|
+
path14 = path14.substring(3);
|
|
6263
6263
|
} else if (twochars === "./") {
|
|
6264
|
-
|
|
6264
|
+
path14 = path14.substring(2);
|
|
6265
6265
|
} else if (threechars === "/./") {
|
|
6266
|
-
|
|
6267
|
-
} else if (twochars === "/." &&
|
|
6268
|
-
|
|
6269
|
-
} else if (fourchars === "/../" || threechars === "/.." &&
|
|
6270
|
-
|
|
6266
|
+
path14 = "/" + path14.substring(3);
|
|
6267
|
+
} else if (twochars === "/." && path14.length === 2) {
|
|
6268
|
+
path14 = "/";
|
|
6269
|
+
} else if (fourchars === "/../" || threechars === "/.." && path14.length === 3) {
|
|
6270
|
+
path14 = "/" + path14.substring(4);
|
|
6271
6271
|
output = output.replace(/\/?[^\/]*$/, "");
|
|
6272
6272
|
} else {
|
|
6273
|
-
var segment =
|
|
6273
|
+
var segment = path14.match(/(\/?([^\/]*))/)[0];
|
|
6274
6274
|
output += segment;
|
|
6275
|
-
|
|
6275
|
+
path14 = path14.substring(segment.length);
|
|
6276
6276
|
}
|
|
6277
6277
|
}
|
|
6278
6278
|
return output;
|
|
@@ -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;
|
|
@@ -18525,6 +18525,11 @@ var InterviewConfigSchema = z2.object({
|
|
|
18525
18525
|
port: z2.number().int().min(0).max(65535).default(0),
|
|
18526
18526
|
dashboard: z2.boolean().default(false)
|
|
18527
18527
|
});
|
|
18528
|
+
var SessionManagerConfigSchema = z2.object({
|
|
18529
|
+
maxSessionsPerAgent: z2.number().int().min(1).max(10).default(2),
|
|
18530
|
+
readContextMinLines: z2.number().int().min(0).max(1000).default(10),
|
|
18531
|
+
readContextMaxFiles: z2.number().int().min(0).max(50).default(8)
|
|
18532
|
+
});
|
|
18528
18533
|
var TodoContinuationConfigSchema = z2.object({
|
|
18529
18534
|
maxContinuations: z2.number().int().min(1).max(50).default(5).describe("Maximum consecutive auto-continuations before stopping to ask user"),
|
|
18530
18535
|
cooldownMs: z2.number().int().min(0).max(30000).default(3000).describe("Delay in ms before auto-continuing (gives user time to abort)"),
|
|
@@ -18566,6 +18571,7 @@ var PluginConfigSchema = z2.object({
|
|
|
18566
18571
|
scoringEngineVersion: z2.enum(["v1", "v2-shadow", "v2"]).optional(),
|
|
18567
18572
|
balanceProviderUsage: z2.boolean().optional(),
|
|
18568
18573
|
showStartupToast: z2.boolean().optional().describe("Show the startup activation toast when OpenCode starts. Defaults to true."),
|
|
18574
|
+
autoUpdate: z2.boolean().optional().describe("Disable automatic installation of plugin updates when false. Defaults to true."),
|
|
18569
18575
|
manualPlan: ManualPlanSchema.optional(),
|
|
18570
18576
|
presets: z2.record(z2.string(), PresetSchema).optional(),
|
|
18571
18577
|
agents: z2.record(z2.string(), AgentOverrideConfigSchema).optional(),
|
|
@@ -18575,6 +18581,7 @@ var PluginConfigSchema = z2.object({
|
|
|
18575
18581
|
tmux: TmuxConfigSchema.optional(),
|
|
18576
18582
|
websearch: WebsearchConfigSchema.optional(),
|
|
18577
18583
|
interview: InterviewConfigSchema.optional(),
|
|
18584
|
+
sessionManager: SessionManagerConfigSchema.optional(),
|
|
18578
18585
|
todoContinuation: TodoContinuationConfigSchema.optional(),
|
|
18579
18586
|
fallback: FailoverConfigSchema.optional(),
|
|
18580
18587
|
council: CouncilConfigSchema.optional()
|
|
@@ -18660,6 +18667,7 @@ function loadPluginConfig(directory) {
|
|
|
18660
18667
|
tmux: deepMerge(config.tmux, projectConfig.tmux),
|
|
18661
18668
|
multiplexer: deepMerge(config.multiplexer, projectConfig.multiplexer),
|
|
18662
18669
|
interview: deepMerge(config.interview, projectConfig.interview),
|
|
18670
|
+
sessionManager: deepMerge(config.sessionManager, projectConfig.sessionManager),
|
|
18663
18671
|
fallback: deepMerge(config.fallback, projectConfig.fallback),
|
|
18664
18672
|
council: deepMerge(config.council, projectConfig.council)
|
|
18665
18673
|
};
|
|
@@ -18931,6 +18939,12 @@ ${enabledParallelExamples}
|
|
|
18931
18939
|
|
|
18932
18940
|
Balance: respect dependencies, avoid parallelizing what must be sequential.
|
|
18933
18941
|
|
|
18942
|
+
### OpenCode subagent execution model
|
|
18943
|
+
- A delegated specialist runs in a separate child session.
|
|
18944
|
+
- Delegation is blocking for the parent at that point: send work out, then continue that line after results return.
|
|
18945
|
+
- Parallel delegation means launching multiple independent child-session branches.
|
|
18946
|
+
- Only parallelize branches that are truly independent; reconcile dependent steps after delegated results come back.
|
|
18947
|
+
|
|
18934
18948
|
## 5. Execute
|
|
18935
18949
|
1. Break complex tasks into todos
|
|
18936
18950
|
2. Fire parallel research/implementation
|
|
@@ -18938,6 +18952,12 @@ Balance: respect dependencies, avoid parallelizing what must be sequential.
|
|
|
18938
18952
|
4. Integrate results
|
|
18939
18953
|
5. Adjust if needed
|
|
18940
18954
|
|
|
18955
|
+
### Session Reuse
|
|
18956
|
+
- Reuse an available specialist session only for clear follow-up work on the same thread.
|
|
18957
|
+
- Prefer a fresh session for unrelated work, even with the same specialist.
|
|
18958
|
+
- If multiple remembered sessions fit, prefer the most recently used matching session.
|
|
18959
|
+
- If reuse is unclear, start a fresh session.
|
|
18960
|
+
|
|
18941
18961
|
### Auto-Continue
|
|
18942
18962
|
When working through multi-step tasks, consider enabling auto-continue to avoid stopping between batches:
|
|
18943
18963
|
- **Enable when:** User requests autonomous/batch work, or you create 4+ todos in a session
|
|
@@ -19751,12 +19771,14 @@ function getDisabledAgents(config) {
|
|
|
19751
19771
|
|
|
19752
19772
|
// src/utils/logger.ts
|
|
19753
19773
|
import * as fs2 from "node:fs";
|
|
19774
|
+
import { appendFile } from "node:fs/promises";
|
|
19754
19775
|
import * as os from "node:os";
|
|
19755
19776
|
import * as path2 from "node:path";
|
|
19756
19777
|
var LOG_PREFIX = "oh-my-opencode-slim.";
|
|
19757
19778
|
var LOG_SUFFIX = ".log";
|
|
19758
19779
|
var RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
19759
19780
|
var logFile = null;
|
|
19781
|
+
var writeChain = Promise.resolve();
|
|
19760
19782
|
function getLogDir() {
|
|
19761
19783
|
return process.env.OPENCODE_LOG_DIR ?? path2.join(os.homedir(), ".local/share/opencode");
|
|
19762
19784
|
}
|
|
@@ -19799,10 +19821,14 @@ function initLogger(sessionId) {
|
|
|
19799
19821
|
fs2.mkdirSync(dir, { recursive: true });
|
|
19800
19822
|
} catch {}
|
|
19801
19823
|
logFile = path2.join(dir, `${LOG_PREFIX}${sessionId}${LOG_SUFFIX}`);
|
|
19824
|
+
try {
|
|
19825
|
+
fs2.closeSync(fs2.openSync(logFile, "a"));
|
|
19826
|
+
} catch {}
|
|
19802
19827
|
cleanupOldLogs(dir);
|
|
19803
19828
|
}
|
|
19804
19829
|
function log(message, data) {
|
|
19805
|
-
|
|
19830
|
+
const target = logFile;
|
|
19831
|
+
if (!target)
|
|
19806
19832
|
return;
|
|
19807
19833
|
try {
|
|
19808
19834
|
const timestamp = new Date().toISOString();
|
|
@@ -19816,7 +19842,7 @@ function log(message, data) {
|
|
|
19816
19842
|
}
|
|
19817
19843
|
const logEntry = `[${timestamp}] ${message} ${dataStr}
|
|
19818
19844
|
`;
|
|
19819
|
-
|
|
19845
|
+
writeChain = writeChain.then(() => appendFile(target, logEntry)).catch(() => {});
|
|
19820
19846
|
} catch {}
|
|
19821
19847
|
}
|
|
19822
19848
|
|
|
@@ -20002,7 +20028,7 @@ class CouncilManager {
|
|
|
20002
20028
|
}
|
|
20003
20029
|
} else {
|
|
20004
20030
|
const promises = entries.map(([name, config], index) => (async () => {
|
|
20005
|
-
if (index > 0) {
|
|
20031
|
+
if (this.tmuxEnabled && index > 0) {
|
|
20006
20032
|
await new Promise((r) => setTimeout(r, index * COUNCILLOR_STAGGER_MS));
|
|
20007
20033
|
}
|
|
20008
20034
|
return this.runCouncillorWithRetry(name, config, prompt, parentSessionId, timeout, maxRetries);
|
|
@@ -22018,11 +22044,8 @@ function resolveRuntimeAgentName(config, agentName) {
|
|
|
22018
22044
|
function escapeRegExp2(value) {
|
|
22019
22045
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
22020
22046
|
}
|
|
22021
|
-
function
|
|
22022
|
-
|
|
22023
|
-
return text;
|
|
22024
|
-
}
|
|
22025
|
-
let rewritten = text;
|
|
22047
|
+
function createDisplayNameMentionRewriter(config) {
|
|
22048
|
+
const replacements = [];
|
|
22026
22049
|
for (const internalName of getRuntimeAgentNames(config)) {
|
|
22027
22050
|
const displayName = getAgentOverride(config, internalName)?.displayName;
|
|
22028
22051
|
if (!displayName) {
|
|
@@ -22032,9 +22055,24 @@ function rewriteDisplayNameMentions(config, text) {
|
|
|
22032
22055
|
if (!normalizedDisplayName || normalizedDisplayName === internalName) {
|
|
22033
22056
|
continue;
|
|
22034
22057
|
}
|
|
22035
|
-
|
|
22058
|
+
replacements.push({
|
|
22059
|
+
regex: new RegExp(`(^|[^\\w.])@${escapeRegExp2(normalizedDisplayName)}\\b`, "g"),
|
|
22060
|
+
internalName
|
|
22061
|
+
});
|
|
22062
|
+
}
|
|
22063
|
+
if (replacements.length === 0) {
|
|
22064
|
+
return (text) => text;
|
|
22036
22065
|
}
|
|
22037
|
-
return
|
|
22066
|
+
return (text) => {
|
|
22067
|
+
if (!text.includes("@")) {
|
|
22068
|
+
return text;
|
|
22069
|
+
}
|
|
22070
|
+
let rewritten = text;
|
|
22071
|
+
for (const replacement of replacements) {
|
|
22072
|
+
rewritten = rewritten.replace(replacement.regex, `$1@${replacement.internalName}`);
|
|
22073
|
+
}
|
|
22074
|
+
return rewritten;
|
|
22075
|
+
};
|
|
22038
22076
|
}
|
|
22039
22077
|
// src/utils/internal-initiator.ts
|
|
22040
22078
|
var SLIM_INTERNAL_INITIATOR_MARKER = "<!-- SLIM_INTERNAL_INITIATOR -->";
|
|
@@ -22057,6 +22095,249 @@ function hasInternalInitiatorMarker(part) {
|
|
|
22057
22095
|
}
|
|
22058
22096
|
return part.text.includes(SLIM_INTERNAL_INITIATOR_MARKER);
|
|
22059
22097
|
}
|
|
22098
|
+
// src/utils/session-manager.ts
|
|
22099
|
+
var MIN_CONTEXT_FILE_LINES = 10;
|
|
22100
|
+
var MAX_CONTEXT_FILES_PER_SESSION = 8;
|
|
22101
|
+
function aliasPrefix(agentType) {
|
|
22102
|
+
switch (agentType) {
|
|
22103
|
+
case "explorer":
|
|
22104
|
+
return "exp";
|
|
22105
|
+
case "librarian":
|
|
22106
|
+
return "lib";
|
|
22107
|
+
case "oracle":
|
|
22108
|
+
return "ora";
|
|
22109
|
+
case "designer":
|
|
22110
|
+
return "des";
|
|
22111
|
+
case "fixer":
|
|
22112
|
+
return "fix";
|
|
22113
|
+
case "observer":
|
|
22114
|
+
return "obs";
|
|
22115
|
+
case "council":
|
|
22116
|
+
return "cnc";
|
|
22117
|
+
case "councillor":
|
|
22118
|
+
return "clr";
|
|
22119
|
+
case "orchestrator":
|
|
22120
|
+
return "orc";
|
|
22121
|
+
}
|
|
22122
|
+
}
|
|
22123
|
+
function normalizeWhitespace(value) {
|
|
22124
|
+
return value.replace(/\s+/g, " ").trim();
|
|
22125
|
+
}
|
|
22126
|
+
function deriveTaskSessionLabel(input) {
|
|
22127
|
+
const preferred = normalizeWhitespace(input.description ?? "");
|
|
22128
|
+
if (preferred) {
|
|
22129
|
+
return preferred.slice(0, 48);
|
|
22130
|
+
}
|
|
22131
|
+
const firstPromptLine = (input.prompt ?? "").split(/\r?\n/).map((line) => normalizeWhitespace(line)).find(Boolean);
|
|
22132
|
+
if (firstPromptLine) {
|
|
22133
|
+
return firstPromptLine.slice(0, 48);
|
|
22134
|
+
}
|
|
22135
|
+
return `recent ${input.agentType} task`;
|
|
22136
|
+
}
|
|
22137
|
+
|
|
22138
|
+
class SessionManager {
|
|
22139
|
+
maxSessionsPerAgent;
|
|
22140
|
+
readContextMinLines;
|
|
22141
|
+
readContextMaxFiles;
|
|
22142
|
+
sessionsByParent = new Map;
|
|
22143
|
+
nextAliasIndexByParent = new Map;
|
|
22144
|
+
orderCounter = 0;
|
|
22145
|
+
constructor(maxSessionsPerAgent, options = {}) {
|
|
22146
|
+
this.maxSessionsPerAgent = maxSessionsPerAgent;
|
|
22147
|
+
this.readContextMinLines = options.readContextMinLines ?? MIN_CONTEXT_FILE_LINES;
|
|
22148
|
+
this.readContextMaxFiles = options.readContextMaxFiles ?? MAX_CONTEXT_FILES_PER_SESSION;
|
|
22149
|
+
}
|
|
22150
|
+
remember(input) {
|
|
22151
|
+
const now = this.nextOrder();
|
|
22152
|
+
const group = this.getAgentGroup(input.parentSessionId, input.agentType, true);
|
|
22153
|
+
if (!group) {
|
|
22154
|
+
throw new Error("Failed to initialize session group");
|
|
22155
|
+
}
|
|
22156
|
+
const existing = group.find((entry) => entry.taskId === input.taskId);
|
|
22157
|
+
if (existing) {
|
|
22158
|
+
existing.label = input.label;
|
|
22159
|
+
existing.lastUsedAt = this.nextOrder();
|
|
22160
|
+
return existing;
|
|
22161
|
+
}
|
|
22162
|
+
const remembered = {
|
|
22163
|
+
alias: this.nextAlias(input.parentSessionId, input.agentType),
|
|
22164
|
+
taskId: input.taskId,
|
|
22165
|
+
agentType: input.agentType,
|
|
22166
|
+
label: input.label,
|
|
22167
|
+
contextFiles: [],
|
|
22168
|
+
createdAt: now,
|
|
22169
|
+
lastUsedAt: now
|
|
22170
|
+
};
|
|
22171
|
+
group.push(remembered);
|
|
22172
|
+
this.trimGroup(group);
|
|
22173
|
+
return remembered;
|
|
22174
|
+
}
|
|
22175
|
+
markUsed(parentSessionId, agentType, key) {
|
|
22176
|
+
const group = this.getAgentGroup(parentSessionId, agentType, false);
|
|
22177
|
+
const match = group?.find((entry) => entry.alias === key || entry.taskId === key);
|
|
22178
|
+
if (match) {
|
|
22179
|
+
match.lastUsedAt = this.nextOrder();
|
|
22180
|
+
}
|
|
22181
|
+
}
|
|
22182
|
+
resolve(parentSessionId, agentType, key) {
|
|
22183
|
+
const group = this.getAgentGroup(parentSessionId, agentType, false);
|
|
22184
|
+
return group?.find((entry) => entry.alias === key || entry.taskId === key);
|
|
22185
|
+
}
|
|
22186
|
+
drop(parentSessionId, agentType, key) {
|
|
22187
|
+
const group = this.getAgentGroup(parentSessionId, agentType, false);
|
|
22188
|
+
if (!group)
|
|
22189
|
+
return;
|
|
22190
|
+
const next = group.filter((entry) => entry.alias !== key && entry.taskId !== key);
|
|
22191
|
+
this.setAgentGroup(parentSessionId, agentType, next);
|
|
22192
|
+
}
|
|
22193
|
+
dropTask(taskId) {
|
|
22194
|
+
for (const [parentSessionId, groups] of this.sessionsByParent.entries()) {
|
|
22195
|
+
for (const [agentType, group] of groups.entries()) {
|
|
22196
|
+
const next = group.filter((entry) => entry.taskId !== taskId);
|
|
22197
|
+
this.setAgentGroup(parentSessionId, agentType, next);
|
|
22198
|
+
}
|
|
22199
|
+
}
|
|
22200
|
+
}
|
|
22201
|
+
taskIds() {
|
|
22202
|
+
const ids = new Set;
|
|
22203
|
+
for (const groups of this.sessionsByParent.values()) {
|
|
22204
|
+
for (const group of groups.values()) {
|
|
22205
|
+
for (const entry of group) {
|
|
22206
|
+
ids.add(entry.taskId);
|
|
22207
|
+
}
|
|
22208
|
+
}
|
|
22209
|
+
}
|
|
22210
|
+
return ids;
|
|
22211
|
+
}
|
|
22212
|
+
addContext(taskId, files) {
|
|
22213
|
+
if (files.length === 0)
|
|
22214
|
+
return;
|
|
22215
|
+
for (const groups of this.sessionsByParent.values()) {
|
|
22216
|
+
for (const group of groups.values()) {
|
|
22217
|
+
const match = group.find((entry) => entry.taskId === taskId);
|
|
22218
|
+
if (!match)
|
|
22219
|
+
continue;
|
|
22220
|
+
const existing = new Map(match.contextFiles.map((file) => [file.path, file]));
|
|
22221
|
+
for (const file of files) {
|
|
22222
|
+
const previous = existing.get(file.path);
|
|
22223
|
+
if (previous) {
|
|
22224
|
+
previous.lineCount = Math.max(previous.lineCount, file.lineCount);
|
|
22225
|
+
previous.lastReadAt = Math.max(previous.lastReadAt, file.lastReadAt);
|
|
22226
|
+
continue;
|
|
22227
|
+
}
|
|
22228
|
+
match.contextFiles.push({ ...file });
|
|
22229
|
+
}
|
|
22230
|
+
this.trimContextFiles(match);
|
|
22231
|
+
}
|
|
22232
|
+
}
|
|
22233
|
+
}
|
|
22234
|
+
clearParent(parentSessionId) {
|
|
22235
|
+
this.sessionsByParent.delete(parentSessionId);
|
|
22236
|
+
this.nextAliasIndexByParent.delete(parentSessionId);
|
|
22237
|
+
}
|
|
22238
|
+
formatForPrompt(parentSessionId) {
|
|
22239
|
+
const groups = this.sessionsByParent.get(parentSessionId);
|
|
22240
|
+
if (!groups || groups.size === 0)
|
|
22241
|
+
return;
|
|
22242
|
+
const lines = [...groups.entries()].map(([agentType, entries]) => [
|
|
22243
|
+
agentType,
|
|
22244
|
+
[...entries].sort((a, b) => b.lastUsedAt - a.lastUsedAt)
|
|
22245
|
+
]).filter(([, entries]) => entries.length > 0).sort((a, b) => b[1][0].lastUsedAt - a[1][0].lastUsedAt).map(([agentType, entries]) => [
|
|
22246
|
+
`- ${agentType}: ${entries.map((entry) => `${entry.alias} ${entry.label}`).join("; ")}`,
|
|
22247
|
+
...entries.map((entry) => [
|
|
22248
|
+
entry,
|
|
22249
|
+
formatContextFiles(entry.contextFiles, {
|
|
22250
|
+
minLines: this.readContextMinLines,
|
|
22251
|
+
maxFiles: this.readContextMaxFiles
|
|
22252
|
+
})
|
|
22253
|
+
]).filter(([, context]) => context.length > 0).map(([entry, context]) => ` Context read by ${entry.alias}: ${context}`)
|
|
22254
|
+
].join(`
|
|
22255
|
+
`));
|
|
22256
|
+
if (lines.length === 0)
|
|
22257
|
+
return;
|
|
22258
|
+
return [
|
|
22259
|
+
"### Resumable Sessions",
|
|
22260
|
+
"Reuse only for clear continuation of the same thread. Otherwise start fresh.",
|
|
22261
|
+
"",
|
|
22262
|
+
...lines
|
|
22263
|
+
].join(`
|
|
22264
|
+
`);
|
|
22265
|
+
}
|
|
22266
|
+
getAgentGroup(parentSessionId, agentType, create) {
|
|
22267
|
+
let groups = this.sessionsByParent.get(parentSessionId);
|
|
22268
|
+
if (!groups && create) {
|
|
22269
|
+
groups = new Map;
|
|
22270
|
+
this.sessionsByParent.set(parentSessionId, groups);
|
|
22271
|
+
}
|
|
22272
|
+
let group = groups?.get(agentType);
|
|
22273
|
+
if (!group && create && groups) {
|
|
22274
|
+
group = [];
|
|
22275
|
+
groups.set(agentType, group);
|
|
22276
|
+
}
|
|
22277
|
+
return group;
|
|
22278
|
+
}
|
|
22279
|
+
setAgentGroup(parentSessionId, agentType, entries) {
|
|
22280
|
+
const groups = this.sessionsByParent.get(parentSessionId);
|
|
22281
|
+
if (!groups)
|
|
22282
|
+
return;
|
|
22283
|
+
if (entries.length === 0) {
|
|
22284
|
+
groups.delete(agentType);
|
|
22285
|
+
if (groups.size === 0) {
|
|
22286
|
+
this.sessionsByParent.delete(parentSessionId);
|
|
22287
|
+
this.nextAliasIndexByParent.delete(parentSessionId);
|
|
22288
|
+
}
|
|
22289
|
+
return;
|
|
22290
|
+
}
|
|
22291
|
+
groups.set(agentType, entries);
|
|
22292
|
+
}
|
|
22293
|
+
nextAlias(parentSessionId, agentType) {
|
|
22294
|
+
let counters = this.nextAliasIndexByParent.get(parentSessionId);
|
|
22295
|
+
if (!counters) {
|
|
22296
|
+
counters = new Map;
|
|
22297
|
+
this.nextAliasIndexByParent.set(parentSessionId, counters);
|
|
22298
|
+
}
|
|
22299
|
+
const next = (counters.get(agentType) ?? 0) + 1;
|
|
22300
|
+
counters.set(agentType, next);
|
|
22301
|
+
return `${aliasPrefix(agentType)}-${next}`;
|
|
22302
|
+
}
|
|
22303
|
+
trimGroup(group) {
|
|
22304
|
+
group.sort((a, b) => b.lastUsedAt - a.lastUsedAt);
|
|
22305
|
+
if (group.length > this.maxSessionsPerAgent) {
|
|
22306
|
+
group.length = this.maxSessionsPerAgent;
|
|
22307
|
+
}
|
|
22308
|
+
}
|
|
22309
|
+
trimContextFiles(entry) {
|
|
22310
|
+
if (this.readContextMaxFiles === 0) {
|
|
22311
|
+
entry.contextFiles = [];
|
|
22312
|
+
return;
|
|
22313
|
+
}
|
|
22314
|
+
entry.contextFiles = entry.contextFiles.filter((file) => file.lineCount >= this.readContextMinLines).sort((a, b) => b.lastReadAt - a.lastReadAt).slice(0, this.readContextMaxFiles + 1);
|
|
22315
|
+
}
|
|
22316
|
+
nextOrder() {
|
|
22317
|
+
this.orderCounter += 1;
|
|
22318
|
+
return this.orderCounter;
|
|
22319
|
+
}
|
|
22320
|
+
}
|
|
22321
|
+
function formatContextFiles(files, options) {
|
|
22322
|
+
const eligible = files.filter((file) => file.lineCount >= options.minLines).sort((a, b) => b.lastReadAt - a.lastReadAt);
|
|
22323
|
+
const shown = eligible.slice(0, options.maxFiles);
|
|
22324
|
+
const rest = eligible.length - shown.length;
|
|
22325
|
+
const rendered = shown.map((file) => `${file.path} (${file.lineCount} lines)`);
|
|
22326
|
+
return `${rendered.join(", ")}${rest > 0 ? ` (+${rest} more)` : ""}`;
|
|
22327
|
+
}
|
|
22328
|
+
// src/utils/task.ts
|
|
22329
|
+
function parseTaskIdFromTaskOutput(output) {
|
|
22330
|
+
const lines = output.split(/\r?\n/);
|
|
22331
|
+
for (const line of lines) {
|
|
22332
|
+
const trimmed = line.trim();
|
|
22333
|
+
const match = /^task_id:\s*([^\s()]+)(?:\s*\(.*)?$/.exec(trimmed);
|
|
22334
|
+
if (!match) {
|
|
22335
|
+
continue;
|
|
22336
|
+
}
|
|
22337
|
+
return match[1];
|
|
22338
|
+
}
|
|
22339
|
+
return;
|
|
22340
|
+
}
|
|
22060
22341
|
// src/utils/zip-extractor.ts
|
|
22061
22342
|
import { spawnSync } from "node:child_process";
|
|
22062
22343
|
import { release } from "node:os";
|
|
@@ -22345,6 +22626,17 @@ ${allowedEntries.map((entry) => entry.block).join(`
|
|
|
22345
22626
|
});
|
|
22346
22627
|
}
|
|
22347
22628
|
function createFilterAvailableSkillsHook(_ctx, config) {
|
|
22629
|
+
const permissionRulesByAgent = new Map;
|
|
22630
|
+
const getPermissionRules = (agentName) => {
|
|
22631
|
+
const cached = permissionRulesByAgent.get(agentName);
|
|
22632
|
+
if (cached) {
|
|
22633
|
+
return cached;
|
|
22634
|
+
}
|
|
22635
|
+
const configuredSkills = getAgentOverride(config, agentName)?.skills;
|
|
22636
|
+
const permissionRules = getSkillPermissionsForAgent(agentName, configuredSkills);
|
|
22637
|
+
permissionRulesByAgent.set(agentName, permissionRules);
|
|
22638
|
+
return permissionRules;
|
|
22639
|
+
};
|
|
22348
22640
|
return {
|
|
22349
22641
|
"experimental.chat.messages.transform": async (_input, output) => {
|
|
22350
22642
|
const { messages } = output;
|
|
@@ -22352,8 +22644,7 @@ function createFilterAvailableSkillsHook(_ctx, config) {
|
|
|
22352
22644
|
return;
|
|
22353
22645
|
}
|
|
22354
22646
|
const agentName = getCurrentAgent(messages);
|
|
22355
|
-
const
|
|
22356
|
-
const permissionRules = getSkillPermissionsForAgent(agentName, configuredSkills);
|
|
22647
|
+
const permissionRules = getPermissionRules(agentName);
|
|
22357
22648
|
for (const message of messages) {
|
|
22358
22649
|
for (const part of message.parts) {
|
|
22359
22650
|
if (part.type !== "text" || !part.text || !part.text.includes("<available_skills>")) {
|
|
@@ -22372,6 +22663,8 @@ var RATE_LIMIT_PATTERNS = [
|
|
|
22372
22663
|
/too many requests/i,
|
|
22373
22664
|
/quota.?exceeded/i,
|
|
22374
22665
|
/usage.?exceeded/i,
|
|
22666
|
+
/ExceededBudget/i,
|
|
22667
|
+
/over.?budget/i,
|
|
22375
22668
|
/usage limit/i,
|
|
22376
22669
|
/overloaded/i,
|
|
22377
22670
|
/resource.?exhausted/i,
|
|
@@ -22450,7 +22743,7 @@ class ForegroundFallbackManager {
|
|
|
22450
22743
|
if (!props?.sessionID || props.status?.type !== "retry")
|
|
22451
22744
|
break;
|
|
22452
22745
|
const msg = props.status.message?.toLowerCase() ?? "";
|
|
22453
|
-
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")) {
|
|
22746
|
+
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")) {
|
|
22454
22747
|
await this.tryFallback(props.sessionID);
|
|
22455
22748
|
}
|
|
22456
22749
|
break;
|
|
@@ -22704,7 +22997,21 @@ function processImageAttachments(args) {
|
|
|
22704
22997
|
const observerEnabled = !disabledAgents.has("observer");
|
|
22705
22998
|
if (!observerEnabled)
|
|
22706
22999
|
return;
|
|
23000
|
+
const messagesWithImages = [];
|
|
23001
|
+
for (const msg of messages) {
|
|
23002
|
+
if (msg.info.role !== "user")
|
|
23003
|
+
continue;
|
|
23004
|
+
const imageParts = msg.parts.filter(isImagePart);
|
|
23005
|
+
if (imageParts.length > 0) {
|
|
23006
|
+
messagesWithImages.push({ msg, imageParts });
|
|
23007
|
+
}
|
|
23008
|
+
}
|
|
22707
23009
|
const saveDir = join7(workDir, ".opencode", "images");
|
|
23010
|
+
if (messagesWithImages.length === 0) {
|
|
23011
|
+
if (existsSync4(saveDir))
|
|
23012
|
+
cleanupAllSessions(saveDir);
|
|
23013
|
+
return;
|
|
23014
|
+
}
|
|
22708
23015
|
const gitignorePath = join7(workDir, ".opencode", ".gitignore");
|
|
22709
23016
|
try {
|
|
22710
23017
|
mkdirSync2(saveDir, { recursive: true });
|
|
@@ -22715,12 +23022,7 @@ function processImageAttachments(args) {
|
|
|
22715
23022
|
log2(`[image-hook] failed to create image directory: ${e}`);
|
|
22716
23023
|
}
|
|
22717
23024
|
cleanupAllSessions(saveDir);
|
|
22718
|
-
for (const msg of
|
|
22719
|
-
if (msg.info.role !== "user")
|
|
22720
|
-
continue;
|
|
22721
|
-
const imageParts = msg.parts.filter(isImagePart);
|
|
22722
|
-
if (imageParts.length === 0)
|
|
22723
|
-
continue;
|
|
23025
|
+
for (const { msg, imageParts } of messagesWithImages) {
|
|
22724
23026
|
const sessionSubdir = msg.info.sessionID ? sanitizeFilename(msg.info.sessionID) : undefined;
|
|
22725
23027
|
const targetDir = sessionSubdir ? join7(saveDir, sessionSubdir) : saveDir;
|
|
22726
23028
|
try {
|
|
@@ -22880,6 +23182,255 @@ function createPostFileToolNudgeHook(options = {}) {
|
|
|
22880
23182
|
}
|
|
22881
23183
|
};
|
|
22882
23184
|
}
|
|
23185
|
+
// src/hooks/task-session-manager/index.ts
|
|
23186
|
+
import path8 from "node:path";
|
|
23187
|
+
var AGENT_NAME_SET = new Set([
|
|
23188
|
+
"orchestrator",
|
|
23189
|
+
"oracle",
|
|
23190
|
+
"designer",
|
|
23191
|
+
"explorer",
|
|
23192
|
+
"librarian",
|
|
23193
|
+
"fixer",
|
|
23194
|
+
"observer",
|
|
23195
|
+
"council",
|
|
23196
|
+
"councillor"
|
|
23197
|
+
]);
|
|
23198
|
+
var MAX_PENDING_TASK_CALLS = 100;
|
|
23199
|
+
function isAgentName(value) {
|
|
23200
|
+
return typeof value === "string" && AGENT_NAME_SET.has(value);
|
|
23201
|
+
}
|
|
23202
|
+
function isObjectRecord(value) {
|
|
23203
|
+
return typeof value === "object" && value !== null;
|
|
23204
|
+
}
|
|
23205
|
+
function extractPath(output) {
|
|
23206
|
+
return /<path>([^<]+)<\/path>/.exec(output)?.[1];
|
|
23207
|
+
}
|
|
23208
|
+
function normalizePath(root, file) {
|
|
23209
|
+
const relative = path8.relative(root, file);
|
|
23210
|
+
if (!relative || relative.startsWith("..") || path8.isAbsolute(relative)) {
|
|
23211
|
+
return file;
|
|
23212
|
+
}
|
|
23213
|
+
return relative;
|
|
23214
|
+
}
|
|
23215
|
+
function extractReadFiles(root, output) {
|
|
23216
|
+
if (typeof output.output !== "string")
|
|
23217
|
+
return [];
|
|
23218
|
+
const file = extractPath(output.output);
|
|
23219
|
+
if (!file)
|
|
23220
|
+
return [];
|
|
23221
|
+
return [
|
|
23222
|
+
{
|
|
23223
|
+
path: normalizePath(root, file),
|
|
23224
|
+
lineCount: countReadLines(output.output).length,
|
|
23225
|
+
lineNumbers: countReadLines(output.output),
|
|
23226
|
+
lastReadAt: Date.now()
|
|
23227
|
+
}
|
|
23228
|
+
];
|
|
23229
|
+
}
|
|
23230
|
+
function countReadLines(output) {
|
|
23231
|
+
const lines = new Set;
|
|
23232
|
+
for (const match of output.matchAll(/^([0-9]+):/gm)) {
|
|
23233
|
+
lines.add(Number(match[1]));
|
|
23234
|
+
}
|
|
23235
|
+
return [...lines];
|
|
23236
|
+
}
|
|
23237
|
+
function createTaskSessionManagerHook(_ctx, options) {
|
|
23238
|
+
const sessionManager = new SessionManager(options.maxSessionsPerAgent, {
|
|
23239
|
+
readContextMinLines: options.readContextMinLines,
|
|
23240
|
+
readContextMaxFiles: options.readContextMaxFiles
|
|
23241
|
+
});
|
|
23242
|
+
const pendingCalls = new Map;
|
|
23243
|
+
const pendingCallOrder = [];
|
|
23244
|
+
const contextByTask = new Map;
|
|
23245
|
+
const pendingManagedTaskIds = new Set;
|
|
23246
|
+
function addTaskContext(taskId, files) {
|
|
23247
|
+
if (files.length === 0)
|
|
23248
|
+
return;
|
|
23249
|
+
let context = contextByTask.get(taskId);
|
|
23250
|
+
if (!context) {
|
|
23251
|
+
context = new Map;
|
|
23252
|
+
contextByTask.set(taskId, context);
|
|
23253
|
+
}
|
|
23254
|
+
for (const file of files) {
|
|
23255
|
+
const pending = context.get(file.path) ?? {
|
|
23256
|
+
path: file.path,
|
|
23257
|
+
lines: new Set,
|
|
23258
|
+
lastReadAt: file.lastReadAt
|
|
23259
|
+
};
|
|
23260
|
+
for (const line of file.lineNumbers ?? []) {
|
|
23261
|
+
pending.lines.add(line);
|
|
23262
|
+
}
|
|
23263
|
+
pending.lastReadAt = Math.max(pending.lastReadAt, file.lastReadAt);
|
|
23264
|
+
context.set(file.path, pending);
|
|
23265
|
+
}
|
|
23266
|
+
sessionManager.addContext(taskId, contextFilesForPrompt(context));
|
|
23267
|
+
}
|
|
23268
|
+
function contextFilesForPrompt(context) {
|
|
23269
|
+
if (!context)
|
|
23270
|
+
return [];
|
|
23271
|
+
return [...context.values()].map((file) => ({
|
|
23272
|
+
path: file.path,
|
|
23273
|
+
lineCount: file.lines.size,
|
|
23274
|
+
lastReadAt: file.lastReadAt
|
|
23275
|
+
}));
|
|
23276
|
+
}
|
|
23277
|
+
function canTrackTaskContext(taskId) {
|
|
23278
|
+
return pendingManagedTaskIds.has(taskId) || sessionManager.taskIds().has(taskId);
|
|
23279
|
+
}
|
|
23280
|
+
function pruneContext() {
|
|
23281
|
+
const remembered = sessionManager.taskIds();
|
|
23282
|
+
for (const taskId of contextByTask.keys()) {
|
|
23283
|
+
if (!pendingManagedTaskIds.has(taskId) && !remembered.has(taskId)) {
|
|
23284
|
+
contextByTask.delete(taskId);
|
|
23285
|
+
}
|
|
23286
|
+
}
|
|
23287
|
+
}
|
|
23288
|
+
function isMissingRememberedSessionError(output) {
|
|
23289
|
+
const firstLine = output.split(/\r?\n/, 1)[0]?.trim().toLowerCase() ?? "";
|
|
23290
|
+
return firstLine.startsWith("[error]") && firstLine.includes("session") && (firstLine.includes("not found") || firstLine.includes("no session"));
|
|
23291
|
+
}
|
|
23292
|
+
function rememberPendingCall(call) {
|
|
23293
|
+
const existingIndex = pendingCallOrder.indexOf(call.callId);
|
|
23294
|
+
if (existingIndex >= 0) {
|
|
23295
|
+
pendingCallOrder.splice(existingIndex, 1);
|
|
23296
|
+
}
|
|
23297
|
+
pendingCalls.set(call.callId, call);
|
|
23298
|
+
pendingCallOrder.push(call.callId);
|
|
23299
|
+
while (pendingCallOrder.length > MAX_PENDING_TASK_CALLS) {
|
|
23300
|
+
const evictedCallId = pendingCallOrder.shift();
|
|
23301
|
+
if (!evictedCallId) {
|
|
23302
|
+
break;
|
|
23303
|
+
}
|
|
23304
|
+
pendingCalls.delete(evictedCallId);
|
|
23305
|
+
}
|
|
23306
|
+
}
|
|
23307
|
+
function takePendingCall(callId) {
|
|
23308
|
+
if (!callId)
|
|
23309
|
+
return;
|
|
23310
|
+
const pending = pendingCalls.get(callId);
|
|
23311
|
+
pendingCalls.delete(callId);
|
|
23312
|
+
const orderIndex = pendingCallOrder.indexOf(callId);
|
|
23313
|
+
if (orderIndex >= 0) {
|
|
23314
|
+
pendingCallOrder.splice(orderIndex, 1);
|
|
23315
|
+
}
|
|
23316
|
+
return pending;
|
|
23317
|
+
}
|
|
23318
|
+
return {
|
|
23319
|
+
"tool.execute.before": async (input, output) => {
|
|
23320
|
+
if (input.tool.toLowerCase() !== "task")
|
|
23321
|
+
return;
|
|
23322
|
+
if (!input.sessionID || !options.shouldManageSession(input.sessionID)) {
|
|
23323
|
+
return;
|
|
23324
|
+
}
|
|
23325
|
+
if (!isObjectRecord(output.args))
|
|
23326
|
+
return;
|
|
23327
|
+
const args = output.args;
|
|
23328
|
+
if (!isAgentName(args.subagent_type))
|
|
23329
|
+
return;
|
|
23330
|
+
const label = deriveTaskSessionLabel({
|
|
23331
|
+
description: typeof args.description === "string" ? args.description : undefined,
|
|
23332
|
+
prompt: typeof args.prompt === "string" ? args.prompt : undefined,
|
|
23333
|
+
agentType: args.subagent_type
|
|
23334
|
+
});
|
|
23335
|
+
if (input.callID) {
|
|
23336
|
+
rememberPendingCall({
|
|
23337
|
+
callId: input.callID,
|
|
23338
|
+
parentSessionId: input.sessionID,
|
|
23339
|
+
agentType: args.subagent_type,
|
|
23340
|
+
label
|
|
23341
|
+
});
|
|
23342
|
+
}
|
|
23343
|
+
if (typeof args.task_id !== "string" || args.task_id.trim() === "") {
|
|
23344
|
+
return;
|
|
23345
|
+
}
|
|
23346
|
+
const requested = args.task_id.trim();
|
|
23347
|
+
const remembered = sessionManager.resolve(input.sessionID, args.subagent_type, requested);
|
|
23348
|
+
if (!remembered) {
|
|
23349
|
+
delete args.task_id;
|
|
23350
|
+
return;
|
|
23351
|
+
}
|
|
23352
|
+
args.task_id = remembered.taskId;
|
|
23353
|
+
pendingManagedTaskIds.add(remembered.taskId);
|
|
23354
|
+
sessionManager.markUsed(input.sessionID, args.subagent_type, remembered.taskId);
|
|
23355
|
+
if (input.callID) {
|
|
23356
|
+
rememberPendingCall({
|
|
23357
|
+
callId: input.callID,
|
|
23358
|
+
parentSessionId: input.sessionID,
|
|
23359
|
+
agentType: args.subagent_type,
|
|
23360
|
+
label,
|
|
23361
|
+
resumedTaskId: remembered.taskId
|
|
23362
|
+
});
|
|
23363
|
+
}
|
|
23364
|
+
},
|
|
23365
|
+
"tool.execute.after": async (input, output) => {
|
|
23366
|
+
if (input.tool.toLowerCase() === "read") {
|
|
23367
|
+
if (input.sessionID && canTrackTaskContext(input.sessionID)) {
|
|
23368
|
+
addTaskContext(input.sessionID, extractReadFiles(_ctx.directory, output));
|
|
23369
|
+
}
|
|
23370
|
+
return;
|
|
23371
|
+
}
|
|
23372
|
+
if (input.tool.toLowerCase() !== "task")
|
|
23373
|
+
return;
|
|
23374
|
+
const pending = takePendingCall(input.callID);
|
|
23375
|
+
if (!pending || typeof output.output !== "string")
|
|
23376
|
+
return;
|
|
23377
|
+
const taskId = parseTaskIdFromTaskOutput(output.output);
|
|
23378
|
+
if (!taskId) {
|
|
23379
|
+
if (pending.resumedTaskId && isMissingRememberedSessionError(output.output)) {
|
|
23380
|
+
sessionManager.drop(pending.parentSessionId, pending.agentType, pending.resumedTaskId);
|
|
23381
|
+
}
|
|
23382
|
+
return;
|
|
23383
|
+
}
|
|
23384
|
+
if (pending.resumedTaskId && pending.resumedTaskId !== taskId) {
|
|
23385
|
+
sessionManager.drop(pending.parentSessionId, pending.agentType, pending.resumedTaskId);
|
|
23386
|
+
}
|
|
23387
|
+
sessionManager.remember({
|
|
23388
|
+
parentSessionId: pending.parentSessionId,
|
|
23389
|
+
taskId,
|
|
23390
|
+
agentType: pending.agentType,
|
|
23391
|
+
label: pending.label
|
|
23392
|
+
});
|
|
23393
|
+
pendingManagedTaskIds.delete(taskId);
|
|
23394
|
+
const contextFiles = contextFilesForPrompt(contextByTask.get(taskId));
|
|
23395
|
+
sessionManager.addContext(taskId, contextFiles);
|
|
23396
|
+
pruneContext();
|
|
23397
|
+
},
|
|
23398
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
23399
|
+
if (!input.sessionID || !options.shouldManageSession(input.sessionID)) {
|
|
23400
|
+
return;
|
|
23401
|
+
}
|
|
23402
|
+
const reminder = sessionManager.formatForPrompt(input.sessionID);
|
|
23403
|
+
if (!reminder)
|
|
23404
|
+
return;
|
|
23405
|
+
output.system.push(reminder);
|
|
23406
|
+
},
|
|
23407
|
+
event: async (input) => {
|
|
23408
|
+
if (input.event.type === "session.created") {
|
|
23409
|
+
const info = input.event.properties?.info;
|
|
23410
|
+
if (info?.id && info.parentID && options.shouldManageSession(info.parentID)) {
|
|
23411
|
+
pendingManagedTaskIds.add(info.id);
|
|
23412
|
+
}
|
|
23413
|
+
return;
|
|
23414
|
+
}
|
|
23415
|
+
if (input.event.type !== "session.deleted")
|
|
23416
|
+
return;
|
|
23417
|
+
const sessionId = input.event.properties?.info?.id ?? input.event.properties?.sessionID;
|
|
23418
|
+
if (!sessionId)
|
|
23419
|
+
return;
|
|
23420
|
+
sessionManager.clearParent(sessionId);
|
|
23421
|
+
sessionManager.dropTask(sessionId);
|
|
23422
|
+
contextByTask.delete(sessionId);
|
|
23423
|
+
pendingManagedTaskIds.delete(sessionId);
|
|
23424
|
+
pruneContext();
|
|
23425
|
+
for (const [callId, pending] of pendingCalls.entries()) {
|
|
23426
|
+
if (pending.parentSessionId !== sessionId) {
|
|
23427
|
+
continue;
|
|
23428
|
+
}
|
|
23429
|
+
takePendingCall(callId);
|
|
23430
|
+
}
|
|
23431
|
+
}
|
|
23432
|
+
};
|
|
23433
|
+
}
|
|
22883
23434
|
// src/hooks/todo-continuation/index.ts
|
|
22884
23435
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
22885
23436
|
|
|
@@ -23542,7 +24093,7 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
23542
24093
|
};
|
|
23543
24094
|
}
|
|
23544
24095
|
// src/interview/manager.ts
|
|
23545
|
-
import
|
|
24096
|
+
import path12 from "node:path";
|
|
23546
24097
|
|
|
23547
24098
|
// src/interview/dashboard.ts
|
|
23548
24099
|
import crypto from "node:crypto";
|
|
@@ -23552,27 +24103,27 @@ import {
|
|
|
23552
24103
|
createServer
|
|
23553
24104
|
} from "node:http";
|
|
23554
24105
|
import os3 from "node:os";
|
|
23555
|
-
import
|
|
24106
|
+
import path10 from "node:path";
|
|
23556
24107
|
import { URL as URL2 } from "node:url";
|
|
23557
24108
|
|
|
23558
24109
|
// src/interview/document.ts
|
|
23559
24110
|
import * as fsSync from "node:fs";
|
|
23560
24111
|
import * as fs6 from "node:fs/promises";
|
|
23561
|
-
import * as
|
|
24112
|
+
import * as path9 from "node:path";
|
|
23562
24113
|
var DEFAULT_OUTPUT_FOLDER = "interview";
|
|
23563
24114
|
function normalizeOutputFolder(outputFolder) {
|
|
23564
24115
|
const normalized = outputFolder.trim().replace(/^\/+|\/+$/g, "");
|
|
23565
24116
|
return normalized || DEFAULT_OUTPUT_FOLDER;
|
|
23566
24117
|
}
|
|
23567
24118
|
function createInterviewDirectoryPath(directory, outputFolder) {
|
|
23568
|
-
return
|
|
24119
|
+
return path9.join(directory, normalizeOutputFolder(outputFolder));
|
|
23569
24120
|
}
|
|
23570
24121
|
function createInterviewFilePath(directory, outputFolder, idea) {
|
|
23571
24122
|
const fileName = `${slugify(idea) || "interview"}.md`;
|
|
23572
|
-
return
|
|
24123
|
+
return path9.join(createInterviewDirectoryPath(directory, outputFolder), fileName);
|
|
23573
24124
|
}
|
|
23574
24125
|
function relativeInterviewPath(directory, filePath) {
|
|
23575
|
-
return
|
|
24126
|
+
return path9.relative(directory, filePath) || path9.basename(filePath);
|
|
23576
24127
|
}
|
|
23577
24128
|
function resolveExistingInterviewPath(directory, outputFolder, value) {
|
|
23578
24129
|
const trimmed = value.trim();
|
|
@@ -23581,22 +24132,22 @@ function resolveExistingInterviewPath(directory, outputFolder, value) {
|
|
|
23581
24132
|
}
|
|
23582
24133
|
const outputDir = createInterviewDirectoryPath(directory, outputFolder);
|
|
23583
24134
|
const candidates = new Set;
|
|
23584
|
-
const resolvedRoot =
|
|
23585
|
-
if (
|
|
24135
|
+
const resolvedRoot = path9.resolve(directory);
|
|
24136
|
+
if (path9.isAbsolute(trimmed)) {
|
|
23586
24137
|
candidates.add(trimmed);
|
|
23587
24138
|
} else {
|
|
23588
|
-
candidates.add(
|
|
23589
|
-
candidates.add(
|
|
24139
|
+
candidates.add(path9.resolve(directory, trimmed));
|
|
24140
|
+
candidates.add(path9.join(outputDir, trimmed));
|
|
23590
24141
|
if (!trimmed.endsWith(".md")) {
|
|
23591
|
-
candidates.add(
|
|
24142
|
+
candidates.add(path9.join(outputDir, `${trimmed}.md`));
|
|
23592
24143
|
}
|
|
23593
24144
|
}
|
|
23594
24145
|
for (const candidate of candidates) {
|
|
23595
|
-
if (
|
|
24146
|
+
if (path9.extname(candidate) !== ".md") {
|
|
23596
24147
|
continue;
|
|
23597
24148
|
}
|
|
23598
|
-
const resolved =
|
|
23599
|
-
if (!resolved.startsWith(resolvedRoot +
|
|
24149
|
+
const resolved = path9.resolve(candidate);
|
|
24150
|
+
if (!resolved.startsWith(resolvedRoot + path9.sep) && resolved !== resolvedRoot) {
|
|
23600
24151
|
continue;
|
|
23601
24152
|
}
|
|
23602
24153
|
if (fsSync.existsSync(candidate)) {
|
|
@@ -23676,7 +24227,7 @@ function parseFrontmatter(content) {
|
|
|
23676
24227
|
return result;
|
|
23677
24228
|
}
|
|
23678
24229
|
async function ensureInterviewFile(record) {
|
|
23679
|
-
await fs6.mkdir(
|
|
24230
|
+
await fs6.mkdir(path9.dirname(record.markdownPath), { recursive: true });
|
|
23680
24231
|
try {
|
|
23681
24232
|
await fs6.access(record.markdownPath);
|
|
23682
24233
|
} catch {
|
|
@@ -23764,6 +24315,7 @@ async function readJsonBody(request) {
|
|
|
23764
24315
|
}
|
|
23765
24316
|
|
|
23766
24317
|
// src/interview/ui.ts
|
|
24318
|
+
var BRAND_LOGO_URL = "https://ohmyopencodeslim.com/android-chrome-512x512.png";
|
|
23767
24319
|
function escapeHtml(value) {
|
|
23768
24320
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
23769
24321
|
}
|
|
@@ -23861,12 +24413,8 @@ function sharedStyles() {
|
|
|
23861
24413
|
}
|
|
23862
24414
|
.footer { margin-top: 32px; text-align: center; font-size: 13px; color: rgba(255,255,255,0.4); }`;
|
|
23863
24415
|
}
|
|
23864
|
-
function
|
|
23865
|
-
return `<
|
|
23866
|
-
<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"/>
|
|
23867
|
-
<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"/>
|
|
23868
|
-
<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"/>
|
|
23869
|
-
</svg>`;
|
|
24416
|
+
function brandImage(size) {
|
|
24417
|
+
return `<img class="brand-mark" src="${BRAND_LOGO_URL}" alt="Oh My Opencode Slim" width="${size}" height="${size}" />`;
|
|
23870
24418
|
}
|
|
23871
24419
|
function renderDashboardPage(interviews, files, outputFolder) {
|
|
23872
24420
|
const activeHtml = interviews.length === 0 ? "" : interviews.map((item) => {
|
|
@@ -24075,7 +24623,7 @@ function renderDashboardPage(interviews, files, outputFolder) {
|
|
|
24075
24623
|
<body>
|
|
24076
24624
|
<div class="wrap">
|
|
24077
24625
|
<div class="brand-header">
|
|
24078
|
-
${
|
|
24626
|
+
${brandImage(96)}
|
|
24079
24627
|
<h1>Interviews</h1>
|
|
24080
24628
|
<p class="muted">${totalCount} item${totalCount === 1 ? "" : "s"}</p>
|
|
24081
24629
|
</div>
|
|
@@ -24276,6 +24824,37 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
24276
24824
|
}
|
|
24277
24825
|
|
|
24278
24826
|
.options { display: flex; flex-direction: column; gap: 10px; margin-bottom: 20px; }
|
|
24827
|
+
.question-hint {
|
|
24828
|
+
display: flex;
|
|
24829
|
+
flex-wrap: wrap;
|
|
24830
|
+
gap: 8px;
|
|
24831
|
+
margin: -4px 0 16px;
|
|
24832
|
+
color: rgba(255,255,255,0.5);
|
|
24833
|
+
font-size: 13px;
|
|
24834
|
+
line-height: 1.5;
|
|
24835
|
+
transition: color 0.2s ease;
|
|
24836
|
+
}
|
|
24837
|
+
.active-question .question-hint {
|
|
24838
|
+
color: rgba(255,255,255,0.78);
|
|
24839
|
+
}
|
|
24840
|
+
.hint-chip {
|
|
24841
|
+
display: inline-flex;
|
|
24842
|
+
align-items: center;
|
|
24843
|
+
gap: 6px;
|
|
24844
|
+
padding: 4px 10px;
|
|
24845
|
+
border-radius: 999px;
|
|
24846
|
+
background: rgba(255,255,255,0.06);
|
|
24847
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
24848
|
+
}
|
|
24849
|
+
.hint-chip kbd {
|
|
24850
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
24851
|
+
font-size: 12px;
|
|
24852
|
+
padding: 2px 6px;
|
|
24853
|
+
border-radius: 6px;
|
|
24854
|
+
background: rgba(255,255,255,0.12);
|
|
24855
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
24856
|
+
color: rgba(255,255,255,0.95);
|
|
24857
|
+
}
|
|
24279
24858
|
|
|
24280
24859
|
.option {
|
|
24281
24860
|
border: 1px solid rgba(255,255,255,0.1);
|
|
@@ -24555,7 +25134,7 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
24555
25134
|
<div class="wrap">
|
|
24556
25135
|
<a href="/" class="back-link">← All Interviews</a>
|
|
24557
25136
|
<div class="brand-header">
|
|
24558
|
-
${
|
|
25137
|
+
${brandImage(144)}
|
|
24559
25138
|
</div>
|
|
24560
25139
|
<h1 id="idea">Connecting...</h1>
|
|
24561
25140
|
<p class="muted" id="summary">Preparing interview session</p>
|
|
@@ -24596,7 +25175,15 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
24596
25175
|
${clipboardHelperJs()}
|
|
24597
25176
|
const interviewId = ${JSON.stringify(interviewId).replace(/</g, "\\u003c")};
|
|
24598
25177
|
const resumeSlug = ${JSON.stringify(resumeSlug).replace(/</g, "\\u003c")};
|
|
24599
|
-
const state = {
|
|
25178
|
+
const state = {
|
|
25179
|
+
data: null,
|
|
25180
|
+
answers: {},
|
|
25181
|
+
activeQuestionIndex: 0,
|
|
25182
|
+
lastQuestionIds: [],
|
|
25183
|
+
lastSig: null,
|
|
25184
|
+
customMode: {},
|
|
25185
|
+
isSubmitting: false,
|
|
25186
|
+
};
|
|
24600
25187
|
|
|
24601
25188
|
function updateSubmitButton() {
|
|
24602
25189
|
const button = document.getElementById('submitButton');
|
|
@@ -24609,7 +25196,11 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
24609
25196
|
const allAnswered = questions.every((question) =>
|
|
24610
25197
|
(state.answers[question.id] || '').trim().length > 0,
|
|
24611
25198
|
);
|
|
24612
|
-
button.disabled =
|
|
25199
|
+
button.disabled =
|
|
25200
|
+
state.data.isBusy ||
|
|
25201
|
+
state.isSubmitting ||
|
|
25202
|
+
!questions.length ||
|
|
25203
|
+
!allAnswered;
|
|
24613
25204
|
const hideSubmit = ['completed', 'session-disconnected'];
|
|
24614
25205
|
button.style.display = hideSubmit.includes(state.data.mode) ? 'none' : '';
|
|
24615
25206
|
|
|
@@ -24760,6 +25351,54 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
24760
25351
|
});
|
|
24761
25352
|
}
|
|
24762
25353
|
|
|
25354
|
+
function scrollToActiveQuestion(behavior) {
|
|
25355
|
+
const questions = state.data?.questions || [];
|
|
25356
|
+
const activeQ = questions[state.activeQuestionIndex];
|
|
25357
|
+
if (!activeQ) return;
|
|
25358
|
+
|
|
25359
|
+
const wrapper = document.getElementById('question-' + activeQ.id);
|
|
25360
|
+
if (wrapper) {
|
|
25361
|
+
wrapper.scrollIntoView({ behavior, block: 'center' });
|
|
25362
|
+
}
|
|
25363
|
+
}
|
|
25364
|
+
|
|
25365
|
+
function syncActiveQuestionIndex(questions) {
|
|
25366
|
+
if (!questions.length) {
|
|
25367
|
+
state.activeQuestionIndex = 0;
|
|
25368
|
+
state.lastQuestionIds = [];
|
|
25369
|
+
return;
|
|
25370
|
+
}
|
|
25371
|
+
|
|
25372
|
+
const nextQuestionIds = questions.map((question) => question.id);
|
|
25373
|
+
const previousQuestionIds = state.lastQuestionIds || [];
|
|
25374
|
+
const activeQuestionId = previousQuestionIds[state.activeQuestionIndex];
|
|
25375
|
+
const nextActiveIndex = activeQuestionId
|
|
25376
|
+
? nextQuestionIds.indexOf(activeQuestionId)
|
|
25377
|
+
: -1;
|
|
25378
|
+
|
|
25379
|
+
if (nextActiveIndex >= 0) {
|
|
25380
|
+
state.activeQuestionIndex = nextActiveIndex;
|
|
25381
|
+
} else {
|
|
25382
|
+
state.activeQuestionIndex = 0;
|
|
25383
|
+
}
|
|
25384
|
+
|
|
25385
|
+
state.lastQuestionIds = nextQuestionIds;
|
|
25386
|
+
}
|
|
25387
|
+
|
|
25388
|
+
function isTextEntryTarget(target) {
|
|
25389
|
+
return target &&
|
|
25390
|
+
(target.tagName === 'TEXTAREA' ||
|
|
25391
|
+
target.tagName === 'INPUT' ||
|
|
25392
|
+
target.isContentEditable);
|
|
25393
|
+
}
|
|
25394
|
+
|
|
25395
|
+
function isShortcutBlockedTarget(target) {
|
|
25396
|
+
if (!target) return false;
|
|
25397
|
+
return !!target.closest(
|
|
25398
|
+
'button, a, select, summary, textarea, input, [contenteditable="true"]',
|
|
25399
|
+
);
|
|
25400
|
+
}
|
|
25401
|
+
|
|
24763
25402
|
document.addEventListener('keydown', (e) => {
|
|
24764
25403
|
const isSubmitShortcut =
|
|
24765
25404
|
(e.key === 'Enter' && (e.metaKey || e.ctrlKey)) ||
|
|
@@ -24773,12 +25412,41 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
24773
25412
|
return;
|
|
24774
25413
|
}
|
|
24775
25414
|
|
|
24776
|
-
if (e.target
|
|
25415
|
+
if (isTextEntryTarget(e.target)) return;
|
|
24777
25416
|
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
|
24778
25417
|
|
|
24779
25418
|
const questions = state.data?.questions || [];
|
|
24780
25419
|
if (!questions.length) return;
|
|
24781
25420
|
|
|
25421
|
+
if (e.key === 'Enter') {
|
|
25422
|
+
if (e.repeat) {
|
|
25423
|
+
e.preventDefault();
|
|
25424
|
+
return;
|
|
25425
|
+
}
|
|
25426
|
+
if (isShortcutBlockedTarget(e.target)) return;
|
|
25427
|
+
if (state.data.isBusy || state.isSubmitting) return;
|
|
25428
|
+
|
|
25429
|
+
const activeQ = questions[state.activeQuestionIndex];
|
|
25430
|
+
if (!activeQ) return;
|
|
25431
|
+
|
|
25432
|
+
const answer = (state.answers[activeQ.id] || '').trim();
|
|
25433
|
+
if (!answer) return;
|
|
25434
|
+
|
|
25435
|
+
const isLastQuestion =
|
|
25436
|
+
state.activeQuestionIndex === questions.length - 1;
|
|
25437
|
+
if (isLastQuestion) {
|
|
25438
|
+
const submitBtn = document.getElementById('submitButton');
|
|
25439
|
+
if (submitBtn && !submitBtn.disabled) {
|
|
25440
|
+
submitBtn.click();
|
|
25441
|
+
}
|
|
25442
|
+
} else {
|
|
25443
|
+
advanceToNextQuestion(activeQ.id);
|
|
25444
|
+
}
|
|
25445
|
+
|
|
25446
|
+
e.preventDefault();
|
|
25447
|
+
return;
|
|
25448
|
+
}
|
|
25449
|
+
|
|
24782
25450
|
const num = parseInt(e.key, 10);
|
|
24783
25451
|
if (num >= 1 && num <= 9) {
|
|
24784
25452
|
const activeQ = questions[state.activeQuestionIndex];
|
|
@@ -24946,6 +25614,10 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
24946
25614
|
function renderQuestions(questions) {
|
|
24947
25615
|
const sig = JSON.stringify([questions, state.data?.mode]);
|
|
24948
25616
|
const container = document.getElementById('questions');
|
|
25617
|
+
const previousActiveQuestionId =
|
|
25618
|
+
state.lastQuestionIds[state.activeQuestionIndex];
|
|
25619
|
+
|
|
25620
|
+
syncActiveQuestionIndex(questions);
|
|
24949
25621
|
|
|
24950
25622
|
if (state.lastSig === sig) {
|
|
24951
25623
|
questions.forEach((q) => updateOptionsDOM(q.id));
|
|
@@ -24986,6 +25658,15 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
24986
25658
|
wrapper.appendChild(title);
|
|
24987
25659
|
|
|
24988
25660
|
const predefined = question.options || [];
|
|
25661
|
+
if (predefined.length) {
|
|
25662
|
+
const hint = document.createElement('div');
|
|
25663
|
+
hint.className = 'question-hint';
|
|
25664
|
+
hint.innerHTML =
|
|
25665
|
+
'<span class="hint-chip"><kbd>1-9</kbd><span>Choose an option</span></span>' +
|
|
25666
|
+
'<span class="hint-chip"><kbd>Enter</kbd><span>Accept selected answer</span></span>';
|
|
25667
|
+
wrapper.appendChild(hint);
|
|
25668
|
+
}
|
|
25669
|
+
|
|
24989
25670
|
if (predefined.length) {
|
|
24990
25671
|
const options = document.createElement('div');
|
|
24991
25672
|
options.className = 'options';
|
|
@@ -25021,6 +25702,13 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
25021
25702
|
|
|
25022
25703
|
updateActiveQuestionFocus();
|
|
25023
25704
|
questions.forEach(q => updateOptionsDOM(q.id));
|
|
25705
|
+
const currentActiveQuestionId = questions[state.activeQuestionIndex]?.id;
|
|
25706
|
+
if (
|
|
25707
|
+
questions.length > 0 &&
|
|
25708
|
+
previousActiveQuestionId !== currentActiveQuestionId
|
|
25709
|
+
) {
|
|
25710
|
+
scrollToActiveQuestion('smooth');
|
|
25711
|
+
}
|
|
25024
25712
|
}
|
|
25025
25713
|
|
|
25026
25714
|
function render(data) {
|
|
@@ -25094,7 +25782,8 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
25094
25782
|
}
|
|
25095
25783
|
|
|
25096
25784
|
document.getElementById('submitButton').addEventListener('click', async () => {
|
|
25097
|
-
if (!state.data) return;
|
|
25785
|
+
if (!state.data || state.isSubmitting) return;
|
|
25786
|
+
document.getElementById('submitButton').blur();
|
|
25098
25787
|
const answers = (state.data.questions || []).map((question) => {
|
|
25099
25788
|
return {
|
|
25100
25789
|
questionId: question.id,
|
|
@@ -25102,6 +25791,9 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
25102
25791
|
};
|
|
25103
25792
|
});
|
|
25104
25793
|
|
|
25794
|
+
state.isSubmitting = true;
|
|
25795
|
+
updateSubmitButton();
|
|
25796
|
+
|
|
25105
25797
|
const overlay = document.getElementById('loadingOverlay');
|
|
25106
25798
|
const overlayText = document.getElementById('loadingText');
|
|
25107
25799
|
overlay.classList.add('active');
|
|
@@ -25119,6 +25811,8 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
25119
25811
|
} catch (err) {
|
|
25120
25812
|
document.getElementById('submitStatus').textContent = 'Error submitting answers.';
|
|
25121
25813
|
}
|
|
25814
|
+
state.isSubmitting = false;
|
|
25815
|
+
updateSubmitButton();
|
|
25122
25816
|
try {
|
|
25123
25817
|
await refresh();
|
|
25124
25818
|
} catch (_error) {
|
|
@@ -25203,12 +25897,12 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
25203
25897
|
|
|
25204
25898
|
// src/interview/dashboard.ts
|
|
25205
25899
|
function getAuthFilePath(port) {
|
|
25206
|
-
const dataHome = process.env.XDG_DATA_HOME ||
|
|
25207
|
-
return
|
|
25900
|
+
const dataHome = process.env.XDG_DATA_HOME || path10.join(os3.homedir(), ".local", "share");
|
|
25901
|
+
return path10.join(dataHome, "opencode", `.dashboard-${port}.json`);
|
|
25208
25902
|
}
|
|
25209
25903
|
function writeAuthFile(port, token) {
|
|
25210
25904
|
const filePath = getAuthFilePath(port);
|
|
25211
|
-
const dir =
|
|
25905
|
+
const dir = path10.dirname(filePath);
|
|
25212
25906
|
try {
|
|
25213
25907
|
fsSync2.mkdirSync(dir, { recursive: true });
|
|
25214
25908
|
} catch {}
|
|
@@ -25345,7 +26039,7 @@ function createDashboardServer(config) {
|
|
|
25345
26039
|
const directories = getKnownDirectories();
|
|
25346
26040
|
const items = [];
|
|
25347
26041
|
for (const dir of directories) {
|
|
25348
|
-
const interviewDir =
|
|
26042
|
+
const interviewDir = path10.join(dir, config.outputFolder);
|
|
25349
26043
|
let entries;
|
|
25350
26044
|
try {
|
|
25351
26045
|
entries = await fs7.readdir(interviewDir);
|
|
@@ -25357,7 +26051,7 @@ function createDashboardServer(config) {
|
|
|
25357
26051
|
continue;
|
|
25358
26052
|
let content;
|
|
25359
26053
|
try {
|
|
25360
|
-
content = await fs7.readFile(
|
|
26054
|
+
content = await fs7.readFile(path10.join(interviewDir, entry), "utf8");
|
|
25361
26055
|
} catch {
|
|
25362
26056
|
continue;
|
|
25363
26057
|
}
|
|
@@ -25383,7 +26077,7 @@ function createDashboardServer(config) {
|
|
|
25383
26077
|
const directories = getKnownDirectories();
|
|
25384
26078
|
let rebuilt = 0;
|
|
25385
26079
|
for (const dir of directories) {
|
|
25386
|
-
const interviewDir =
|
|
26080
|
+
const interviewDir = path10.join(dir, config.outputFolder);
|
|
25387
26081
|
let entries;
|
|
25388
26082
|
try {
|
|
25389
26083
|
entries = await fs7.readdir(interviewDir);
|
|
@@ -25395,7 +26089,7 @@ function createDashboardServer(config) {
|
|
|
25395
26089
|
continue;
|
|
25396
26090
|
let content;
|
|
25397
26091
|
try {
|
|
25398
|
-
content = await fs7.readFile(
|
|
26092
|
+
content = await fs7.readFile(path10.join(interviewDir, entry), "utf8");
|
|
25399
26093
|
} catch {
|
|
25400
26094
|
continue;
|
|
25401
26095
|
}
|
|
@@ -25421,7 +26115,7 @@ function createDashboardServer(config) {
|
|
|
25421
26115
|
questions: [],
|
|
25422
26116
|
pendingAnswers: null,
|
|
25423
26117
|
lastUpdatedAt: fm.updatedAt ? new Date(fm.updatedAt).getTime() : Date.now(),
|
|
25424
|
-
filePath:
|
|
26118
|
+
filePath: path10.join(interviewDir, entry),
|
|
25425
26119
|
nudgeAction: null
|
|
25426
26120
|
});
|
|
25427
26121
|
if (!sessions.has(fm.sessionID)) {
|
|
@@ -25685,7 +26379,7 @@ function createDashboardServer(config) {
|
|
|
25685
26379
|
const dirs = getKnownDirectories();
|
|
25686
26380
|
for (const dir of dirs) {
|
|
25687
26381
|
const slug = extractResumeSlug(interviewId);
|
|
25688
|
-
const candidate =
|
|
26382
|
+
const candidate = path10.join(dir, config.outputFolder, `${slug}.md`);
|
|
25689
26383
|
try {
|
|
25690
26384
|
document = await fs7.readFile(candidate, "utf8");
|
|
25691
26385
|
markdownPath = candidate;
|
|
@@ -26213,7 +26907,7 @@ function createInterviewServer(deps) {
|
|
|
26213
26907
|
// src/interview/service.ts
|
|
26214
26908
|
import { spawn } from "node:child_process";
|
|
26215
26909
|
import * as fs8 from "node:fs/promises";
|
|
26216
|
-
import * as
|
|
26910
|
+
import * as path11 from "node:path";
|
|
26217
26911
|
|
|
26218
26912
|
// src/interview/types.ts
|
|
26219
26913
|
import { z as z3 } from "zod";
|
|
@@ -26438,6 +27132,7 @@ function createInterviewService(ctx, config, deps) {
|
|
|
26438
27132
|
const activeInterviewIds = new Map;
|
|
26439
27133
|
const interviewsById = new Map;
|
|
26440
27134
|
const sessionBusy = new Map;
|
|
27135
|
+
const sessionModel = new Map;
|
|
26441
27136
|
const browserOpened = new Set;
|
|
26442
27137
|
let resolveBaseUrl = null;
|
|
26443
27138
|
let onStateChange = null;
|
|
@@ -26479,12 +27174,12 @@ function createInterviewService(ctx, config, deps) {
|
|
|
26479
27174
|
if (!newSlug) {
|
|
26480
27175
|
return;
|
|
26481
27176
|
}
|
|
26482
|
-
const currentFileName =
|
|
27177
|
+
const currentFileName = path11.basename(interview.markdownPath, ".md");
|
|
26483
27178
|
if (currentFileName === newSlug) {
|
|
26484
27179
|
return;
|
|
26485
27180
|
}
|
|
26486
|
-
const dir =
|
|
26487
|
-
const newPath =
|
|
27181
|
+
const dir = path11.dirname(interview.markdownPath);
|
|
27182
|
+
const newPath = path11.join(dir, `${newSlug}.md`);
|
|
26488
27183
|
try {
|
|
26489
27184
|
await fs8.access(newPath);
|
|
26490
27185
|
return;
|
|
@@ -26560,9 +27255,9 @@ function createInterviewService(ctx, config, deps) {
|
|
|
26560
27255
|
const messages = await loadMessages(sessionID);
|
|
26561
27256
|
const title = extractTitle(document);
|
|
26562
27257
|
const record = {
|
|
26563
|
-
id: `${Date.now()}-${++idCounter}-${slugify(
|
|
27258
|
+
id: `${Date.now()}-${++idCounter}-${slugify(path11.basename(markdownPath, ".md")) || "interview"}`,
|
|
26564
27259
|
sessionID,
|
|
26565
|
-
idea: title ||
|
|
27260
|
+
idea: title || path11.basename(markdownPath, ".md"),
|
|
26566
27261
|
markdownPath,
|
|
26567
27262
|
createdAt: nowIso(),
|
|
26568
27263
|
status: "active",
|
|
@@ -26693,10 +27388,12 @@ function createInterviewService(ctx, config, deps) {
|
|
|
26693
27388
|
}
|
|
26694
27389
|
await appendInterviewAnswers(interview, state.questions, answers);
|
|
26695
27390
|
const prompt = buildAnswerPrompt(answers, state.questions, maxQuestions);
|
|
27391
|
+
const model = sessionModel.get(interview.sessionID);
|
|
26696
27392
|
await ctx.client.session.promptAsync({
|
|
26697
27393
|
path: { id: interview.sessionID },
|
|
26698
27394
|
body: {
|
|
26699
|
-
parts: [createInternalAgentTextPart(prompt)]
|
|
27395
|
+
parts: [createInternalAgentTextPart(prompt)],
|
|
27396
|
+
...model ? { model: parseModelReference(model) ?? undefined } : {}
|
|
26700
27397
|
}
|
|
26701
27398
|
});
|
|
26702
27399
|
promptSent = true;
|
|
@@ -26746,12 +27443,23 @@ function createInterviewService(ctx, config, deps) {
|
|
|
26746
27443
|
}
|
|
26747
27444
|
return;
|
|
26748
27445
|
}
|
|
27446
|
+
if (event.type === "message.updated") {
|
|
27447
|
+
const info = properties;
|
|
27448
|
+
const sessionID = info?.info?.sessionID;
|
|
27449
|
+
const providerID = info?.info?.providerID;
|
|
27450
|
+
const modelID = info?.info?.modelID;
|
|
27451
|
+
if (sessionID && providerID && modelID) {
|
|
27452
|
+
sessionModel.set(sessionID, `${providerID}/${modelID}`);
|
|
27453
|
+
}
|
|
27454
|
+
return;
|
|
27455
|
+
}
|
|
26749
27456
|
if (event.type === "session.deleted") {
|
|
26750
27457
|
const deletedSessionId = (properties.info?.id ?? properties.sessionID) || null;
|
|
26751
27458
|
if (!deletedSessionId) {
|
|
26752
27459
|
return;
|
|
26753
27460
|
}
|
|
26754
27461
|
sessionBusy.delete(deletedSessionId);
|
|
27462
|
+
sessionModel.delete(deletedSessionId);
|
|
26755
27463
|
const interviewId = activeInterviewIds.get(deletedSessionId);
|
|
26756
27464
|
if (!interviewId) {
|
|
26757
27465
|
return;
|
|
@@ -26776,7 +27484,7 @@ function createInterviewService(ctx, config, deps) {
|
|
|
26776
27484
|
return fileCache.items;
|
|
26777
27485
|
}
|
|
26778
27486
|
const outputDir = createInterviewDirectoryPath(ctx.directory, outputFolder);
|
|
26779
|
-
const activePaths = new Set([...interviewsById.values()].filter((i) => i.status === "active").map((i) =>
|
|
27487
|
+
const activePaths = new Set([...interviewsById.values()].filter((i) => i.status === "active").map((i) => path11.resolve(i.markdownPath)));
|
|
26780
27488
|
let entries;
|
|
26781
27489
|
try {
|
|
26782
27490
|
entries = await fs8.readdir(outputDir);
|
|
@@ -26787,8 +27495,8 @@ function createInterviewService(ctx, config, deps) {
|
|
|
26787
27495
|
for (const entry of entries) {
|
|
26788
27496
|
if (!entry.endsWith(".md"))
|
|
26789
27497
|
continue;
|
|
26790
|
-
const fullPath =
|
|
26791
|
-
if (activePaths.has(
|
|
27498
|
+
const fullPath = path11.join(outputDir, entry);
|
|
27499
|
+
if (activePaths.has(path11.resolve(fullPath)))
|
|
26792
27500
|
continue;
|
|
26793
27501
|
let content;
|
|
26794
27502
|
try {
|
|
@@ -26848,10 +27556,12 @@ function createInterviewService(ctx, config, deps) {
|
|
|
26848
27556
|
].join(`
|
|
26849
27557
|
`);
|
|
26850
27558
|
}
|
|
27559
|
+
const model = sessionModel.get(interview.sessionID);
|
|
26851
27560
|
await ctx.client.session.promptAsync({
|
|
26852
27561
|
path: { id: interview.sessionID },
|
|
26853
27562
|
body: {
|
|
26854
|
-
parts: [createInternalAgentTextPart(prompt)]
|
|
27563
|
+
parts: [createInternalAgentTextPart(prompt)],
|
|
27564
|
+
...model ? { model: parseModelReference(model) ?? undefined } : {}
|
|
26855
27565
|
}
|
|
26856
27566
|
});
|
|
26857
27567
|
promptSent = true;
|
|
@@ -26885,7 +27595,7 @@ function createInterviewManager(ctx, config) {
|
|
|
26885
27595
|
const outputFolder = interviewConfig?.outputFolder ?? "interview";
|
|
26886
27596
|
if (!dashboardEnabled) {
|
|
26887
27597
|
const service2 = createInterviewService(ctx, interviewConfig);
|
|
26888
|
-
const resolvedOutputPath =
|
|
27598
|
+
const resolvedOutputPath = path12.join(ctx.directory, outputFolder);
|
|
26889
27599
|
const server = createInterviewServer({
|
|
26890
27600
|
getState: async (interviewId) => service2.getInterviewState(interviewId),
|
|
26891
27601
|
listInterviewFiles: async () => service2.listInterviewFiles(),
|
|
@@ -26990,7 +27700,7 @@ function createInterviewManager(ctx, config) {
|
|
|
26990
27700
|
listInterviews: () => service.listInterviews(),
|
|
26991
27701
|
submitAnswers: async (interviewId, answers) => service.submitAnswers(interviewId, answers),
|
|
26992
27702
|
handleNudgeAction: async (interviewId, action) => service.handleNudgeAction(interviewId, action),
|
|
26993
|
-
outputFolder:
|
|
27703
|
+
outputFolder: path12.join(ctx.directory, outputFolder),
|
|
26994
27704
|
port: 0
|
|
26995
27705
|
});
|
|
26996
27706
|
service.setBaseUrlResolver(() => perSessionServer.ensureStarted());
|
|
@@ -27430,23 +28140,23 @@ class TmuxMultiplexer {
|
|
|
27430
28140
|
return null;
|
|
27431
28141
|
}
|
|
27432
28142
|
const stdout = await proc.stdout();
|
|
27433
|
-
const
|
|
28143
|
+
const path13 = stdout.trim().split(`
|
|
27434
28144
|
`)[0];
|
|
27435
|
-
if (!
|
|
28145
|
+
if (!path13) {
|
|
27436
28146
|
log("[tmux] findBinary: no path in output");
|
|
27437
28147
|
return null;
|
|
27438
28148
|
}
|
|
27439
|
-
const verifyProc = crossSpawn([
|
|
28149
|
+
const verifyProc = crossSpawn([path13, "-V"], {
|
|
27440
28150
|
stdout: "pipe",
|
|
27441
28151
|
stderr: "pipe"
|
|
27442
28152
|
});
|
|
27443
28153
|
const verifyExit = await verifyProc.exited;
|
|
27444
28154
|
if (verifyExit !== 0) {
|
|
27445
|
-
log("[tmux] findBinary: tmux -V failed", { path:
|
|
28155
|
+
log("[tmux] findBinary: tmux -V failed", { path: path13, verifyExit });
|
|
27446
28156
|
return null;
|
|
27447
28157
|
}
|
|
27448
|
-
log("[tmux] findBinary: found", { path:
|
|
27449
|
-
return
|
|
28158
|
+
log("[tmux] findBinary: found", { path: path13 });
|
|
28159
|
+
return path13;
|
|
27450
28160
|
} catch (err) {
|
|
27451
28161
|
log("[tmux] findBinary: exception", { error: String(err) });
|
|
27452
28162
|
return null;
|
|
@@ -27825,6 +28535,8 @@ class MultiplexerSessionManager {
|
|
|
27825
28535
|
directory;
|
|
27826
28536
|
multiplexer = null;
|
|
27827
28537
|
sessions = new Map;
|
|
28538
|
+
knownSessions = new Map;
|
|
28539
|
+
spawningSessions = new Set;
|
|
27828
28540
|
pollInterval;
|
|
27829
28541
|
enabled = false;
|
|
27830
28542
|
constructor(ctx, config) {
|
|
@@ -27853,45 +28565,59 @@ class MultiplexerSessionManager {
|
|
|
27853
28565
|
const parentId = info.parentID;
|
|
27854
28566
|
const title = info.title ?? "Subagent";
|
|
27855
28567
|
const directory = info.directory ?? this.directory;
|
|
27856
|
-
|
|
27857
|
-
|
|
28568
|
+
this.knownSessions.set(sessionId, {
|
|
28569
|
+
parentId,
|
|
28570
|
+
title,
|
|
28571
|
+
directory
|
|
28572
|
+
});
|
|
28573
|
+
if (this.isTrackedOrSpawning(sessionId)) {
|
|
28574
|
+
log("[multiplexer-session-manager] session already tracked or spawning", {
|
|
27858
28575
|
sessionId
|
|
27859
28576
|
});
|
|
27860
28577
|
return;
|
|
27861
28578
|
}
|
|
27862
|
-
|
|
27863
|
-
|
|
27864
|
-
|
|
27865
|
-
|
|
27866
|
-
|
|
27867
|
-
|
|
27868
|
-
|
|
27869
|
-
|
|
27870
|
-
|
|
27871
|
-
|
|
27872
|
-
|
|
27873
|
-
|
|
27874
|
-
|
|
27875
|
-
log("[multiplexer-session-manager] failed to spawn pane", {
|
|
27876
|
-
error: String(err)
|
|
27877
|
-
});
|
|
27878
|
-
return { success: false, paneId: undefined };
|
|
27879
|
-
});
|
|
27880
|
-
if (paneResult.success && paneResult.paneId) {
|
|
27881
|
-
const now = Date.now();
|
|
27882
|
-
this.sessions.set(sessionId, {
|
|
28579
|
+
this.spawningSessions.add(sessionId);
|
|
28580
|
+
try {
|
|
28581
|
+
const serverRunning = await isServerRunning(this.serverUrl);
|
|
28582
|
+
if (!serverRunning) {
|
|
28583
|
+
log("[multiplexer-session-manager] server not running, skipping", {
|
|
28584
|
+
serverUrl: this.serverUrl
|
|
28585
|
+
});
|
|
28586
|
+
return;
|
|
28587
|
+
}
|
|
28588
|
+
if (this.sessions.has(sessionId)) {
|
|
28589
|
+
return;
|
|
28590
|
+
}
|
|
28591
|
+
log("[multiplexer-session-manager] child session created, spawning pane", {
|
|
27883
28592
|
sessionId,
|
|
27884
|
-
paneId: paneResult.paneId,
|
|
27885
28593
|
parentId,
|
|
27886
|
-
title
|
|
27887
|
-
createdAt: now,
|
|
27888
|
-
lastSeenAt: now
|
|
28594
|
+
title
|
|
27889
28595
|
});
|
|
27890
|
-
|
|
27891
|
-
|
|
27892
|
-
|
|
28596
|
+
const paneResult = await this.multiplexer.spawnPane(sessionId, title, this.serverUrl, directory).catch((err) => {
|
|
28597
|
+
log("[multiplexer-session-manager] failed to spawn pane", {
|
|
28598
|
+
error: String(err)
|
|
28599
|
+
});
|
|
28600
|
+
return { success: false, paneId: undefined };
|
|
27893
28601
|
});
|
|
27894
|
-
|
|
28602
|
+
if (paneResult.success && paneResult.paneId) {
|
|
28603
|
+
const now = Date.now();
|
|
28604
|
+
this.sessions.set(sessionId, {
|
|
28605
|
+
sessionId,
|
|
28606
|
+
paneId: paneResult.paneId,
|
|
28607
|
+
parentId,
|
|
28608
|
+
title,
|
|
28609
|
+
directory,
|
|
28610
|
+
createdAt: now,
|
|
28611
|
+
lastSeenAt: now
|
|
28612
|
+
});
|
|
28613
|
+
log("[multiplexer-session-manager] pane spawned", {
|
|
28614
|
+
sessionId,
|
|
28615
|
+
paneId: paneResult.paneId
|
|
28616
|
+
});
|
|
28617
|
+
this.startPolling();
|
|
28618
|
+
}
|
|
28619
|
+
} finally {
|
|
28620
|
+
this.spawningSessions.delete(sessionId);
|
|
27895
28621
|
}
|
|
27896
28622
|
}
|
|
27897
28623
|
async onSessionStatus(event) {
|
|
@@ -27904,6 +28630,10 @@ class MultiplexerSessionManager {
|
|
|
27904
28630
|
return;
|
|
27905
28631
|
if (event.properties?.status?.type === "idle") {
|
|
27906
28632
|
await this.closeSession(sessionId);
|
|
28633
|
+
return;
|
|
28634
|
+
}
|
|
28635
|
+
if (event.properties?.status?.type === "busy") {
|
|
28636
|
+
await this.respawnIfKnown(sessionId);
|
|
27907
28637
|
}
|
|
27908
28638
|
}
|
|
27909
28639
|
async onSessionDeleted(event) {
|
|
@@ -27918,6 +28648,7 @@ class MultiplexerSessionManager {
|
|
|
27918
28648
|
sessionId
|
|
27919
28649
|
});
|
|
27920
28650
|
await this.closeSession(sessionId);
|
|
28651
|
+
this.knownSessions.delete(sessionId);
|
|
27921
28652
|
}
|
|
27922
28653
|
startPolling() {
|
|
27923
28654
|
if (this.pollInterval)
|
|
@@ -27978,6 +28709,61 @@ class MultiplexerSessionManager {
|
|
|
27978
28709
|
this.stopPolling();
|
|
27979
28710
|
}
|
|
27980
28711
|
}
|
|
28712
|
+
async respawnIfKnown(sessionId) {
|
|
28713
|
+
if (!this.enabled || !this.multiplexer)
|
|
28714
|
+
return;
|
|
28715
|
+
if (this.isTrackedOrSpawning(sessionId))
|
|
28716
|
+
return;
|
|
28717
|
+
const known = this.knownSessions.get(sessionId);
|
|
28718
|
+
if (!known)
|
|
28719
|
+
return;
|
|
28720
|
+
this.spawningSessions.add(sessionId);
|
|
28721
|
+
try {
|
|
28722
|
+
const serverRunning = await isServerRunning(this.serverUrl);
|
|
28723
|
+
if (!serverRunning) {
|
|
28724
|
+
log("[multiplexer-session-manager] server not running, skipping busy respawn", {
|
|
28725
|
+
serverUrl: this.serverUrl,
|
|
28726
|
+
sessionId
|
|
28727
|
+
});
|
|
28728
|
+
return;
|
|
28729
|
+
}
|
|
28730
|
+
if (this.sessions.has(sessionId))
|
|
28731
|
+
return;
|
|
28732
|
+
log("[multiplexer-session-manager] child session busy again, respawning pane", {
|
|
28733
|
+
sessionId,
|
|
28734
|
+
parentId: known.parentId,
|
|
28735
|
+
title: known.title
|
|
28736
|
+
});
|
|
28737
|
+
const paneResult = await this.multiplexer.spawnPane(sessionId, known.title, this.serverUrl, known.directory).catch((err) => {
|
|
28738
|
+
log("[multiplexer-session-manager] failed to respawn pane", {
|
|
28739
|
+
error: String(err)
|
|
28740
|
+
});
|
|
28741
|
+
return { success: false, paneId: undefined };
|
|
28742
|
+
});
|
|
28743
|
+
if (!paneResult.success || !paneResult.paneId)
|
|
28744
|
+
return;
|
|
28745
|
+
const now = Date.now();
|
|
28746
|
+
this.sessions.set(sessionId, {
|
|
28747
|
+
sessionId,
|
|
28748
|
+
paneId: paneResult.paneId,
|
|
28749
|
+
parentId: known.parentId,
|
|
28750
|
+
title: known.title,
|
|
28751
|
+
directory: known.directory,
|
|
28752
|
+
createdAt: now,
|
|
28753
|
+
lastSeenAt: now
|
|
28754
|
+
});
|
|
28755
|
+
log("[multiplexer-session-manager] pane respawned on busy", {
|
|
28756
|
+
sessionId,
|
|
28757
|
+
paneId: paneResult.paneId
|
|
28758
|
+
});
|
|
28759
|
+
this.startPolling();
|
|
28760
|
+
} finally {
|
|
28761
|
+
this.spawningSessions.delete(sessionId);
|
|
28762
|
+
}
|
|
28763
|
+
}
|
|
28764
|
+
isTrackedOrSpawning(sessionId) {
|
|
28765
|
+
return this.sessions.has(sessionId) || this.spawningSessions.has(sessionId);
|
|
28766
|
+
}
|
|
27981
28767
|
async cleanup() {
|
|
27982
28768
|
this.stopPolling();
|
|
27983
28769
|
if (this.sessions.size > 0 && this.multiplexer) {
|
|
@@ -27992,6 +28778,8 @@ class MultiplexerSessionManager {
|
|
|
27992
28778
|
await Promise.all(closePromises);
|
|
27993
28779
|
this.sessions.clear();
|
|
27994
28780
|
}
|
|
28781
|
+
this.knownSessions.clear();
|
|
28782
|
+
this.spawningSessions.clear();
|
|
27995
28783
|
log("[multiplexer-session-manager] cleanup complete");
|
|
27996
28784
|
}
|
|
27997
28785
|
}
|
|
@@ -28203,9 +28991,9 @@ function findSgCliPathSync() {
|
|
|
28203
28991
|
}
|
|
28204
28992
|
if (process.platform === "darwin") {
|
|
28205
28993
|
const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"];
|
|
28206
|
-
for (const
|
|
28207
|
-
if (existsSync7(
|
|
28208
|
-
return
|
|
28994
|
+
for (const path13 of homebrewPaths) {
|
|
28995
|
+
if (existsSync7(path13) && isValidBinary(path13)) {
|
|
28996
|
+
return path13;
|
|
28209
28997
|
}
|
|
28210
28998
|
}
|
|
28211
28999
|
}
|
|
@@ -28222,8 +29010,8 @@ function getSgCliPath() {
|
|
|
28222
29010
|
}
|
|
28223
29011
|
return "sg";
|
|
28224
29012
|
}
|
|
28225
|
-
function setSgCliPath(
|
|
28226
|
-
resolvedCliPath =
|
|
29013
|
+
function setSgCliPath(path13) {
|
|
29014
|
+
resolvedCliPath = path13;
|
|
28227
29015
|
}
|
|
28228
29016
|
var DEFAULT_TIMEOUT_MS2 = 300000;
|
|
28229
29017
|
var DEFAULT_MAX_OUTPUT_BYTES = 1 * 1024 * 1024;
|
|
@@ -28791,14 +29579,14 @@ var BINARY_PREFIXES = [
|
|
|
28791
29579
|
var WEBFETCH_DESCRIPTION = "Fetch a URL with better extraction for static/docs pages. Supports llms.txt probing, content-focused HTML extraction, metadata, redirects, and an optional prompt processed by a cheap secondary model.";
|
|
28792
29580
|
// src/tools/smartfetch/tool.ts
|
|
28793
29581
|
import os4 from "node:os";
|
|
28794
|
-
import
|
|
29582
|
+
import path16 from "node:path";
|
|
28795
29583
|
import {
|
|
28796
29584
|
tool as tool4
|
|
28797
29585
|
} from "@opencode-ai/plugin";
|
|
28798
29586
|
|
|
28799
29587
|
// src/tools/smartfetch/binary.ts
|
|
28800
29588
|
import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
|
|
28801
|
-
import
|
|
29589
|
+
import path13 from "node:path";
|
|
28802
29590
|
function extensionForMime(contentType) {
|
|
28803
29591
|
const mime = contentType.split(";")[0]?.trim().toLowerCase();
|
|
28804
29592
|
const map = {
|
|
@@ -28819,10 +29607,10 @@ function buildBinaryResultMessage(fetchResult, savedPath) {
|
|
|
28819
29607
|
async function saveBinary(binaryDir, data, contentType, filename) {
|
|
28820
29608
|
await mkdir2(binaryDir, { recursive: true });
|
|
28821
29609
|
const initialName = filename || `webfetch-${Date.now()}.${extensionForMime(contentType)}`;
|
|
28822
|
-
const parsed =
|
|
29610
|
+
const parsed = path13.parse(initialName);
|
|
28823
29611
|
for (let attempt = 0;attempt < 1000; attempt++) {
|
|
28824
29612
|
const candidateName = attempt === 0 ? initialName : `${parsed.name}-${attempt}${parsed.ext || `.${extensionForMime(contentType)}`}`;
|
|
28825
|
-
const file =
|
|
29613
|
+
const file = path13.join(binaryDir, candidateName);
|
|
28826
29614
|
try {
|
|
28827
29615
|
await writeFile2(file, data, { flag: "wx" });
|
|
28828
29616
|
return file;
|
|
@@ -29476,7 +30264,7 @@ var L = class u2 {
|
|
|
29476
30264
|
};
|
|
29477
30265
|
|
|
29478
30266
|
// src/tools/smartfetch/network.ts
|
|
29479
|
-
import
|
|
30267
|
+
import path14 from "node:path";
|
|
29480
30268
|
|
|
29481
30269
|
// src/tools/smartfetch/utils.ts
|
|
29482
30270
|
var import_readability = __toESM(require_readability(), 1);
|
|
@@ -30201,7 +30989,7 @@ function inferFilenameFromUrl(url) {
|
|
|
30201
30989
|
function truncateFilename(name, maxLength = 180) {
|
|
30202
30990
|
if (name.length <= maxLength)
|
|
30203
30991
|
return name;
|
|
30204
|
-
const parsed =
|
|
30992
|
+
const parsed = path14.parse(name);
|
|
30205
30993
|
const ext = parsed.ext || "";
|
|
30206
30994
|
const baseLimit = Math.max(1, maxLength - ext.length);
|
|
30207
30995
|
return `${parsed.name.slice(0, baseLimit)}${ext}`;
|
|
@@ -30373,7 +31161,7 @@ function isInvalidLlmsResult(fetchResult) {
|
|
|
30373
31161
|
// src/tools/smartfetch/secondary-model.ts
|
|
30374
31162
|
import { existsSync as existsSync9 } from "node:fs";
|
|
30375
31163
|
import { readFile as readFile4 } from "node:fs/promises";
|
|
30376
|
-
import
|
|
31164
|
+
import path15 from "node:path";
|
|
30377
31165
|
function parseModelRef(value) {
|
|
30378
31166
|
if (!value)
|
|
30379
31167
|
return;
|
|
@@ -30399,7 +31187,7 @@ function pickAgentModelRef(value) {
|
|
|
30399
31187
|
}
|
|
30400
31188
|
function findPreferredOpenCodeConfigPath(baseDir) {
|
|
30401
31189
|
for (const file of ["opencode.jsonc", "opencode.json"]) {
|
|
30402
|
-
const fullPath =
|
|
31190
|
+
const fullPath = path15.join(baseDir, file);
|
|
30403
31191
|
if (existsSync9(fullPath))
|
|
30404
31192
|
return fullPath;
|
|
30405
31193
|
}
|
|
@@ -30416,7 +31204,7 @@ async function readOpenCodeConfigFile(configPath) {
|
|
|
30416
31204
|
}
|
|
30417
31205
|
}
|
|
30418
31206
|
async function readEffectiveOpenCodeConfig(directory) {
|
|
30419
|
-
const projectDir =
|
|
31207
|
+
const projectDir = path15.join(directory, ".opencode");
|
|
30420
31208
|
const userDirs = getConfigSearchDirs();
|
|
30421
31209
|
const projectPath = findPreferredOpenCodeConfigPath(projectDir);
|
|
30422
31210
|
const userPath = userDirs.map((configDir) => findPreferredOpenCodeConfigPath(configDir)).find(Boolean);
|
|
@@ -30577,7 +31365,7 @@ async function runSecondaryModelWithFallback(client, directory, models, prompt,
|
|
|
30577
31365
|
// src/tools/smartfetch/tool.ts
|
|
30578
31366
|
var z5 = tool4.schema;
|
|
30579
31367
|
function createWebfetchTool(pluginCtx, options = {}) {
|
|
30580
|
-
const binaryDir = options.binaryDir ||
|
|
31368
|
+
const binaryDir = options.binaryDir || path16.join(os4.tmpdir(), "opencode-smartfetch");
|
|
30581
31369
|
return tool4({
|
|
30582
31370
|
description: WEBFETCH_DESCRIPTION,
|
|
30583
31371
|
args: {
|
|
@@ -31099,6 +31887,27 @@ class SubagentDepthTracker {
|
|
|
31099
31887
|
}
|
|
31100
31888
|
}
|
|
31101
31889
|
|
|
31890
|
+
// src/utils/system-collapse.ts
|
|
31891
|
+
function collapseSystemInPlace(system2) {
|
|
31892
|
+
if (system2.length === 0) {
|
|
31893
|
+
return;
|
|
31894
|
+
}
|
|
31895
|
+
if (system2.length === 1) {
|
|
31896
|
+
if (system2[0]) {
|
|
31897
|
+
return;
|
|
31898
|
+
}
|
|
31899
|
+
system2.length = 0;
|
|
31900
|
+
return;
|
|
31901
|
+
}
|
|
31902
|
+
const joined = system2.join(`
|
|
31903
|
+
|
|
31904
|
+
`);
|
|
31905
|
+
system2.length = 0;
|
|
31906
|
+
if (joined) {
|
|
31907
|
+
system2.push(joined);
|
|
31908
|
+
}
|
|
31909
|
+
}
|
|
31910
|
+
|
|
31102
31911
|
// src/index.ts
|
|
31103
31912
|
async function appLog(ctx, level, message) {
|
|
31104
31913
|
try {
|
|
@@ -31128,6 +31937,7 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
31128
31937
|
const sessionId = new Date().toISOString().replace(/[-:]/g, "").slice(0, 15);
|
|
31129
31938
|
initLogger(sessionId);
|
|
31130
31939
|
let config;
|
|
31940
|
+
let disabledAgents;
|
|
31131
31941
|
let agentDefs;
|
|
31132
31942
|
let agents;
|
|
31133
31943
|
let mcps;
|
|
@@ -31148,13 +31958,17 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
31148
31958
|
let jsonErrorRecoveryHook;
|
|
31149
31959
|
let foregroundFallback;
|
|
31150
31960
|
let todoContinuationHook;
|
|
31961
|
+
let taskSessionManagerHook;
|
|
31151
31962
|
let interviewManager;
|
|
31152
31963
|
let presetManager;
|
|
31153
31964
|
let councilTools;
|
|
31154
31965
|
let webfetch;
|
|
31966
|
+
let rewriteDisplayNameMentions;
|
|
31155
31967
|
let toolCount = 0;
|
|
31156
31968
|
try {
|
|
31157
31969
|
config = loadPluginConfig(ctx.directory);
|
|
31970
|
+
disabledAgents = getDisabledAgents(config);
|
|
31971
|
+
rewriteDisplayNameMentions = createDisplayNameMentionRewriter(config);
|
|
31158
31972
|
agentDefs = createAgents(config);
|
|
31159
31973
|
agents = getAgentConfigs(config);
|
|
31160
31974
|
modelArrayMap = {};
|
|
@@ -31191,7 +32005,7 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
31191
32005
|
main_pane_size: config.multiplexer?.main_pane_size ?? 60
|
|
31192
32006
|
};
|
|
31193
32007
|
const multiplexer = getMultiplexer(multiplexerConfig);
|
|
31194
|
-
multiplexerEnabled = multiplexerConfig.type !== "none" && multiplexer !== null;
|
|
32008
|
+
multiplexerEnabled = multiplexerConfig.type !== "none" && multiplexer !== null && multiplexer.isInsideSession();
|
|
31195
32009
|
log("[plugin] initialized with multiplexer config", {
|
|
31196
32010
|
multiplexerConfig,
|
|
31197
32011
|
enabled: multiplexerEnabled,
|
|
@@ -31207,7 +32021,7 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
31207
32021
|
multiplexerSessionManager = new MultiplexerSessionManager(ctx, multiplexerConfig);
|
|
31208
32022
|
autoUpdateChecker = createAutoUpdateCheckerHook(ctx, {
|
|
31209
32023
|
showStartupToast: config.showStartupToast ?? true,
|
|
31210
|
-
autoUpdate: true
|
|
32024
|
+
autoUpdate: config.autoUpdate ?? true
|
|
31211
32025
|
});
|
|
31212
32026
|
phaseReminderHook = createPhaseReminderHook();
|
|
31213
32027
|
filterAvailableSkillsHook = createFilterAvailableSkillsHook(ctx, config);
|
|
@@ -31226,6 +32040,12 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
31226
32040
|
autoEnable: config.todoContinuation?.autoEnable ?? false,
|
|
31227
32041
|
autoEnableThreshold: config.todoContinuation?.autoEnableThreshold ?? 4
|
|
31228
32042
|
});
|
|
32043
|
+
taskSessionManagerHook = createTaskSessionManagerHook(ctx, {
|
|
32044
|
+
maxSessionsPerAgent: config.sessionManager?.maxSessionsPerAgent ?? 2,
|
|
32045
|
+
readContextMinLines: config.sessionManager?.readContextMinLines ?? 10,
|
|
32046
|
+
readContextMaxFiles: config.sessionManager?.readContextMaxFiles ?? 8,
|
|
32047
|
+
shouldManageSession: (sessionID) => sessionAgentMap.get(sessionID) === "orchestrator"
|
|
32048
|
+
});
|
|
31229
32049
|
interviewManager = createInterviewManager(ctx, config);
|
|
31230
32050
|
presetManager = createPresetManager(ctx, config);
|
|
31231
32051
|
toolCount = Object.keys(councilTools).length + Object.keys(todoContinuationHook.tool).length + 1 + 2;
|
|
@@ -31401,6 +32221,7 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
31401
32221
|
await multiplexerSessionManager.onSessionDeleted(event);
|
|
31402
32222
|
await interviewManager.handleEvent(input);
|
|
31403
32223
|
await postFileToolNudgeHook.event(input);
|
|
32224
|
+
await taskSessionManagerHook.event(input);
|
|
31404
32225
|
if (input.event.type === "session.deleted") {
|
|
31405
32226
|
const props = input.event.properties;
|
|
31406
32227
|
const sessionID = props?.info?.id ?? props?.sessionID;
|
|
@@ -31414,6 +32235,7 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
31414
32235
|
},
|
|
31415
32236
|
"tool.execute.before": async (input, output) => {
|
|
31416
32237
|
await applyPatchHook["tool.execute.before"](input, output);
|
|
32238
|
+
await taskSessionManagerHook["tool.execute.before"](input, output);
|
|
31417
32239
|
},
|
|
31418
32240
|
"command.execute.before": async (input, output) => {
|
|
31419
32241
|
await todoContinuationHook.handleCommandExecuteBefore(input, output);
|
|
@@ -31441,7 +32263,7 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
31441
32263
|
const alreadyInjected = output.system.some((s) => typeof s === "string" && s.includes("<Role>") && s.includes("orchestrator"));
|
|
31442
32264
|
if (!alreadyInjected) {
|
|
31443
32265
|
const orchestratorDef = agentDefs.find((a) => a.name === "orchestrator");
|
|
31444
|
-
const orchestratorPrompt = typeof orchestratorDef?.config?.prompt === "string" ? orchestratorDef.config.prompt : buildOrchestratorPrompt(
|
|
32266
|
+
const orchestratorPrompt = typeof orchestratorDef?.config?.prompt === "string" ? orchestratorDef.config.prompt : buildOrchestratorPrompt(disabledAgents);
|
|
31445
32267
|
output.system[0] = orchestratorPrompt + (output.system[0] ? `
|
|
31446
32268
|
|
|
31447
32269
|
${output.system[0]}` : "");
|
|
@@ -31449,10 +32271,8 @@ ${output.system[0]}` : "");
|
|
|
31449
32271
|
}
|
|
31450
32272
|
await todoContinuationHook.handleChatSystemTransform(input, output);
|
|
31451
32273
|
await postFileToolNudgeHook["experimental.chat.system.transform"](input, output);
|
|
31452
|
-
|
|
31453
|
-
|
|
31454
|
-
`);
|
|
31455
|
-
output.system = joined ? [joined] : [];
|
|
32274
|
+
await taskSessionManagerHook["experimental.chat.system.transform"](input, output);
|
|
32275
|
+
collapseSystemInPlace(output.system);
|
|
31456
32276
|
},
|
|
31457
32277
|
"experimental.chat.messages.transform": async (input, output) => {
|
|
31458
32278
|
const typedOutput = output;
|
|
@@ -31464,13 +32284,13 @@ ${output.system[0]}` : "");
|
|
|
31464
32284
|
if (part.type !== "text" || typeof part.text !== "string") {
|
|
31465
32285
|
continue;
|
|
31466
32286
|
}
|
|
31467
|
-
part.text = rewriteDisplayNameMentions(
|
|
32287
|
+
part.text = rewriteDisplayNameMentions(part.text);
|
|
31468
32288
|
}
|
|
31469
32289
|
}
|
|
31470
32290
|
processImageAttachments({
|
|
31471
32291
|
messages: typedOutput.messages,
|
|
31472
32292
|
workDir: ctx.directory,
|
|
31473
|
-
disabledAgents
|
|
32293
|
+
disabledAgents,
|
|
31474
32294
|
log
|
|
31475
32295
|
});
|
|
31476
32296
|
await todoContinuationHook.handleMessagesTransform({
|
|
@@ -31484,6 +32304,7 @@ ${output.system[0]}` : "");
|
|
|
31484
32304
|
await jsonErrorRecoveryHook["tool.execute.after"](input, output);
|
|
31485
32305
|
await todoContinuationHook.handleToolExecuteAfter(input);
|
|
31486
32306
|
await postFileToolNudgeHook["tool.execute.after"](input, output);
|
|
32307
|
+
await taskSessionManagerHook["tool.execute.after"](input, output);
|
|
31487
32308
|
}
|
|
31488
32309
|
};
|
|
31489
32310
|
};
|