oh-my-opencode-slim 2.0.5 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja-JP.md +64 -38
- package/README.ko-KR.md +62 -36
- package/README.md +82 -47
- package/README.zh-CN.md +63 -39
- package/dist/agents/council.d.ts +1 -1
- package/dist/agents/index.d.ts +7 -2
- package/dist/agents/orchestrator.d.ts +1 -1
- package/dist/cli/custom-skills-registry.d.ts +18 -0
- package/dist/cli/custom-skills.d.ts +3 -19
- package/dist/cli/index.js +1130 -195
- package/dist/cli/providers.d.ts +12 -16
- package/dist/cli/skills.d.ts +2 -2
- package/dist/companion/manager.d.ts +10 -0
- package/dist/config/constants.d.ts +2 -1
- package/dist/config/council-schema.d.ts +37 -12
- package/dist/config/loader.d.ts +5 -2
- package/dist/config/schema.d.ts +17 -6
- package/dist/council/council-manager.d.ts +7 -3
- package/dist/hooks/auto-update-checker/skill-sync.d.ts +59 -1
- package/dist/hooks/filter-available-skills/index.d.ts +1 -2
- package/dist/hooks/foreground-fallback/index.d.ts +40 -2
- package/dist/hooks/image-hook.d.ts +1 -1
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/loop-command/index.d.ts +13 -0
- package/dist/hooks/phase-reminder/index.d.ts +4 -3
- package/dist/hooks/post-file-tool-nudge/index.d.ts +15 -9
- package/dist/hooks/session-lifecycle.d.ts +11 -0
- package/dist/hooks/task-session-manager/index.d.ts +15 -4
- package/dist/hooks/task-session-manager/pending-call-tracker.d.ts +13 -0
- package/dist/hooks/task-session-manager/task-context-tracker.d.ts +14 -0
- package/dist/hooks/types.d.ts +3 -1
- package/dist/index.js +2890 -986
- package/dist/interview/dashboard-manager.d.ts +21 -0
- package/dist/interview/manager.d.ts +0 -14
- package/dist/interview/service.d.ts +9 -0
- package/dist/interview/session-server.d.ts +21 -0
- package/dist/interview/types.d.ts +3 -1
- package/dist/loop/loop-session.d.ts +64 -0
- package/dist/multiplexer/factory.d.ts +5 -5
- package/dist/multiplexer/herdr/index.d.ts +33 -0
- package/dist/multiplexer/index.d.ts +1 -0
- package/dist/multiplexer/session-manager.d.ts +6 -5
- package/dist/multiplexer/shared.d.ts +24 -0
- package/dist/multiplexer/tmux/index.d.ts +0 -1
- package/dist/multiplexer/types.d.ts +4 -4
- package/dist/multiplexer/zellij/index.d.ts +0 -1
- package/dist/tools/cancel-task.d.ts +2 -2
- package/dist/tui.d.ts +1 -0
- package/dist/tui.js +123 -45
- package/dist/utils/background-job-board.d.ts +22 -2
- package/dist/utils/background-job-coordinator.d.ts +72 -0
- package/dist/utils/background-job-store.d.ts +46 -0
- package/dist/utils/councillor-models.d.ts +20 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/internal-initiator.d.ts +6 -1
- package/dist/utils/session.d.ts +2 -2
- package/oh-my-opencode-slim.schema.json +18 -1
- package/package.json +1 -1
- package/src/skills/clonedeps/SKILL.md +2 -2
- package/src/skills/clonedeps/codemap.md +23 -32
- package/src/skills/codemap/SKILL.md +2 -2
- package/src/skills/codemap.md +63 -36
- package/src/skills/loop-engineering/SKILL.md +30 -0
- package/src/skills/oh-my-opencode-slim/SKILL.md +3 -3
- package/src/skills/reflect/SKILL.md +133 -0
- package/src/skills/release-smoke-test/SKILL.md +159 -0
- package/src/skills/simplify/SKILL.md +6 -6
package/dist/tui.js
CHANGED
|
@@ -131,7 +131,12 @@ var POLL_INTERVAL_BACKGROUND_MS = 2000;
|
|
|
131
131
|
var MAX_POLL_TIME_MS = 5 * 60 * 1000;
|
|
132
132
|
var DEFAULT_MAX_SUBAGENT_DEPTH = 3;
|
|
133
133
|
var PHASE_REMINDER_TEXT = `!IMPORTANT! Scheduler workflow: plan lanes/dependencies → dispatch background specialists → track task IDs → wait for hook-driven completion → reconcile terminal results → verify. Do not poll running jobs, consume running-job output, or advance dependent work. !END!`;
|
|
134
|
-
|
|
134
|
+
function formatSystemReminder(text) {
|
|
135
|
+
return `<system-reminder>
|
|
136
|
+
${text}
|
|
137
|
+
</system-reminder>`;
|
|
138
|
+
}
|
|
139
|
+
var PHASE_REMINDER = formatSystemReminder(PHASE_REMINDER_TEXT);
|
|
135
140
|
var WRITABLE_FILE_OPERATIONS_RULES = `**File Operations Rules**:
|
|
136
141
|
- Prefer dedicated file tools for normal code work: glob/grep/ast_grep_search for discovery, read for file contents, and edit/write/apply_patch for targeted source changes.
|
|
137
142
|
- Use bash for execution and automation: git, package managers, tests, builds, scripts, diagnostics, and shell-native filesystem operations.
|
|
@@ -188,11 +193,35 @@ function getOpenCodeConfigPaths() {
|
|
|
188
193
|
}
|
|
189
194
|
// src/config/council-schema.ts
|
|
190
195
|
import { z } from "zod";
|
|
191
|
-
|
|
196
|
+
|
|
197
|
+
// src/utils/councillor-models.ts
|
|
198
|
+
function normalizeCouncillorModels(model, fallbackVariant) {
|
|
199
|
+
const raw = Array.isArray(model) ? model : [model];
|
|
200
|
+
return raw.map((entry) => typeof entry === "string" ? { id: entry, variant: fallbackVariant } : { id: entry.id, variant: entry.variant ?? fallbackVariant });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/config/council-schema.ts
|
|
204
|
+
var ModelIdSchema = z.string().regex(/^[^/\s]+\/[^\s]+$/, 'Expected provider/model format (e.g. "openai/gpt-5.6-luna")');
|
|
205
|
+
var CouncillorModelEntrySchema = z.object({
|
|
206
|
+
id: ModelIdSchema,
|
|
207
|
+
variant: z.string().optional()
|
|
208
|
+
});
|
|
209
|
+
var CouncillorModelSchema = z.union([
|
|
210
|
+
ModelIdSchema,
|
|
211
|
+
z.array(z.union([ModelIdSchema, CouncillorModelEntrySchema])).min(1)
|
|
212
|
+
]).describe('Model ID in provider/model format (e.g. "openai/gpt-5.6-luna"), or an ' + "ordered fallback chain (array of model IDs or { id, variant } entries) " + "tried in order until one responds.");
|
|
192
213
|
var CouncillorConfigSchema = z.object({
|
|
193
|
-
model:
|
|
214
|
+
model: CouncillorModelSchema,
|
|
194
215
|
variant: z.string().optional(),
|
|
195
216
|
prompt: z.string().optional().describe("Optional role/guidance injected into the councillor user prompt")
|
|
217
|
+
}).transform((c) => {
|
|
218
|
+
const models = normalizeCouncillorModels(c.model, c.variant);
|
|
219
|
+
return {
|
|
220
|
+
model: models[0].id,
|
|
221
|
+
variant: c.variant,
|
|
222
|
+
prompt: c.prompt,
|
|
223
|
+
models
|
|
224
|
+
};
|
|
196
225
|
});
|
|
197
226
|
var CouncilPresetSchema = z.record(z.string(), z.record(z.string(), z.unknown())).transform((entries, ctx) => {
|
|
198
227
|
const councillors = {};
|
|
@@ -232,7 +261,7 @@ var CouncilConfigSchema = z.object({
|
|
|
232
261
|
default_preset: z.string().default("default"),
|
|
233
262
|
councillor_execution_mode: CouncillorExecutionModeSchema.describe('Execution mode for councillors. "serial" runs them one at a time (required for single-model systems). "parallel" runs them concurrently (default, faster for multi-model systems).'),
|
|
234
263
|
councillor_retries: z.number().int().min(0).max(5).default(3).describe("Number of retry attempts for councillors that return empty responses " + "(e.g. due to provider rate limiting). Default: 3 retries."),
|
|
235
|
-
master: z.unknown().optional().describe("DEPRECATED
|
|
264
|
+
master: z.unknown().optional().describe("DEPRECATED - ignored. Council agent synthesizes directly.")
|
|
236
265
|
}).transform((data) => {
|
|
237
266
|
const deprecated = [];
|
|
238
267
|
if (data.master !== undefined)
|
|
@@ -298,7 +327,13 @@ var AgentOverrideConfigSchema = z2.object({
|
|
|
298
327
|
options: z2.record(z2.string(), z2.unknown()).optional(),
|
|
299
328
|
displayName: z2.string().min(1).optional()
|
|
300
329
|
}).strict();
|
|
301
|
-
var MultiplexerTypeSchema = z2.enum([
|
|
330
|
+
var MultiplexerTypeSchema = z2.enum([
|
|
331
|
+
"auto",
|
|
332
|
+
"tmux",
|
|
333
|
+
"zellij",
|
|
334
|
+
"herdr",
|
|
335
|
+
"none"
|
|
336
|
+
]);
|
|
302
337
|
var MultiplexerLayoutSchema = z2.enum([
|
|
303
338
|
"main-horizontal",
|
|
304
339
|
"main-vertical",
|
|
@@ -340,7 +375,9 @@ var FailoverConfigSchema = z2.object({
|
|
|
340
375
|
enabled: z2.boolean().default(true),
|
|
341
376
|
timeoutMs: z2.number().min(0).default(15000),
|
|
342
377
|
retryDelayMs: z2.number().min(0).default(500),
|
|
343
|
-
|
|
378
|
+
maxRetries: z2.number().int().min(0).default(3).describe("Number of consecutive 429/rate-limit responses tolerated on the " + "same model before aborting (or swapping to the next fallback " + "model when a chain is configured)."),
|
|
379
|
+
retry_on_empty: z2.boolean().default(true).describe("When true (default), empty provider responses are treated as failures, " + "triggering fallback/retry. Set to false to treat them as successes."),
|
|
380
|
+
runtimeOverride: z2.boolean().default(true).describe("When true (default), a runtime model selected via /model that is " + "outside the configured fallback chain will still trigger the chain " + "on rate-limit errors. When false, out-of-chain runtime picks are " + "respected and the error surfaces instead of silently falling back " + "to the chain. Models that are members of the chain always fall back " + "regardless of this setting.")
|
|
344
381
|
}).strict();
|
|
345
382
|
var CompanionConfigSchema = z2.object({
|
|
346
383
|
enabled: z2.boolean().optional(),
|
|
@@ -367,24 +404,13 @@ var AcpAgentConfigSchema = z2.object({
|
|
|
367
404
|
permissionMode: AcpAgentPermissionModeSchema.default("ask")
|
|
368
405
|
}).strict();
|
|
369
406
|
var AcpAgentsConfigSchema = z2.record(z2.string(), AcpAgentConfigSchema);
|
|
370
|
-
function
|
|
407
|
+
function rejectOrchestratorPromptOnOrchestrator(overrides, ctx, pathPrefix) {
|
|
371
408
|
for (const [name, override] of Object.entries(overrides)) {
|
|
372
|
-
|
|
373
|
-
if (!isBuiltInOrAlias) {
|
|
374
|
-
continue;
|
|
375
|
-
}
|
|
376
|
-
if (override.prompt !== undefined) {
|
|
377
|
-
ctx.addIssue({
|
|
378
|
-
code: z2.ZodIssueCode.custom,
|
|
379
|
-
path: [...pathPrefix, name, "prompt"],
|
|
380
|
-
message: "prompt is only supported for custom agents"
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
if (override.orchestratorPrompt !== undefined) {
|
|
409
|
+
if (name === "orchestrator" && override.orchestratorPrompt !== undefined) {
|
|
384
410
|
ctx.addIssue({
|
|
385
411
|
code: z2.ZodIssueCode.custom,
|
|
386
412
|
path: [...pathPrefix, name, "orchestratorPrompt"],
|
|
387
|
-
message: "orchestratorPrompt is
|
|
413
|
+
message: "orchestratorPrompt is not supported for the orchestrator agent"
|
|
388
414
|
});
|
|
389
415
|
}
|
|
390
416
|
}
|
|
@@ -392,6 +418,7 @@ function validateCustomOnlyPromptFields(overrides, ctx, pathPrefix) {
|
|
|
392
418
|
var PluginConfigSchema = z2.object({
|
|
393
419
|
preset: z2.string().optional(),
|
|
394
420
|
setDefaultAgent: z2.boolean().optional(),
|
|
421
|
+
compactSidebar: z2.boolean().optional().describe("Use the compact TUI sidebar layout. Defaults to true; set false to use the expanded layout."),
|
|
395
422
|
autoUpdate: z2.boolean().optional().describe("Disable automatic installation of plugin updates when false. Defaults to true."),
|
|
396
423
|
presets: z2.record(z2.string(), PresetSchema).optional(),
|
|
397
424
|
agents: z2.record(z2.string(), AgentOverrideConfigSchema).optional(),
|
|
@@ -410,11 +437,14 @@ var PluginConfigSchema = z2.object({
|
|
|
410
437
|
acpAgents: AcpAgentsConfigSchema.optional()
|
|
411
438
|
}).superRefine((value, ctx) => {
|
|
412
439
|
if (value.agents) {
|
|
413
|
-
|
|
440
|
+
rejectOrchestratorPromptOnOrchestrator(value.agents, ctx, ["agents"]);
|
|
414
441
|
}
|
|
415
442
|
if (value.presets) {
|
|
416
443
|
for (const [presetName, preset] of Object.entries(value.presets)) {
|
|
417
|
-
|
|
444
|
+
rejectOrchestratorPromptOnOrchestrator(preset, ctx, [
|
|
445
|
+
"presets",
|
|
446
|
+
presetName
|
|
447
|
+
]);
|
|
418
448
|
}
|
|
419
449
|
}
|
|
420
450
|
});
|
|
@@ -470,7 +500,7 @@ function getAgentMcpList(agentName, config) {
|
|
|
470
500
|
return defaultMcps ?? [];
|
|
471
501
|
}
|
|
472
502
|
|
|
473
|
-
// src/cli/custom-skills.ts
|
|
503
|
+
// src/cli/custom-skills-registry.ts
|
|
474
504
|
var CUSTOM_SKILLS = [
|
|
475
505
|
{
|
|
476
506
|
name: "simplify",
|
|
@@ -508,6 +538,12 @@ var CUSTOM_SKILLS = [
|
|
|
508
538
|
allowedAgents: ["orchestrator"],
|
|
509
539
|
sourcePath: "src/skills/oh-my-opencode-slim"
|
|
510
540
|
},
|
|
541
|
+
{
|
|
542
|
+
name: "release-smoke-test",
|
|
543
|
+
description: "Validate packed release candidates and bugfixes before public publish",
|
|
544
|
+
allowedAgents: ["orchestrator"],
|
|
545
|
+
sourcePath: "src/skills/release-smoke-test"
|
|
546
|
+
},
|
|
511
547
|
{
|
|
512
548
|
name: "worktrees",
|
|
513
549
|
description: "Manage Git worktrees as OMO safe isolated coding lanes for complex/risky/parallel work",
|
|
@@ -605,6 +641,7 @@ function mergePluginConfigs(base, override) {
|
|
|
605
641
|
...base,
|
|
606
642
|
...override,
|
|
607
643
|
agents: deepMerge(base.agents, override.agents),
|
|
644
|
+
presets: deepMerge(base.presets, override.presets),
|
|
608
645
|
tmux: deepMerge(base.tmux, override.tmux),
|
|
609
646
|
multiplexer: deepMerge(base.multiplexer, override.multiplexer),
|
|
610
647
|
interview: deepMerge(base.interview, override.interview),
|
|
@@ -676,15 +713,33 @@ function loadPluginConfig(directory, options) {
|
|
|
676
713
|
}
|
|
677
714
|
return config;
|
|
678
715
|
}
|
|
679
|
-
function loadAgentPrompt(agentName,
|
|
716
|
+
function loadAgentPrompt(agentName, optionsOrPreset) {
|
|
717
|
+
let preset;
|
|
718
|
+
let projectDirectory;
|
|
719
|
+
if (typeof optionsOrPreset === "string") {
|
|
720
|
+
preset = optionsOrPreset;
|
|
721
|
+
} else if (optionsOrPreset && typeof optionsOrPreset === "object") {
|
|
722
|
+
preset = optionsOrPreset.preset;
|
|
723
|
+
projectDirectory = optionsOrPreset.projectDirectory;
|
|
724
|
+
}
|
|
680
725
|
const presetDirName = preset && /^[a-zA-Z0-9_-]+$/.test(preset) ? preset : undefined;
|
|
681
|
-
const
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
}
|
|
685
|
-
|
|
726
|
+
const searchDirs = [];
|
|
727
|
+
if (projectDirectory && presetDirName) {
|
|
728
|
+
searchDirs.push(path.join(projectDirectory, ".opencode", PROMPTS_DIR_NAME, presetDirName));
|
|
729
|
+
}
|
|
730
|
+
if (projectDirectory) {
|
|
731
|
+
searchDirs.push(path.join(projectDirectory, ".opencode", PROMPTS_DIR_NAME));
|
|
732
|
+
}
|
|
733
|
+
if (presetDirName) {
|
|
734
|
+
for (const userDir of getConfigSearchDirs()) {
|
|
735
|
+
searchDirs.push(path.join(userDir, PROMPTS_DIR_NAME, presetDirName));
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
for (const userDir of getConfigSearchDirs()) {
|
|
739
|
+
searchDirs.push(path.join(userDir, PROMPTS_DIR_NAME));
|
|
740
|
+
}
|
|
686
741
|
const readFirstPrompt = (fileName, errorPrefix) => {
|
|
687
|
-
for (const dir of
|
|
742
|
+
for (const dir of searchDirs) {
|
|
688
743
|
const promptPath = path.join(dir, fileName);
|
|
689
744
|
if (!fs.existsSync(promptPath)) {
|
|
690
745
|
continue;
|
|
@@ -697,6 +752,7 @@ function loadAgentPrompt(agentName, preset) {
|
|
|
697
752
|
}
|
|
698
753
|
return;
|
|
699
754
|
};
|
|
755
|
+
const result = {};
|
|
700
756
|
result.prompt = readFirstPrompt(`${agentName}.md`, "Error reading prompt file");
|
|
701
757
|
result.appendPrompt = readFirstPrompt(`${agentName}_append.md`, "Error reading append prompt file");
|
|
702
758
|
return result;
|
|
@@ -859,25 +915,36 @@ function getSidebarAgentNames(snapshot) {
|
|
|
859
915
|
function agentRow(label, model, variant, theme) {
|
|
860
916
|
const modelParts = splitSidebarModelId(model);
|
|
861
917
|
const detailRows = [];
|
|
918
|
+
function detailRow(fieldLabel, value) {
|
|
919
|
+
return box({ width: "100%", flexDirection: "row", paddingLeft: 2 }, [
|
|
920
|
+
text({ fg: theme.textMuted, width: 9 }, [fieldLabel]),
|
|
921
|
+
text({ fg: theme.textMuted }, [value])
|
|
922
|
+
]);
|
|
923
|
+
}
|
|
862
924
|
if (modelParts.provider) {
|
|
863
|
-
detailRows.push(
|
|
925
|
+
detailRows.push(detailRow("provider", modelParts.provider));
|
|
864
926
|
}
|
|
865
|
-
detailRows.push(
|
|
927
|
+
detailRows.push(detailRow("model", modelParts.model));
|
|
866
928
|
if (variant) {
|
|
867
|
-
detailRows.push(
|
|
929
|
+
detailRows.push(detailRow("variant", variant));
|
|
868
930
|
}
|
|
869
931
|
return box({ width: "100%", flexDirection: "column", marginBottom: 1 }, [
|
|
870
932
|
text({ fg: theme.textMuted }, [label]),
|
|
871
933
|
...detailRows
|
|
872
934
|
]);
|
|
873
935
|
}
|
|
874
|
-
function
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
936
|
+
function compactAgentRow(label, model, _variant, theme) {
|
|
937
|
+
const modelName = splitSidebarModelId(model).model;
|
|
938
|
+
return box({
|
|
939
|
+
width: "100%",
|
|
940
|
+
flexDirection: "row",
|
|
941
|
+
justifyContent: "space-between"
|
|
942
|
+
}, [
|
|
943
|
+
text({ fg: theme.textMuted, width: 14 }, [label]),
|
|
944
|
+
text({ fg: theme.textMuted }, [modelName])
|
|
878
945
|
]);
|
|
879
946
|
}
|
|
880
|
-
function renderSidebar(snapshot, version, theme, configInvalid) {
|
|
947
|
+
function renderSidebar(snapshot, version, theme, configInvalid, compactSidebar) {
|
|
881
948
|
const configStatusRow = buildConfigStatusRow(configInvalid, theme);
|
|
882
949
|
return box({
|
|
883
950
|
width: "100%",
|
|
@@ -895,7 +962,7 @@ function renderSidebar(snapshot, version, theme, configInvalid) {
|
|
|
895
962
|
justifyContent: "space-between",
|
|
896
963
|
alignItems: "center"
|
|
897
964
|
}, [
|
|
898
|
-
box({ paddingLeft: 1, paddingRight: 1, backgroundColor: theme.accent }, [text({ fg: theme.
|
|
965
|
+
box({ paddingLeft: 1, paddingRight: 1, backgroundColor: theme.accent }, [text({ fg: theme.text }, ["OMO-Slim"])]),
|
|
899
966
|
text({ fg: theme.textMuted }, [`v${version}`])
|
|
900
967
|
]),
|
|
901
968
|
configStatusRow,
|
|
@@ -905,6 +972,9 @@ function renderSidebar(snapshot, version, theme, configInvalid) {
|
|
|
905
972
|
...getSidebarAgentNames(snapshot).map((agentName) => {
|
|
906
973
|
const model = snapshot.agentModels[agentName] ?? "pending";
|
|
907
974
|
const variant = snapshot.agentVariants[agentName];
|
|
975
|
+
if (compactSidebar) {
|
|
976
|
+
return compactAgentRow(agentName, model, variant, theme);
|
|
977
|
+
}
|
|
908
978
|
return agentRow(agentName, model, variant, theme);
|
|
909
979
|
})
|
|
910
980
|
]);
|
|
@@ -922,15 +992,22 @@ function buildConfigStatusRow(configInvalid, theme) {
|
|
|
922
992
|
text({ fg: theme.textMuted }, ["Run doctor for details"])
|
|
923
993
|
]);
|
|
924
994
|
}
|
|
925
|
-
function
|
|
995
|
+
function readConfigState(directory) {
|
|
926
996
|
let configInvalid = false;
|
|
927
|
-
loadPluginConfig(directory, {
|
|
997
|
+
const config = loadPluginConfig(directory, {
|
|
928
998
|
silent: true,
|
|
929
999
|
onWarning: () => {
|
|
930
1000
|
configInvalid = true;
|
|
931
1001
|
}
|
|
932
1002
|
});
|
|
933
|
-
|
|
1003
|
+
const compactSidebar = config.compactSidebar ?? true;
|
|
1004
|
+
return { configInvalid, compactSidebar };
|
|
1005
|
+
}
|
|
1006
|
+
function readConfigInvalid(directory) {
|
|
1007
|
+
return readConfigState(directory).configInvalid;
|
|
1008
|
+
}
|
|
1009
|
+
function readCompactSidebar(directory) {
|
|
1010
|
+
return readConfigState(directory).compactSidebar;
|
|
934
1011
|
}
|
|
935
1012
|
var plugin = {
|
|
936
1013
|
id: `${PLUGIN_NAME}:tui`,
|
|
@@ -939,7 +1016,7 @@ var plugin = {
|
|
|
939
1016
|
return;
|
|
940
1017
|
const version = meta.version ?? await readPackageVersion() ?? "dev";
|
|
941
1018
|
let configDirectory = getTuiDirectory(api);
|
|
942
|
-
let configInvalid =
|
|
1019
|
+
let { configInvalid, compactSidebar } = readConfigState(configDirectory);
|
|
943
1020
|
let snapshot = readTuiSnapshot();
|
|
944
1021
|
const renderTimer = setInterval(async () => {
|
|
945
1022
|
try {
|
|
@@ -947,7 +1024,7 @@ var plugin = {
|
|
|
947
1024
|
const currentDirectory = getTuiDirectory(api);
|
|
948
1025
|
if (currentDirectory !== configDirectory) {
|
|
949
1026
|
configDirectory = currentDirectory;
|
|
950
|
-
configInvalid =
|
|
1027
|
+
({ configInvalid, compactSidebar } = readConfigState(configDirectory));
|
|
951
1028
|
}
|
|
952
1029
|
api.renderer.requestRender();
|
|
953
1030
|
} catch {}
|
|
@@ -959,7 +1036,7 @@ var plugin = {
|
|
|
959
1036
|
order: 900,
|
|
960
1037
|
slots: {
|
|
961
1038
|
sidebar_content() {
|
|
962
|
-
return renderSidebar(snapshot, version, api.theme.current, configInvalid);
|
|
1039
|
+
return renderSidebar(snapshot, version, api.theme.current, configInvalid, compactSidebar);
|
|
963
1040
|
}
|
|
964
1041
|
}
|
|
965
1042
|
});
|
|
@@ -969,6 +1046,7 @@ var tui_default = plugin;
|
|
|
969
1046
|
export {
|
|
970
1047
|
splitSidebarModelId,
|
|
971
1048
|
readConfigInvalid,
|
|
1049
|
+
readCompactSidebar,
|
|
972
1050
|
getSidebarAgentNames,
|
|
973
1051
|
tui_default as default
|
|
974
1052
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BackgroundJobStore } from './background-job-store';
|
|
1
2
|
import { type TaskOutputState } from './task';
|
|
2
3
|
export interface ContextFile {
|
|
3
4
|
path: string;
|
|
@@ -14,6 +15,7 @@ export interface BackgroundJobRecord {
|
|
|
14
15
|
objective?: string;
|
|
15
16
|
state: BackgroundJobState;
|
|
16
17
|
timedOut: boolean;
|
|
18
|
+
recoverableAfterLiveBusy: boolean;
|
|
17
19
|
statusUncertain: boolean;
|
|
18
20
|
cancellationRequested: boolean;
|
|
19
21
|
terminalUnreconciled: boolean;
|
|
@@ -28,6 +30,9 @@ export interface BackgroundJobRecord {
|
|
|
28
30
|
lastUsedAt: number;
|
|
29
31
|
terminalState?: TaskOutputState;
|
|
30
32
|
contextFiles: ContextFile[];
|
|
33
|
+
totalErrors?: number;
|
|
34
|
+
timeoutCount?: number;
|
|
35
|
+
lastErrorAt?: number;
|
|
31
36
|
}
|
|
32
37
|
export interface BackgroundJobBoardOptions {
|
|
33
38
|
maxReusablePerAgent?: number;
|
|
@@ -52,15 +57,18 @@ export interface BackgroundJobStatusInput {
|
|
|
52
57
|
now?: number;
|
|
53
58
|
}
|
|
54
59
|
type TerminalStateListener = (taskID: string) => void;
|
|
55
|
-
export declare class BackgroundJobBoard {
|
|
60
|
+
export declare class BackgroundJobBoard implements BackgroundJobStore {
|
|
56
61
|
private readonly jobs;
|
|
57
62
|
private readonly counters;
|
|
58
|
-
private
|
|
63
|
+
private terminalStateListeners;
|
|
59
64
|
private readonly maxReusablePerAgent;
|
|
60
65
|
private readonly readContextMinLines;
|
|
61
66
|
private readonly readContextMaxFiles;
|
|
62
67
|
constructor(options?: BackgroundJobBoardOptions);
|
|
68
|
+
addTerminalStateListener(listener: TerminalStateListener): void;
|
|
69
|
+
removeTerminalStateListener(listener: TerminalStateListener): void;
|
|
63
70
|
setTerminalStateListener(listener?: TerminalStateListener): void;
|
|
71
|
+
private notifyTerminalStateListeners;
|
|
64
72
|
registerLaunch(input: BackgroundJobLaunchInput): BackgroundJobRecord;
|
|
65
73
|
updateStatus(input: BackgroundJobStatusInput): BackgroundJobRecord | undefined;
|
|
66
74
|
updateFromStatusOutput(output: string): BackgroundJobRecord | undefined;
|
|
@@ -70,17 +78,29 @@ export declare class BackgroundJobBoard {
|
|
|
70
78
|
force?: boolean;
|
|
71
79
|
}): BackgroundJobRecord | undefined;
|
|
72
80
|
get(taskID: string): BackgroundJobRecord | undefined;
|
|
81
|
+
field<K extends keyof BackgroundJobRecord>(taskID: string, key: K): BackgroundJobRecord[K] | undefined;
|
|
82
|
+
isRunning(taskID: string): boolean;
|
|
83
|
+
isTerminalUnreconciled(taskID: string): boolean;
|
|
84
|
+
getResultSummary(taskID: string): string | undefined;
|
|
85
|
+
getLastLiveBusyAt(taskID: string): number | undefined;
|
|
86
|
+
getParentSessionID(taskID: string): string | undefined;
|
|
87
|
+
getState(taskID: string): BackgroundJobState | undefined;
|
|
73
88
|
resolve(parentSessionID: string, taskIDOrAlias: string): BackgroundJobRecord | undefined;
|
|
74
89
|
resolveReusable(parentSessionID: string, taskIDOrAlias: string, agent?: string): BackgroundJobRecord | undefined;
|
|
90
|
+
resolveRecoverable(parentSessionID: string, taskIDOrAlias: string, agent?: string): BackgroundJobRecord | undefined;
|
|
75
91
|
markUsed(parentSessionID: string, key: string, now?: number): void;
|
|
76
92
|
taskIDs(): Set<string>;
|
|
77
93
|
addContext(taskID: string, files: ContextFile[]): void;
|
|
78
94
|
list(parentSessionID?: string): BackgroundJobRecord[];
|
|
79
95
|
hasRunning(parentSessionID: string): boolean;
|
|
80
96
|
hasTerminalUnreconciled(parentSessionID: string): boolean;
|
|
97
|
+
hasConvergenceSignals(taskID: string, threshold?: number): boolean;
|
|
81
98
|
formatForPrompt(parentSessionID: string, now?: number): string | undefined;
|
|
82
99
|
clearParent(parentSessionID: string): void;
|
|
83
100
|
drop(taskID: string): void;
|
|
101
|
+
deferIfRunning(_sessionId: string): boolean;
|
|
102
|
+
retryDeferredClose(_sessionId: string): boolean;
|
|
103
|
+
clearDeferredClose(_sessionId: string): void;
|
|
84
104
|
private trimReusable;
|
|
85
105
|
private formatReusableJob;
|
|
86
106
|
private nextAlias;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { BackgroundJobBoard, BackgroundJobLaunchInput, BackgroundJobRecord, BackgroundJobStatusInput, ContextFile } from './background-job-board';
|
|
2
|
+
import type { BackgroundJobStore } from './background-job-store';
|
|
3
|
+
import type { TaskOutputState } from './task';
|
|
4
|
+
type TerminalStateListener = (taskID: string) => void;
|
|
5
|
+
/**
|
|
6
|
+
* BackgroundJobCoordinator owns the lifecycle policy for background jobs.
|
|
7
|
+
* It sits between the board and its consumers, providing:
|
|
8
|
+
* - Subscription interface for terminal state notifications (replaces fire-and-forget)
|
|
9
|
+
* - Lifecycle policy: determines when jobs are terminal, when closes should be deferred
|
|
10
|
+
* - Single-writer contract: coordinator is the sole writer to the board
|
|
11
|
+
*
|
|
12
|
+
* The board's guards prevent silent overwrites. The coordinator adds:
|
|
13
|
+
* - Centralized notification with guaranteed delivery
|
|
14
|
+
* - Re-checks board state before notifying (handles races)
|
|
15
|
+
*/
|
|
16
|
+
export declare class BackgroundJobCoordinator implements BackgroundJobStore {
|
|
17
|
+
private readonly board;
|
|
18
|
+
private terminalStateListeners;
|
|
19
|
+
private readonly deferredIdleCloses;
|
|
20
|
+
constructor(board: BackgroundJobBoard);
|
|
21
|
+
addTerminalStateListener(listener: TerminalStateListener): void;
|
|
22
|
+
removeTerminalStateListener(listener: TerminalStateListener): void;
|
|
23
|
+
/**
|
|
24
|
+
* Handle terminal state from board. Re-checks board state to handle races.
|
|
25
|
+
* This is the centralized lifecycle policy.
|
|
26
|
+
*/
|
|
27
|
+
private handleTerminalState;
|
|
28
|
+
/**
|
|
29
|
+
* Evaluate close policy. Returns true if session should close now.
|
|
30
|
+
* Mutates deferred state: adds to deferred set if running, removes if not.
|
|
31
|
+
*/
|
|
32
|
+
deferIfRunning(sessionId: string): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Retry closing a deferred session. Called when a background job completes.
|
|
35
|
+
* Returns true if the session should now close.
|
|
36
|
+
*/
|
|
37
|
+
retryDeferredClose(sessionId: string): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Clear deferred close state for a session being deleted.
|
|
40
|
+
*/
|
|
41
|
+
clearDeferredClose(sessionId: string): void;
|
|
42
|
+
registerLaunch(input: BackgroundJobLaunchInput): BackgroundJobRecord;
|
|
43
|
+
updateStatus(input: BackgroundJobStatusInput): BackgroundJobRecord | undefined;
|
|
44
|
+
updateFromStatusOutput(output: string): BackgroundJobRecord | undefined;
|
|
45
|
+
markRunningFromLiveSession(taskID: string, now?: number): BackgroundJobRecord | undefined;
|
|
46
|
+
markReconciled(taskID: string, now?: number): BackgroundJobRecord | undefined;
|
|
47
|
+
markCancelled(taskID: string, reason?: string, now?: number, options?: {
|
|
48
|
+
force?: boolean;
|
|
49
|
+
}): BackgroundJobRecord | undefined;
|
|
50
|
+
get(taskID: string): BackgroundJobRecord | undefined;
|
|
51
|
+
field<K extends keyof BackgroundJobRecord>(taskID: string, key: K): BackgroundJobRecord[K] | undefined;
|
|
52
|
+
isRunning(taskID: string): boolean;
|
|
53
|
+
isTerminalUnreconciled(taskID: string): boolean;
|
|
54
|
+
getResultSummary(taskID: string): string | undefined;
|
|
55
|
+
getLastLiveBusyAt(taskID: string): number | undefined;
|
|
56
|
+
getParentSessionID(taskID: string): string | undefined;
|
|
57
|
+
getState(taskID: string): TaskOutputState | 'reconciled' | undefined;
|
|
58
|
+
resolve(parentSessionID: string, taskIDOrAlias: string): BackgroundJobRecord | undefined;
|
|
59
|
+
resolveReusable(parentSessionID: string, taskIDOrAlias: string, agent?: string): BackgroundJobRecord | undefined;
|
|
60
|
+
resolveRecoverable(parentSessionID: string, taskIDOrAlias: string, agent?: string): BackgroundJobRecord | undefined;
|
|
61
|
+
markUsed(parentSessionID: string, key: string, now?: number): void;
|
|
62
|
+
taskIDs(): Set<string>;
|
|
63
|
+
addContext(taskID: string, files: ContextFile[]): void;
|
|
64
|
+
list(parentSessionID?: string): BackgroundJobRecord[];
|
|
65
|
+
hasRunning(parentSessionID: string): boolean;
|
|
66
|
+
hasTerminalUnreconciled(parentSessionID: string): boolean;
|
|
67
|
+
hasConvergenceSignals(taskID: string, threshold?: number): boolean;
|
|
68
|
+
formatForPrompt(parentSessionID: string, now?: number): string | undefined;
|
|
69
|
+
clearParent(parentSessionID: string): void;
|
|
70
|
+
drop(taskID: string): void;
|
|
71
|
+
}
|
|
72
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { BackgroundJobLaunchInput, BackgroundJobRecord, BackgroundJobStatusInput, ContextFile } from './background-job-board';
|
|
2
|
+
import type { TaskOutputState } from './task';
|
|
3
|
+
/**
|
|
4
|
+
* Unified interface for background job operations.
|
|
5
|
+
* Both BackgroundJobBoard and BackgroundJobCoordinator satisfy this.
|
|
6
|
+
*
|
|
7
|
+
* ponytail: single interface, both board and coordinator implement it.
|
|
8
|
+
*/
|
|
9
|
+
export interface BackgroundJobStore {
|
|
10
|
+
registerLaunch(input: BackgroundJobLaunchInput): BackgroundJobRecord;
|
|
11
|
+
updateStatus(input: BackgroundJobStatusInput): BackgroundJobRecord | undefined;
|
|
12
|
+
updateFromStatusOutput(output: string): BackgroundJobRecord | undefined;
|
|
13
|
+
markRunningFromLiveSession(taskID: string, now?: number): BackgroundJobRecord | undefined;
|
|
14
|
+
markReconciled(taskID: string, now?: number): BackgroundJobRecord | undefined;
|
|
15
|
+
markCancelled(taskID: string, reason?: string, now?: number, options?: {
|
|
16
|
+
force?: boolean;
|
|
17
|
+
}): BackgroundJobRecord | undefined;
|
|
18
|
+
clearParent(parentSessionID: string): void;
|
|
19
|
+
drop(taskID: string): void;
|
|
20
|
+
addContext(taskID: string, files: ContextFile[]): void;
|
|
21
|
+
markUsed(parentSessionID: string, key: string, now?: number): void;
|
|
22
|
+
get(taskID: string): BackgroundJobRecord | undefined;
|
|
23
|
+
field<K extends keyof BackgroundJobRecord>(taskID: string, key: K): BackgroundJobRecord[K] | undefined;
|
|
24
|
+
isRunning(taskID: string): boolean;
|
|
25
|
+
isTerminalUnreconciled(taskID: string): boolean;
|
|
26
|
+
getResultSummary(taskID: string): string | undefined;
|
|
27
|
+
getLastLiveBusyAt(taskID: string): number | undefined;
|
|
28
|
+
getParentSessionID(taskID: string): string | undefined;
|
|
29
|
+
getState(taskID: string): TaskOutputState | 'reconciled' | undefined;
|
|
30
|
+
resolve(parentSessionID: string, taskIDOrAlias: string): BackgroundJobRecord | undefined;
|
|
31
|
+
resolveReusable(parentSessionID: string, taskIDOrAlias: string, agent?: string): BackgroundJobRecord | undefined;
|
|
32
|
+
resolveRecoverable(parentSessionID: string, taskIDOrAlias: string, agent?: string): BackgroundJobRecord | undefined;
|
|
33
|
+
taskIDs(): Set<string>;
|
|
34
|
+
list(parentSessionID?: string): BackgroundJobRecord[];
|
|
35
|
+
hasRunning(parentSessionID: string): boolean;
|
|
36
|
+
hasTerminalUnreconciled(parentSessionID: string): boolean;
|
|
37
|
+
hasConvergenceSignals(taskID: string, threshold?: number): boolean;
|
|
38
|
+
formatForPrompt(parentSessionID: string, now?: number): string | undefined;
|
|
39
|
+
/** Evaluate close policy. Returns true if session should close now.
|
|
40
|
+
* Mutates deferred state: adds to deferred set if running, removes if not. */
|
|
41
|
+
deferIfRunning(sessionId: string): boolean;
|
|
42
|
+
/** Retry closing a deferred session. Returns true if session should now close. */
|
|
43
|
+
retryDeferredClose(sessionId: string): boolean;
|
|
44
|
+
/** Clear deferred close state for a session being deleted. */
|
|
45
|
+
clearDeferredClose(sessionId: string): void;
|
|
46
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for councillor model fallback chains.
|
|
3
|
+
*
|
|
4
|
+
* Kept free of any schema/validation library import so that runtime consumers
|
|
5
|
+
* (e.g. `CouncilManager`) can resolve a councillor's ordered model chain
|
|
6
|
+
* without pulling in zod or the config schema module.
|
|
7
|
+
*/
|
|
8
|
+
/** A single model in a councillor fallback chain, with optional variant. */
|
|
9
|
+
export type CouncillorModelEntry = {
|
|
10
|
+
id: string;
|
|
11
|
+
variant?: string;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Flatten a councillor model config into an ordered list of model entries.
|
|
15
|
+
*
|
|
16
|
+
* Accepts either a single "provider/model" string or an ordered fallback
|
|
17
|
+
* chain (array of strings and/or `{ id, variant }` entries). Entries that
|
|
18
|
+
* don't carry their own variant fall back to the shared `fallbackVariant`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function normalizeCouncillorModels(model: string | Array<string | CouncillorModelEntry>, fallbackVariant?: string): CouncillorModelEntry[];
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export * from './agent-variant';
|
|
2
2
|
export * from './background-job-board';
|
|
3
|
+
export * from './background-job-coordinator';
|
|
4
|
+
export * from './background-job-store';
|
|
3
5
|
export * from './internal-initiator';
|
|
4
6
|
export { getLogDir, initLogger, log } from './logger';
|
|
5
7
|
export * from './polling';
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
export declare const SLIM_INTERNAL_INITIATOR_MARKER = "<!-- SLIM_INTERNAL_INITIATOR -->";
|
|
2
|
+
export declare const INTERNAL_INITIATOR_METADATA_KEY = "oh-my-opencode-slim.internalInitiator";
|
|
2
3
|
export declare function createInternalAgentTextPart(text: string): {
|
|
3
4
|
type: 'text';
|
|
4
5
|
text: string;
|
|
6
|
+
synthetic: true;
|
|
7
|
+
metadata: {
|
|
8
|
+
'oh-my-opencode-slim.internalInitiator': true;
|
|
9
|
+
};
|
|
5
10
|
};
|
|
6
|
-
export declare function
|
|
11
|
+
export declare function isInternalInitiatorPart(part: unknown): boolean;
|
package/dist/utils/session.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export declare function withTimeout<T>(operation: Promise<T>, timeoutMs: number,
|
|
|
11
11
|
export declare function abortSessionWithTimeout(client: OpencodeClient, sessionId: string, timeoutMs?: number): Promise<void>;
|
|
12
12
|
/**
|
|
13
13
|
* Extract the short model label from a "provider/model" string.
|
|
14
|
-
* E.g. "openai/gpt-5.
|
|
14
|
+
* E.g. "openai/gpt-5.6-luna" → "gpt-5.6-luna"
|
|
15
15
|
*/
|
|
16
16
|
export declare function shortModelLabel(model: string): string;
|
|
17
17
|
export type PromptBody = {
|
|
@@ -52,7 +52,7 @@ export declare function parseModelReference(model: string): {
|
|
|
52
52
|
export declare function promptWithTimeout(client: OpencodeClient, args: Parameters<OpencodeClient['session']['prompt']>[0], timeoutMs: number, signal?: AbortSignal): Promise<void>;
|
|
53
53
|
/**
|
|
54
54
|
* Result of extracting session content.
|
|
55
|
-
* `empty` is true when the assistant produced zero text content
|
|
55
|
+
* `empty` is true when the assistant produced zero text content -
|
|
56
56
|
* the provider returned an empty response (e.g. rate-limited silently).
|
|
57
57
|
*/
|
|
58
58
|
export interface SessionExtractionResult {
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
"setDefaultAgent": {
|
|
9
9
|
"type": "boolean"
|
|
10
10
|
},
|
|
11
|
+
"compactSidebar": {
|
|
12
|
+
"description": "Use the compact TUI sidebar layout. Defaults to true; set false to use the expanded layout.",
|
|
13
|
+
"type": "boolean"
|
|
14
|
+
},
|
|
11
15
|
"autoUpdate": {
|
|
12
16
|
"description": "Disable automatic installation of plugin updates when false. Defaults to true.",
|
|
13
17
|
"type": "boolean"
|
|
@@ -221,6 +225,7 @@
|
|
|
221
225
|
"auto",
|
|
222
226
|
"tmux",
|
|
223
227
|
"zellij",
|
|
228
|
+
"herdr",
|
|
224
229
|
"none"
|
|
225
230
|
]
|
|
226
231
|
},
|
|
@@ -361,10 +366,22 @@
|
|
|
361
366
|
"type": "number",
|
|
362
367
|
"minimum": 0
|
|
363
368
|
},
|
|
369
|
+
"maxRetries": {
|
|
370
|
+
"default": 3,
|
|
371
|
+
"description": "Number of consecutive 429/rate-limit responses tolerated on the same model before aborting (or swapping to the next fallback model when a chain is configured).",
|
|
372
|
+
"type": "integer",
|
|
373
|
+
"minimum": 0,
|
|
374
|
+
"maximum": 9007199254740991
|
|
375
|
+
},
|
|
364
376
|
"retry_on_empty": {
|
|
365
377
|
"default": true,
|
|
366
378
|
"description": "When true (default), empty provider responses are treated as failures, triggering fallback/retry. Set to false to treat them as successes.",
|
|
367
379
|
"type": "boolean"
|
|
380
|
+
},
|
|
381
|
+
"runtimeOverride": {
|
|
382
|
+
"default": true,
|
|
383
|
+
"description": "When true (default), a runtime model selected via /model that is outside the configured fallback chain will still trigger the chain on rate-limit errors. When false, out-of-chain runtime picks are respected and the error surfaces instead of silently falling back to the chain. Models that are members of the chain always fall back regardless of this setting.",
|
|
384
|
+
"type": "boolean"
|
|
368
385
|
}
|
|
369
386
|
},
|
|
370
387
|
"additionalProperties": false
|
|
@@ -417,7 +434,7 @@
|
|
|
417
434
|
"maximum": 5
|
|
418
435
|
},
|
|
419
436
|
"master": {
|
|
420
|
-
"description": "DEPRECATED
|
|
437
|
+
"description": "DEPRECATED - ignored. Council agent synthesizes directly."
|
|
421
438
|
}
|
|
422
439
|
},
|
|
423
440
|
"required": [
|
package/package.json
CHANGED