oh-my-opencode-slim 2.0.5 → 2.1.0

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.
Files changed (52) hide show
  1. package/README.ja-JP.md +47 -21
  2. package/README.ko-KR.md +45 -19
  3. package/README.md +56 -32
  4. package/README.zh-CN.md +48 -24
  5. package/dist/agents/council.d.ts +1 -1
  6. package/dist/agents/index.d.ts +7 -2
  7. package/dist/agents/orchestrator.d.ts +1 -1
  8. package/dist/cli/custom-skills-registry.d.ts +18 -0
  9. package/dist/cli/custom-skills.d.ts +3 -19
  10. package/dist/cli/index.js +1089 -185
  11. package/dist/cli/providers.d.ts +5 -9
  12. package/dist/cli/skills.d.ts +2 -2
  13. package/dist/companion/manager.d.ts +7 -0
  14. package/dist/config/loader.d.ts +5 -2
  15. package/dist/config/schema.d.ts +4 -0
  16. package/dist/council/council-manager.d.ts +1 -1
  17. package/dist/hooks/auto-update-checker/skill-sync.d.ts +59 -1
  18. package/dist/hooks/filter-available-skills/index.d.ts +1 -2
  19. package/dist/hooks/foreground-fallback/index.d.ts +5 -1
  20. package/dist/hooks/image-hook.d.ts +1 -1
  21. package/dist/hooks/index.d.ts +1 -0
  22. package/dist/hooks/loop-command/index.d.ts +13 -0
  23. package/dist/hooks/phase-reminder/index.d.ts +1 -2
  24. package/dist/hooks/task-session-manager/index.d.ts +1 -2
  25. package/dist/hooks/task-session-manager/pending-call-tracker.d.ts +13 -0
  26. package/dist/hooks/task-session-manager/task-context-tracker.d.ts +14 -0
  27. package/dist/hooks/types.d.ts +3 -1
  28. package/dist/index.js +2116 -670
  29. package/dist/interview/dashboard-manager.d.ts +21 -0
  30. package/dist/interview/manager.d.ts +0 -14
  31. package/dist/interview/service.d.ts +7 -0
  32. package/dist/interview/session-server.d.ts +21 -0
  33. package/dist/interview/types.d.ts +3 -1
  34. package/dist/loop/loop-session.d.ts +64 -0
  35. package/dist/multiplexer/factory.d.ts +5 -5
  36. package/dist/multiplexer/herdr/index.d.ts +31 -0
  37. package/dist/multiplexer/index.d.ts +1 -0
  38. package/dist/multiplexer/session-manager.d.ts +1 -0
  39. package/dist/multiplexer/types.d.ts +4 -4
  40. package/dist/tui.js +82 -39
  41. package/dist/utils/background-job-board.d.ts +17 -1
  42. package/dist/utils/session.d.ts +1 -1
  43. package/oh-my-opencode-slim.schema.json +6 -1
  44. package/package.json +1 -1
  45. package/src/skills/clonedeps/SKILL.md +2 -2
  46. package/src/skills/clonedeps/codemap.md +23 -32
  47. package/src/skills/codemap/SKILL.md +2 -2
  48. package/src/skills/codemap.md +63 -36
  49. package/src/skills/loop-engineering/SKILL.md +30 -0
  50. package/src/skills/reflect/SKILL.md +133 -0
  51. package/src/skills/release-smoke-test/SKILL.md +159 -0
  52. package/src/skills/simplify/SKILL.md +6 -6
@@ -0,0 +1,21 @@
1
+ import type { PluginInput } from '@opencode-ai/plugin';
2
+ import type { PluginConfig } from '../config';
3
+ export declare function createDashboardManager(ctx: PluginInput, config: PluginConfig, dashboardPort: number, outputFolder: string): {
4
+ registerCommand: (config: Record<string, unknown>) => void;
5
+ handleCommandExecuteBefore: (input: {
6
+ command: string;
7
+ sessionID: string;
8
+ arguments: string;
9
+ }, output: {
10
+ parts: Array<{
11
+ type: string;
12
+ text?: string;
13
+ }>;
14
+ }) => Promise<void>;
15
+ handleEvent: (input: {
16
+ event: {
17
+ type: string;
18
+ properties?: Record<string, unknown>;
19
+ };
20
+ }) => Promise<void>;
21
+ };
@@ -1,19 +1,5 @@
1
1
  import type { PluginInput } from '@opencode-ai/plugin';
2
2
  import type { PluginConfig } from '../config';
3
- /**
4
- * Interview Manager — Composition root.
5
- *
6
- * Two modes:
7
- *
8
- * 1. **Dashboard mode** (dashboard:true or port>0):
9
- * First process to bind the port becomes the dashboard (dumb aggregator).
10
- * Other processes register as sessions and push state to it.
11
- * Sessions drive LLM interaction locally, dashboard just serves the web UI.
12
- *
13
- * 2. **Per-session mode** (default, port=0, dashboard:false):
14
- * Upstream behavior. Each process runs its own interview server on a random
15
- * port. Lazy startup on first /interview command.
16
- */
17
3
  export declare function createInterviewManager(ctx: PluginInput, config: PluginConfig): {
18
4
  registerCommand: (config: Record<string, unknown>) => void;
19
5
  handleCommandExecuteBefore: (input: {
@@ -1,6 +1,13 @@
1
1
  import type { PluginInput } from '@opencode-ai/plugin';
2
2
  import type { InterviewConfig } from '../config';
3
3
  import type { InterviewAnswer, InterviewFileItem, InterviewListItem, InterviewRecord, InterviewState } from './types';
4
+ /**
5
+ * Cap on retained abandoned interview records. Abandoned interviews are kept
6
+ * briefly so a still-open browser tab can render their final state, but
7
+ * without a bound the `interviewsById` and `browserOpened` collections grow
8
+ * for the life of a long-running session/dashboard process.
9
+ */
10
+ export declare const MAX_RETAINED_ABANDONED = 50;
4
11
  export declare function createInterviewService(ctx: PluginInput, config?: InterviewConfig, deps?: {
5
12
  openBrowser?: (url: string) => void;
6
13
  env?: NodeJS.ProcessEnv;
@@ -0,0 +1,21 @@
1
+ import type { PluginInput } from '@opencode-ai/plugin';
2
+ import type { InterviewConfig } from '../config';
3
+ export declare function createPerSessionInterviewServer(ctx: PluginInput, interviewConfig: InterviewConfig | undefined, outputFolder: string): {
4
+ registerCommand: (config: Record<string, unknown>) => void;
5
+ handleCommandExecuteBefore: (input: {
6
+ command: string;
7
+ sessionID: string;
8
+ arguments: string;
9
+ }, output: {
10
+ parts: Array<{
11
+ type: string;
12
+ text?: string;
13
+ }>;
14
+ }) => Promise<void>;
15
+ handleEvent: (input: {
16
+ event: {
17
+ type: string;
18
+ properties?: Record<string, unknown>;
19
+ };
20
+ }) => Promise<void>;
21
+ };
@@ -14,7 +14,7 @@ export interface InterviewAssistantState {
14
14
  title?: string;
15
15
  questions: InterviewQuestion[];
16
16
  }
17
- /** Raw question object from LLM output loose, everything optional. */
17
+ /** Raw question object from LLM output - loose, everything optional. */
18
18
  export declare const RawQuestionSchema: z.ZodObject<{
19
19
  id: z.ZodOptional<z.ZodString>;
20
20
  question: z.ZodOptional<z.ZodString>;
@@ -33,6 +33,8 @@ export interface InterviewRecord {
33
33
  idea: string;
34
34
  markdownPath: string;
35
35
  createdAt: string;
36
+ abandonedAt?: string;
37
+ abandonedOrder?: number;
36
38
  status: 'active' | 'abandoned';
37
39
  baseMessageCount: number;
38
40
  }
@@ -0,0 +1,64 @@
1
+ export declare function loopDirname(loopID: string, goal: string): string;
2
+ export type LoopPhase = 'executing' | 'verifying' | 'done' | 'escalated' | 'cancelled';
3
+ export type ExecuteAgent = 'fixer' | 'designer' | 'explorer' | 'librarian';
4
+ export type VerifyAgent = 'oracle' | 'observer' | 'test';
5
+ export type SuccessCriterion = {
6
+ type: 'test';
7
+ command: string;
8
+ } | {
9
+ type: 'build';
10
+ command: string;
11
+ } | {
12
+ type: 'lint';
13
+ command: string;
14
+ } | {
15
+ type: 'fileExists';
16
+ path: string;
17
+ } | {
18
+ type: 'command';
19
+ command: string;
20
+ expectExitCode?: number;
21
+ } | {
22
+ type: 'oracle';
23
+ } | {
24
+ type: 'observer';
25
+ } | {
26
+ type: 'manual';
27
+ };
28
+ export interface LoopDefinition {
29
+ goal: string;
30
+ successCriteria: string;
31
+ success: SuccessCriterion;
32
+ maxAttempts: number;
33
+ executeAgent: ExecuteAgent;
34
+ verifyAgent: VerifyAgent;
35
+ contextFiles?: string[];
36
+ parentSessionID?: string;
37
+ }
38
+ export type VerificationResult = {
39
+ passed: true;
40
+ reason: string;
41
+ } | {
42
+ passed: false;
43
+ reason: string;
44
+ suggestedFix?: string;
45
+ };
46
+ export interface AttemptRecord {
47
+ attemptNumber: number;
48
+ executionResult: string;
49
+ verificationResult: VerificationResult;
50
+ artifactPaths?: string[];
51
+ }
52
+ export interface LoopSession {
53
+ loopID: string;
54
+ definition: LoopDefinition;
55
+ currentPhase: LoopPhase;
56
+ attempts: number;
57
+ activeJobID?: string;
58
+ history: AttemptRecord[];
59
+ historyDir: string;
60
+ manualReviewPending: boolean;
61
+ }
62
+ export declare function createLoopSession(definition: LoopDefinition, loopID: string): LoopSession;
63
+ export declare function compactAttempt(attempt: AttemptRecord): string;
64
+ export declare function writeHistoryFile(session: LoopSession): void;
@@ -6,9 +6,9 @@ import type { Multiplexer } from './types';
6
6
  /**
7
7
  * Create a multiplexer instance based on config.
8
8
  *
9
- * Do not cache instances: tmux/zellij integrations may depend on
10
- * per-process environment like TMUX_PANE/ZELLIJ, which should be captured
11
- * fresh for each plugin context.
9
+ * Do not cache instances: tmux/zellij/herdr integrations may depend on
10
+ * per-process environment like TMUX_PANE/ZELLIJ/HERDR_PANE_ID, which should
11
+ * be captured fresh for each plugin context.
12
12
  */
13
13
  export declare function getMultiplexer(config: MultiplexerConfig): Multiplexer | null;
14
14
  /**
@@ -17,9 +17,9 @@ export declare function getMultiplexer(config: MultiplexerConfig): Multiplexer |
17
17
  export declare function clearMultiplexerCache(): void;
18
18
  /**
19
19
  * Get the effective multiplexer type for auto mode
20
- * Returns the actual type that would be used (tmux/zellij/none)
20
+ * Returns the actual type that would be used (tmux/zellij/herdr/none)
21
21
  */
22
- export declare function getAutoMultiplexerType(): 'tmux' | 'zellij' | 'none';
22
+ export declare function getAutoMultiplexerType(): 'tmux' | 'zellij' | 'herdr' | 'none';
23
23
  /**
24
24
  * Start background availability check for a multiplexer
25
25
  */
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Herdr multiplexer implementation
3
+ *
4
+ * Splits panes for sub-agent sessions in Herdr.
5
+ *
6
+ * Herdr is an agent-aware terminal multiplexer (workspaces → tabs → panes).
7
+ * Pane IDs use the format `w<workspace>:p<pane>`. The CLI outputs
8
+ * newline-delimited JSON; `pane split` returns a `pane_info` result whose
9
+ * `pane.pane_id` field is the new pane's ID.
10
+ *
11
+ * Environment detection: Herdr injects `HERDR_ENV=1` and `HERDR_PANE_ID`
12
+ * into every pane it manages.
13
+ */
14
+ import type { MultiplexerLayout } from '../../config/schema';
15
+ import type { Multiplexer, PaneResult } from '../types';
16
+ export declare class HerdrMultiplexer implements Multiplexer {
17
+ readonly type: "herdr";
18
+ private binaryPath;
19
+ private hasChecked;
20
+ private readonly parentPaneId;
21
+ private readonly paneDirection;
22
+ constructor(layout?: MultiplexerLayout, mainPaneSize?: number);
23
+ isAvailable(): Promise<boolean>;
24
+ isInsideSession(): boolean;
25
+ spawnPane(sessionId: string, description: string, serverUrl: string, directory: string): Promise<PaneResult>;
26
+ closePane(paneId: string): Promise<boolean>;
27
+ applyLayout(_layout: MultiplexerLayout, _mainPaneSize: number): Promise<void>;
28
+ private targetPaneArg;
29
+ private getBinary;
30
+ private findBinary;
31
+ }
@@ -2,6 +2,7 @@
2
2
  * Multiplexer module exports
3
3
  */
4
4
  export { clearMultiplexerCache, getMultiplexer, startAvailabilityCheck, } from './factory';
5
+ export { HerdrMultiplexer } from './herdr';
5
6
  export { MultiplexerSessionManager, TmuxSessionManager, } from './session-manager';
6
7
  export { TmuxMultiplexer } from './tmux';
7
8
  export type { Multiplexer, PaneResult } from './types';
@@ -49,6 +49,7 @@ export declare class MultiplexerSessionManager {
49
49
  private isTrackedOrSpawning;
50
50
  private updatePolling;
51
51
  private getSessionId;
52
+ private backgroundJobState;
52
53
  private isRunningBackgroundJob;
53
54
  retryDeferredIdleClose(sessionId: string): Promise<void>;
54
55
  cleanup(): Promise<void>;
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Multiplexer abstraction layer
3
3
  *
4
- * Provides a unified interface for terminal multiplexers (tmux, zellij, etc.)
5
- * to spawn and manage panes for child agent sessions.
4
+ * Provides a unified interface for terminal multiplexers (tmux, zellij,
5
+ * herdr, etc.) to spawn and manage panes for child agent sessions.
6
6
  */
7
7
  import type { MultiplexerConfig, MultiplexerLayout } from '../config/schema';
8
8
  export interface PaneResult {
@@ -11,10 +11,10 @@ export interface PaneResult {
11
11
  }
12
12
  /**
13
13
  * Core multiplexer interface
14
- * Implementations: TmuxMultiplexer, ZellijMultiplexer
14
+ * Implementations: TmuxMultiplexer, ZellijMultiplexer, HerdrMultiplexer
15
15
  */
16
16
  export interface Multiplexer {
17
- readonly type: 'tmux' | 'zellij';
17
+ readonly type: 'tmux' | 'zellij' | 'herdr';
18
18
  /**
19
19
  * Check if the multiplexer binary is available on the system
20
20
  */
package/dist/tui.js CHANGED
@@ -232,7 +232,7 @@ var CouncilConfigSchema = z.object({
232
232
  default_preset: z.string().default("default"),
233
233
  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
234
  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 ignored. Council agent synthesizes directly.")
235
+ master: z.unknown().optional().describe("DEPRECATED - ignored. Council agent synthesizes directly.")
236
236
  }).transform((data) => {
237
237
  const deprecated = [];
238
238
  if (data.master !== undefined)
@@ -298,7 +298,13 @@ var AgentOverrideConfigSchema = z2.object({
298
298
  options: z2.record(z2.string(), z2.unknown()).optional(),
299
299
  displayName: z2.string().min(1).optional()
300
300
  }).strict();
301
- var MultiplexerTypeSchema = z2.enum(["auto", "tmux", "zellij", "none"]);
301
+ var MultiplexerTypeSchema = z2.enum([
302
+ "auto",
303
+ "tmux",
304
+ "zellij",
305
+ "herdr",
306
+ "none"
307
+ ]);
302
308
  var MultiplexerLayoutSchema = z2.enum([
303
309
  "main-horizontal",
304
310
  "main-vertical",
@@ -367,24 +373,13 @@ var AcpAgentConfigSchema = z2.object({
367
373
  permissionMode: AcpAgentPermissionModeSchema.default("ask")
368
374
  }).strict();
369
375
  var AcpAgentsConfigSchema = z2.record(z2.string(), AcpAgentConfigSchema);
370
- function validateCustomOnlyPromptFields(overrides, ctx, pathPrefix) {
376
+ function rejectOrchestratorPromptOnOrchestrator(overrides, ctx, pathPrefix) {
371
377
  for (const [name, override] of Object.entries(overrides)) {
372
- const isBuiltInOrAlias = ALL_AGENT_NAMES.includes(name) || AGENT_ALIASES[name] !== undefined;
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) {
378
+ if (name === "orchestrator" && override.orchestratorPrompt !== undefined) {
384
379
  ctx.addIssue({
385
380
  code: z2.ZodIssueCode.custom,
386
381
  path: [...pathPrefix, name, "orchestratorPrompt"],
387
- message: "orchestratorPrompt is only supported for custom agents"
382
+ message: "orchestratorPrompt is not supported for the orchestrator agent"
388
383
  });
389
384
  }
390
385
  }
@@ -392,6 +387,7 @@ function validateCustomOnlyPromptFields(overrides, ctx, pathPrefix) {
392
387
  var PluginConfigSchema = z2.object({
393
388
  preset: z2.string().optional(),
394
389
  setDefaultAgent: z2.boolean().optional(),
390
+ compactSidebar: z2.boolean().optional().describe("Use the compact TUI sidebar layout when enabled."),
395
391
  autoUpdate: z2.boolean().optional().describe("Disable automatic installation of plugin updates when false. Defaults to true."),
396
392
  presets: z2.record(z2.string(), PresetSchema).optional(),
397
393
  agents: z2.record(z2.string(), AgentOverrideConfigSchema).optional(),
@@ -410,11 +406,14 @@ var PluginConfigSchema = z2.object({
410
406
  acpAgents: AcpAgentsConfigSchema.optional()
411
407
  }).superRefine((value, ctx) => {
412
408
  if (value.agents) {
413
- validateCustomOnlyPromptFields(value.agents, ctx, ["agents"]);
409
+ rejectOrchestratorPromptOnOrchestrator(value.agents, ctx, ["agents"]);
414
410
  }
415
411
  if (value.presets) {
416
412
  for (const [presetName, preset] of Object.entries(value.presets)) {
417
- validateCustomOnlyPromptFields(preset, ctx, ["presets", presetName]);
413
+ rejectOrchestratorPromptOnOrchestrator(preset, ctx, [
414
+ "presets",
415
+ presetName
416
+ ]);
418
417
  }
419
418
  }
420
419
  });
@@ -470,7 +469,7 @@ function getAgentMcpList(agentName, config) {
470
469
  return defaultMcps ?? [];
471
470
  }
472
471
 
473
- // src/cli/custom-skills.ts
472
+ // src/cli/custom-skills-registry.ts
474
473
  var CUSTOM_SKILLS = [
475
474
  {
476
475
  name: "simplify",
@@ -508,6 +507,12 @@ var CUSTOM_SKILLS = [
508
507
  allowedAgents: ["orchestrator"],
509
508
  sourcePath: "src/skills/oh-my-opencode-slim"
510
509
  },
510
+ {
511
+ name: "release-smoke-test",
512
+ description: "Validate packed release candidates and bugfixes before public publish",
513
+ allowedAgents: ["orchestrator"],
514
+ sourcePath: "src/skills/release-smoke-test"
515
+ },
511
516
  {
512
517
  name: "worktrees",
513
518
  description: "Manage Git worktrees as OMO safe isolated coding lanes for complex/risky/parallel work",
@@ -605,6 +610,7 @@ function mergePluginConfigs(base, override) {
605
610
  ...base,
606
611
  ...override,
607
612
  agents: deepMerge(base.agents, override.agents),
613
+ presets: deepMerge(base.presets, override.presets),
608
614
  tmux: deepMerge(base.tmux, override.tmux),
609
615
  multiplexer: deepMerge(base.multiplexer, override.multiplexer),
610
616
  interview: deepMerge(base.interview, override.interview),
@@ -676,15 +682,33 @@ function loadPluginConfig(directory, options) {
676
682
  }
677
683
  return config;
678
684
  }
679
- function loadAgentPrompt(agentName, preset) {
685
+ function loadAgentPrompt(agentName, optionsOrPreset) {
686
+ let preset;
687
+ let projectDirectory;
688
+ if (typeof optionsOrPreset === "string") {
689
+ preset = optionsOrPreset;
690
+ } else if (optionsOrPreset && typeof optionsOrPreset === "object") {
691
+ preset = optionsOrPreset.preset;
692
+ projectDirectory = optionsOrPreset.projectDirectory;
693
+ }
680
694
  const presetDirName = preset && /^[a-zA-Z0-9_-]+$/.test(preset) ? preset : undefined;
681
- const promptSearchDirs = getConfigSearchDirs().flatMap((configDir) => {
682
- const promptsDir = path.join(configDir, PROMPTS_DIR_NAME);
683
- return presetDirName ? [path.join(promptsDir, presetDirName), promptsDir] : [promptsDir];
684
- });
685
- const result = {};
695
+ const searchDirs = [];
696
+ if (projectDirectory && presetDirName) {
697
+ searchDirs.push(path.join(projectDirectory, ".opencode", PROMPTS_DIR_NAME, presetDirName));
698
+ }
699
+ if (projectDirectory) {
700
+ searchDirs.push(path.join(projectDirectory, ".opencode", PROMPTS_DIR_NAME));
701
+ }
702
+ if (presetDirName) {
703
+ for (const userDir of getConfigSearchDirs()) {
704
+ searchDirs.push(path.join(userDir, PROMPTS_DIR_NAME, presetDirName));
705
+ }
706
+ }
707
+ for (const userDir of getConfigSearchDirs()) {
708
+ searchDirs.push(path.join(userDir, PROMPTS_DIR_NAME));
709
+ }
686
710
  const readFirstPrompt = (fileName, errorPrefix) => {
687
- for (const dir of promptSearchDirs) {
711
+ for (const dir of searchDirs) {
688
712
  const promptPath = path.join(dir, fileName);
689
713
  if (!fs.existsSync(promptPath)) {
690
714
  continue;
@@ -697,6 +721,7 @@ function loadAgentPrompt(agentName, preset) {
697
721
  }
698
722
  return;
699
723
  };
724
+ const result = {};
700
725
  result.prompt = readFirstPrompt(`${agentName}.md`, "Error reading prompt file");
701
726
  result.appendPrompt = readFirstPrompt(`${agentName}_append.md`, "Error reading append prompt file");
702
727
  return result;
@@ -859,25 +884,36 @@ function getSidebarAgentNames(snapshot) {
859
884
  function agentRow(label, model, variant, theme) {
860
885
  const modelParts = splitSidebarModelId(model);
861
886
  const detailRows = [];
887
+ function detailRow(fieldLabel, value) {
888
+ return box({ width: "100%", flexDirection: "row", paddingLeft: 2 }, [
889
+ text({ fg: theme.textMuted, width: 9 }, [fieldLabel]),
890
+ text({ fg: theme.textMuted }, [value])
891
+ ]);
892
+ }
862
893
  if (modelParts.provider) {
863
- detailRows.push(agentDetailRow("provider", modelParts.provider, theme));
894
+ detailRows.push(detailRow("provider", modelParts.provider));
864
895
  }
865
- detailRows.push(agentDetailRow("model", modelParts.model, theme));
896
+ detailRows.push(detailRow("model", modelParts.model));
866
897
  if (variant) {
867
- detailRows.push(agentDetailRow("variant", variant, theme));
898
+ detailRows.push(detailRow("variant", variant));
868
899
  }
869
900
  return box({ width: "100%", flexDirection: "column", marginBottom: 1 }, [
870
901
  text({ fg: theme.textMuted }, [label]),
871
902
  ...detailRows
872
903
  ]);
873
904
  }
874
- function agentDetailRow(label, value, theme) {
875
- return box({ width: "100%", flexDirection: "row", paddingLeft: 2 }, [
876
- text({ fg: theme.textMuted, width: 9 }, [label]),
905
+ function compactAgentRow(label, model, variant, theme) {
906
+ const value = variant ? `${model} (${variant})` : model;
907
+ return box({
908
+ width: "100%",
909
+ flexDirection: "row",
910
+ justifyContent: "space-between"
911
+ }, [
912
+ text({ fg: theme.textMuted, width: 14 }, [label]),
877
913
  text({ fg: theme.textMuted }, [value])
878
914
  ]);
879
915
  }
880
- function renderSidebar(snapshot, version, theme, configInvalid) {
916
+ function renderSidebar(snapshot, version, theme, configInvalid, compactSidebar) {
881
917
  const configStatusRow = buildConfigStatusRow(configInvalid, theme);
882
918
  return box({
883
919
  width: "100%",
@@ -905,6 +941,9 @@ function renderSidebar(snapshot, version, theme, configInvalid) {
905
941
  ...getSidebarAgentNames(snapshot).map((agentName) => {
906
942
  const model = snapshot.agentModels[agentName] ?? "pending";
907
943
  const variant = snapshot.agentVariants[agentName];
944
+ if (compactSidebar) {
945
+ return compactAgentRow(agentName, model, variant, theme);
946
+ }
908
947
  return agentRow(agentName, model, variant, theme);
909
948
  })
910
949
  ]);
@@ -922,15 +961,19 @@ function buildConfigStatusRow(configInvalid, theme) {
922
961
  text({ fg: theme.textMuted }, ["Run doctor for details"])
923
962
  ]);
924
963
  }
925
- function readConfigInvalid(directory) {
964
+ function readConfigState(directory) {
926
965
  let configInvalid = false;
927
- loadPluginConfig(directory, {
966
+ const config = loadPluginConfig(directory, {
928
967
  silent: true,
929
968
  onWarning: () => {
930
969
  configInvalid = true;
931
970
  }
932
971
  });
933
- return configInvalid;
972
+ const compactSidebar = config.compactSidebar ?? false;
973
+ return { configInvalid, compactSidebar };
974
+ }
975
+ function readConfigInvalid(directory) {
976
+ return readConfigState(directory).configInvalid;
934
977
  }
935
978
  var plugin = {
936
979
  id: `${PLUGIN_NAME}:tui`,
@@ -939,7 +982,7 @@ var plugin = {
939
982
  return;
940
983
  const version = meta.version ?? await readPackageVersion() ?? "dev";
941
984
  let configDirectory = getTuiDirectory(api);
942
- let configInvalid = readConfigInvalid(configDirectory);
985
+ let { configInvalid, compactSidebar } = readConfigState(configDirectory);
943
986
  let snapshot = readTuiSnapshot();
944
987
  const renderTimer = setInterval(async () => {
945
988
  try {
@@ -947,7 +990,7 @@ var plugin = {
947
990
  const currentDirectory = getTuiDirectory(api);
948
991
  if (currentDirectory !== configDirectory) {
949
992
  configDirectory = currentDirectory;
950
- configInvalid = readConfigInvalid(configDirectory);
993
+ ({ configInvalid, compactSidebar } = readConfigState(configDirectory));
951
994
  }
952
995
  api.renderer.requestRender();
953
996
  } catch {}
@@ -959,7 +1002,7 @@ var plugin = {
959
1002
  order: 900,
960
1003
  slots: {
961
1004
  sidebar_content() {
962
- return renderSidebar(snapshot, version, api.theme.current, configInvalid);
1005
+ return renderSidebar(snapshot, version, api.theme.current, configInvalid, compactSidebar);
963
1006
  }
964
1007
  }
965
1008
  });
@@ -14,6 +14,7 @@ export interface BackgroundJobRecord {
14
14
  objective?: string;
15
15
  state: BackgroundJobState;
16
16
  timedOut: boolean;
17
+ recoverableAfterLiveBusy: boolean;
17
18
  statusUncertain: boolean;
18
19
  cancellationRequested: boolean;
19
20
  terminalUnreconciled: boolean;
@@ -28,6 +29,9 @@ export interface BackgroundJobRecord {
28
29
  lastUsedAt: number;
29
30
  terminalState?: TaskOutputState;
30
31
  contextFiles: ContextFile[];
32
+ totalErrors?: number;
33
+ timeoutCount?: number;
34
+ lastErrorAt?: number;
31
35
  }
32
36
  export interface BackgroundJobBoardOptions {
33
37
  maxReusablePerAgent?: number;
@@ -55,12 +59,15 @@ type TerminalStateListener = (taskID: string) => void;
55
59
  export declare class BackgroundJobBoard {
56
60
  private readonly jobs;
57
61
  private readonly counters;
58
- private terminalStateListener?;
62
+ private terminalStateListeners;
59
63
  private readonly maxReusablePerAgent;
60
64
  private readonly readContextMinLines;
61
65
  private readonly readContextMaxFiles;
62
66
  constructor(options?: BackgroundJobBoardOptions);
67
+ addTerminalStateListener(listener: TerminalStateListener): void;
68
+ removeTerminalStateListener(listener: TerminalStateListener): void;
63
69
  setTerminalStateListener(listener?: TerminalStateListener): void;
70
+ private notifyTerminalStateListeners;
64
71
  registerLaunch(input: BackgroundJobLaunchInput): BackgroundJobRecord;
65
72
  updateStatus(input: BackgroundJobStatusInput): BackgroundJobRecord | undefined;
66
73
  updateFromStatusOutput(output: string): BackgroundJobRecord | undefined;
@@ -70,14 +77,23 @@ export declare class BackgroundJobBoard {
70
77
  force?: boolean;
71
78
  }): BackgroundJobRecord | undefined;
72
79
  get(taskID: string): BackgroundJobRecord | undefined;
80
+ field<K extends keyof BackgroundJobRecord>(taskID: string, key: K): BackgroundJobRecord[K] | undefined;
81
+ isRunning(taskID: string): boolean;
82
+ isTerminalUnreconciled(taskID: string): boolean;
83
+ getResultSummary(taskID: string): string | undefined;
84
+ getLastLiveBusyAt(taskID: string): number | undefined;
85
+ getParentSessionID(taskID: string): string | undefined;
86
+ getState(taskID: string): BackgroundJobState | undefined;
73
87
  resolve(parentSessionID: string, taskIDOrAlias: string): BackgroundJobRecord | undefined;
74
88
  resolveReusable(parentSessionID: string, taskIDOrAlias: string, agent?: string): BackgroundJobRecord | undefined;
89
+ resolveRecoverable(parentSessionID: string, taskIDOrAlias: string, agent?: string): BackgroundJobRecord | undefined;
75
90
  markUsed(parentSessionID: string, key: string, now?: number): void;
76
91
  taskIDs(): Set<string>;
77
92
  addContext(taskID: string, files: ContextFile[]): void;
78
93
  list(parentSessionID?: string): BackgroundJobRecord[];
79
94
  hasRunning(parentSessionID: string): boolean;
80
95
  hasTerminalUnreconciled(parentSessionID: string): boolean;
96
+ hasConvergenceSignals(taskID: string, threshold?: number): boolean;
81
97
  formatForPrompt(parentSessionID: string, now?: number): string | undefined;
82
98
  clearParent(parentSessionID: string): void;
83
99
  drop(taskID: string): void;
@@ -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 when enabled.",
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
  },
@@ -417,7 +422,7 @@
417
422
  "maximum": 5
418
423
  },
419
424
  "master": {
420
- "description": "DEPRECATED ignored. Council agent synthesizes directly."
425
+ "description": "DEPRECATED - ignored. Council agent synthesizes directly."
421
426
  }
422
427
  },
423
428
  "required": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-opencode-slim",
3
- "version": "2.0.5",
3
+ "version": "2.1.0",
4
4
  "description": "Lightweight agent orchestration plugin for OpenCode - a slimmed-down fork of oh-my-opencode",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -217,9 +217,9 @@ sentence so future agents do not need an extra read just to know what is there:
217
217
  Read-only dependency source repositories are available under
218
218
  `.slim/clonedeps/repos/` for inspection. Do not edit these clones.
219
219
 
220
- - `.slim/clonedeps/repos/<safe-name>/` `<repo>` at `<ref>`; <one sentence on
220
+ - `.slim/clonedeps/repos/<safe-name>/` - `<repo>` at `<ref>`; <one sentence on
221
221
  why this source is useful>.
222
- - `.slim/clonedeps/repos/<safe-name-2>/` `<repo>` at `<ref>`; <one sentence on
222
+ - `.slim/clonedeps/repos/<safe-name-2>/` - `<repo>` at `<ref>`; <one sentence on
223
223
  why this source is useful>.
224
224
  ```
225
225