oh-my-opencode-slim 0.9.5 → 0.9.7
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 +5 -1
- package/dist/agents/orchestrator.d.ts +1 -0
- package/dist/cli/index.js +6 -0
- package/dist/config/schema.d.ts +11 -0
- package/dist/hooks/auto-update-checker/index.d.ts +1 -0
- package/dist/index.js +1978 -467
- package/dist/interview/index.d.ts +1 -0
- package/dist/interview/manager.d.ts +39 -0
- package/dist/interview/parser.d.ts +11 -0
- package/dist/interview/prompts.d.ts +7 -0
- package/dist/interview/repository.d.ts +12 -0
- package/dist/interview/schemas.d.ts +27 -0
- package/dist/interview/server.d.ts +7 -0
- package/dist/interview/service.d.ts +27 -0
- package/dist/interview/types.d.ts +46 -0
- package/dist/interview/ui.d.ts +1 -0
- package/oh-my-opencode-slim.schema.json +20 -0
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,194 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __export = (target, all) => {
|
|
4
|
+
for (var name in all)
|
|
5
|
+
__defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true,
|
|
8
|
+
configurable: true,
|
|
9
|
+
set: (newValue) => all[name] = () => newValue
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// src/agents/orchestrator.ts
|
|
14
|
+
var exports_orchestrator = {};
|
|
15
|
+
__export(exports_orchestrator, {
|
|
16
|
+
resolvePrompt: () => resolvePrompt,
|
|
17
|
+
createOrchestratorAgent: () => createOrchestratorAgent,
|
|
18
|
+
ORCHESTRATOR_PROMPT: () => ORCHESTRATOR_PROMPT
|
|
19
|
+
});
|
|
20
|
+
function resolvePrompt(base, customPrompt, customAppendPrompt) {
|
|
21
|
+
if (customPrompt)
|
|
22
|
+
return customPrompt;
|
|
23
|
+
if (customAppendPrompt)
|
|
24
|
+
return `${base}
|
|
25
|
+
|
|
26
|
+
${customAppendPrompt}`;
|
|
27
|
+
return base;
|
|
28
|
+
}
|
|
29
|
+
function createOrchestratorAgent(model, customPrompt, customAppendPrompt) {
|
|
30
|
+
const prompt = resolvePrompt(ORCHESTRATOR_PROMPT, customPrompt, customAppendPrompt);
|
|
31
|
+
const definition = {
|
|
32
|
+
name: "orchestrator",
|
|
33
|
+
description: "AI coding orchestrator that delegates tasks to specialist agents for optimal quality, speed, and cost",
|
|
34
|
+
config: {
|
|
35
|
+
temperature: 0.1,
|
|
36
|
+
prompt
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
if (Array.isArray(model)) {
|
|
40
|
+
definition._modelArray = model.map((m) => typeof m === "string" ? { id: m } : m);
|
|
41
|
+
} else if (typeof model === "string" && model) {
|
|
42
|
+
definition.config.model = model;
|
|
43
|
+
}
|
|
44
|
+
return definition;
|
|
45
|
+
}
|
|
46
|
+
var ORCHESTRATOR_PROMPT = `<Role>
|
|
47
|
+
You are an AI coding orchestrator that optimizes for quality, speed, cost, and reliability by delegating to specialists when it provides net efficiency gains.
|
|
48
|
+
</Role>
|
|
49
|
+
|
|
50
|
+
<Agents>
|
|
51
|
+
|
|
52
|
+
@explorer
|
|
53
|
+
- Role: Parallel search specialist for discovering unknowns across the codebase
|
|
54
|
+
- Stats: 3x faster codebase search than orchestrator, 1/2 cost of orchestrator
|
|
55
|
+
- Capabilities: Glob, grep, AST queries to locate files, symbols, patterns
|
|
56
|
+
- **Delegate when:** Need to discover what exists before planning \u2022 Parallel searches speed discovery \u2022 Need summarized map vs full contents \u2022 Broad/uncertain scope
|
|
57
|
+
- **Don't delegate when:** Know the path and need actual content \u2022 Need full file anyway \u2022 Single specific lookup \u2022 About to edit the file
|
|
58
|
+
|
|
59
|
+
@librarian
|
|
60
|
+
- Role: Authoritative source for current library docs and API references
|
|
61
|
+
- Stats: 10x better finding up-to-date library docs than orchestrator, 1/2 cost of orchestrator
|
|
62
|
+
- Capabilities: Fetches latest official docs, examples, API signatures, version-specific behavior via grep_app MCP
|
|
63
|
+
- **Delegate when:** Libraries with frequent API changes (React, Next.js, AI SDKs) \u2022 Complex APIs needing official examples (ORMs, auth) \u2022 Version-specific behavior matters \u2022 Unfamiliar library \u2022 Edge cases or advanced features \u2022 Nuanced best practices
|
|
64
|
+
- **Don't delegate when:** Standard usage you're confident about (\`Array.map()\`, \`fetch()\`) \u2022 Simple stable APIs \u2022 General programming knowledge \u2022 Info already in conversation \u2022 Built-in language features
|
|
65
|
+
- **Rule of thumb:** "How does this library work?" \u2192 @librarian. "How does programming work?" \u2192 yourself.
|
|
66
|
+
|
|
67
|
+
@oracle
|
|
68
|
+
- Role: Strategic advisor for high-stakes decisions and persistent problems, code reviewer
|
|
69
|
+
- Stats: 5x better decision maker, problem solver, investigator than orchestrator, 0.8x speed of orchestrator, same cost.
|
|
70
|
+
- Capabilities: Deep architectural reasoning, system-level trade-offs, complex debugging, code review, simplification, maintainability review
|
|
71
|
+
- **Delegate when:** Major architectural decisions with long-term impact \u2022 Problems persisting after 2+ fix attempts \u2022 High-risk multi-system refactors \u2022 Costly trade-offs (performance vs maintainability) \u2022 Complex debugging with unclear root cause \u2022 Security/scalability/data integrity decisions \u2022 Genuinely uncertain and cost of wrong choice is high \u2022 When a workflow calls for a **reviewer** subagent \u2022 Code needs simplification or YAGNI scrutiny
|
|
72
|
+
- **Don't delegate when:** Routine decisions you're confident about \u2022 First bug fix attempt \u2022 Straightforward trade-offs \u2022 Tactical "how" vs strategic "should" \u2022 Time-sensitive good-enough decisions \u2022 Quick research/testing can answer
|
|
73
|
+
- **Rule of thumb:** Need senior architect review? \u2192 @oracle. Need code review or simplification? \u2192 @oracle. Just do it and PR? \u2192 yourself.
|
|
74
|
+
|
|
75
|
+
@designer
|
|
76
|
+
- Role: UI/UX specialist for intentional, polished experiences
|
|
77
|
+
- Stats: 10x better UI/UX than orchestrator
|
|
78
|
+
- Capabilities: Visual relevant edits, interactions, responsive layouts, design systems with aesthetic intent, deep UI/UX knowledge; can edits files directly
|
|
79
|
+
- **Delegate when:** User-facing interfaces needing polish \u2022 Responsive layouts \u2022 UX-critical components (forms, nav, dashboards) \u2022 Visual consistency systems \u2022 Animations/micro-interactions \u2022 Landing/marketing pages \u2022 Refining functional\u2192delightful \u2022 Reviewing existing UI/UX quality
|
|
80
|
+
- **Don't delegate when:** Backend/logic with no visual \u2022 Quick prototypes where design doesn't matter yet
|
|
81
|
+
- **Rule of thumb:** Users see it and polish matters? \u2192 @designer. Headless/functional? \u2192 yourself.
|
|
82
|
+
|
|
83
|
+
@fixer
|
|
84
|
+
- Role: Fast execution specialist for well-defined tasks, which empowers orchestrator with parallel, speedy executions
|
|
85
|
+
- Stats: 2x faster code edits, 1/2 cost of orchestrator, 0.8x quality of orchestrator
|
|
86
|
+
- Tools/Constraints: Execution-focused\u2014no research, no architectural decisions
|
|
87
|
+
- **Delegate when:** For implementation work, think and triage first. If the change is non-trivial or multi-file, hand bounded execution to @fixer \u2022 Writing or updating tests \u2022 Tasks that touch test files, fixtures, mocks, or test helpers
|
|
88
|
+
- **Don't delegate when:** Needs discovery/research/decisions \u2022 Single small change (<20 lines, one file) \u2022 Unclear requirements needing iteration \u2022 Explaining to fixer > doing \u2022 Tight integration with your current work \u2022 Sequential dependencies
|
|
89
|
+
- **Rule of thumb:** Explaining > doing? \u2192 yourself. Test file modifications and bounded implementation work usually go to @fixer. Orchestrator paths selection is vastly improved by Fixer. eg it can reduce overall speed if Orchestrator splits what's usually a single task into multiple subtasks and parallelize it with fixer.
|
|
90
|
+
|
|
91
|
+
@council
|
|
92
|
+
- Role: Multi-LLM consensus engine for high-confidence answers
|
|
93
|
+
- Stats: 3x slower than orchestrator, 3x or more cost of orchestrator
|
|
94
|
+
- Capabilities: Runs multiple models in parallel, synthesizes their responses via a council master
|
|
95
|
+
- **Delegate when:** Critical decisions needing diverse model perspectives \u2022 High-stakes architectural choices where consensus reduces risk \u2022 Ambiguous problems where multi-model disagreement is informative \u2022 Security-sensitive design reviews
|
|
96
|
+
- **Don't delegate when:** Straightforward tasks you're confident about \u2022 Speed matters more than confidence \u2022 Single-model answer is sufficient \u2022 Routine implementation work
|
|
97
|
+
- **Result handling:** Present the council's synthesized response verbatim. Do not re-summarize \u2014 the council master has already produced the final answer.
|
|
98
|
+
- **Rule of thumb:** Need second/third opinions from different models? \u2192 @council. One good answer enough? \u2192 yourself.
|
|
99
|
+
|
|
100
|
+
</Agents>
|
|
101
|
+
|
|
102
|
+
<Workflow>
|
|
103
|
+
|
|
104
|
+
## 1. Understand
|
|
105
|
+
Parse request: explicit requirements + implicit needs.
|
|
106
|
+
|
|
107
|
+
## 2. Path Selection
|
|
108
|
+
Evaluate approach by: quality, speed, cost, reliability.
|
|
109
|
+
Choose the path that optimizes all four.
|
|
110
|
+
|
|
111
|
+
## 3. Delegation Check
|
|
112
|
+
**STOP. Review specialists before acting.**
|
|
113
|
+
|
|
114
|
+
!!! Review available agents and delegation rules. Decide whether to delegate or do it yourself. !!!
|
|
115
|
+
|
|
116
|
+
**Delegation efficiency:**
|
|
117
|
+
- Reference paths/lines, don't paste files (\`src/app.ts:42\` not full contents)
|
|
118
|
+
- Provide context summaries, let specialists read what they need
|
|
119
|
+
- Brief user on delegation goal before each call
|
|
120
|
+
- Skip delegation if overhead \u2265 doing it yourself
|
|
121
|
+
|
|
122
|
+
## 4. Split and Parallelize
|
|
123
|
+
Can tasks be split into subtasks and run in parallel?
|
|
124
|
+
- Multiple @explorer searches across different domains?
|
|
125
|
+
- @explorer + @librarian research in parallel?
|
|
126
|
+
- Multiple @fixer instances for faster, scoped implementation?
|
|
127
|
+
|
|
128
|
+
Balance: respect dependencies, avoid parallelizing what must be sequential.
|
|
129
|
+
|
|
130
|
+
## 5. Execute
|
|
131
|
+
1. Break complex tasks into todos
|
|
132
|
+
2. Fire parallel research/implementation
|
|
133
|
+
3. Delegate to specialists or do it yourself based on step 3
|
|
134
|
+
4. Integrate results
|
|
135
|
+
5. Adjust if needed
|
|
136
|
+
|
|
137
|
+
### Auto-Continue
|
|
138
|
+
When working through multi-step tasks, consider enabling auto-continue to avoid stopping between batches:
|
|
139
|
+
- **Enable when:** User requests autonomous/batch work, or you create 4+ todos in a session
|
|
140
|
+
- **Don't enable when:** User is in an interactive/conversational flow, or each step needs explicit review
|
|
141
|
+
- Use the \`auto_continue\` tool with \`enabled: true\` to activate. The system will automatically resume you when incomplete todos remain after you stop.
|
|
142
|
+
- The user can toggle this anytime via the \`/auto-continue\` command.
|
|
143
|
+
|
|
144
|
+
### Validation routing
|
|
145
|
+
- Validation is a workflow stage owned by the Orchestrator, not a separate specialist
|
|
146
|
+
- Route UI/UX validation and review to @designer
|
|
147
|
+
- Route code review, simplification, maintainability review, and YAGNI checks to @oracle
|
|
148
|
+
- Route test writing, test updates, and changes touching test files to @fixer
|
|
149
|
+
- If a request spans multiple lanes, delegate only the lanes that add clear value
|
|
150
|
+
|
|
151
|
+
## 6. Verify
|
|
152
|
+
- Run \`lsp_diagnostics\` for errors
|
|
153
|
+
- Use validation routing when applicable instead of doing all review work yourself
|
|
154
|
+
- If test files are involved, prefer @fixer for bounded test changes and @oracle only for test strategy or quality review
|
|
155
|
+
- Confirm specialists completed successfully
|
|
156
|
+
- Verify solution meets requirements
|
|
157
|
+
|
|
158
|
+
</Workflow>
|
|
159
|
+
|
|
160
|
+
<Communication>
|
|
161
|
+
|
|
162
|
+
## Clarity Over Assumptions
|
|
163
|
+
- If request is vague or has multiple valid interpretations, ask a targeted question before proceeding
|
|
164
|
+
- Don't guess at critical details (file paths, API choices, architectural decisions)
|
|
165
|
+
- Do make reasonable assumptions for minor details and state them briefly
|
|
166
|
+
|
|
167
|
+
## Concise Execution
|
|
168
|
+
- Answer directly, no preamble
|
|
169
|
+
- Don't summarize what you did unless asked
|
|
170
|
+
- Don't explain code unless asked
|
|
171
|
+
- One-word answers are fine when appropriate
|
|
172
|
+
- Brief delegation notices: "Checking docs via @librarian..." not "I'm going to delegate to @librarian because..."
|
|
173
|
+
|
|
174
|
+
## No Flattery
|
|
175
|
+
Never: "Great question!" "Excellent idea!" "Smart choice!" or any praise of user input.
|
|
176
|
+
|
|
177
|
+
## Honest Pushback
|
|
178
|
+
When user's approach seems problematic:
|
|
179
|
+
- State concern + alternative concisely
|
|
180
|
+
- Ask if they want to proceed anyway
|
|
181
|
+
- Don't lecture, don't blindly implement
|
|
182
|
+
|
|
183
|
+
## Example
|
|
184
|
+
**Bad:** "Great question! Let me think about the best approach here. I'm going to delegate to @librarian to check the latest Next.js documentation for the App Router, and then I'll implement the solution for you."
|
|
185
|
+
|
|
186
|
+
**Good:** "Checking Next.js App Router docs via @librarian..."
|
|
187
|
+
[proceeds with implementation]
|
|
188
|
+
|
|
189
|
+
</Communication>
|
|
190
|
+
`;
|
|
191
|
+
|
|
2
192
|
// src/cli/paths.ts
|
|
3
193
|
import { homedir } from "os";
|
|
4
194
|
import { dirname, join } from "path";
|
|
@@ -341,6 +531,11 @@ var McpNameSchema = z2.enum(["websearch", "context7", "grep_app"]);
|
|
|
341
531
|
var BackgroundTaskConfigSchema = z2.object({
|
|
342
532
|
maxConcurrentStarts: z2.number().min(1).max(50).default(10)
|
|
343
533
|
});
|
|
534
|
+
var InterviewConfigSchema = z2.object({
|
|
535
|
+
maxQuestions: z2.number().int().min(1).max(10).default(2),
|
|
536
|
+
outputFolder: z2.string().min(1).default("interview"),
|
|
537
|
+
autoOpenBrowser: z2.boolean().default(true)
|
|
538
|
+
});
|
|
344
539
|
var TodoContinuationConfigSchema = z2.object({
|
|
345
540
|
maxContinuations: z2.number().int().min(1).max(50).default(5).describe("Maximum consecutive auto-continuations before stopping to ask user"),
|
|
346
541
|
cooldownMs: z2.number().int().min(0).max(30000).default(3000).describe("Delay in ms before auto-continuing (gives user time to abort)"),
|
|
@@ -367,6 +562,7 @@ var PluginConfigSchema = z2.object({
|
|
|
367
562
|
tmux: TmuxConfigSchema.optional(),
|
|
368
563
|
websearch: WebsearchConfigSchema.optional(),
|
|
369
564
|
background: BackgroundTaskConfigSchema.optional(),
|
|
565
|
+
interview: InterviewConfigSchema.optional(),
|
|
370
566
|
todoContinuation: TodoContinuationConfigSchema.optional(),
|
|
371
567
|
fallback: FailoverConfigSchema.optional(),
|
|
372
568
|
council: CouncilConfigSchema.optional()
|
|
@@ -442,6 +638,7 @@ function loadPluginConfig(directory) {
|
|
|
442
638
|
agents: deepMerge(config.agents, projectConfig.agents),
|
|
443
639
|
tmux: deepMerge(config.tmux, projectConfig.tmux),
|
|
444
640
|
multiplexer: deepMerge(config.multiplexer, projectConfig.multiplexer),
|
|
641
|
+
interview: deepMerge(config.interview, projectConfig.interview),
|
|
445
642
|
fallback: deepMerge(config.fallback, projectConfig.fallback),
|
|
446
643
|
council: deepMerge(config.council, projectConfig.council)
|
|
447
644
|
};
|
|
@@ -568,221 +765,48 @@ async function extractSessionResult(client, sessionId, options) {
|
|
|
568
765
|
return { text, empty: text.length === 0 };
|
|
569
766
|
}
|
|
570
767
|
|
|
571
|
-
// src/agents/
|
|
572
|
-
|
|
573
|
-
if (customPrompt)
|
|
574
|
-
return customPrompt;
|
|
575
|
-
if (customAppendPrompt)
|
|
576
|
-
return `${base}
|
|
577
|
-
|
|
578
|
-
${customAppendPrompt}`;
|
|
579
|
-
return base;
|
|
580
|
-
}
|
|
581
|
-
var ORCHESTRATOR_PROMPT = `<Role>
|
|
582
|
-
You are an AI coding orchestrator that optimizes for quality, speed, cost, and reliability by delegating to specialists when it provides net efficiency gains.
|
|
583
|
-
</Role>
|
|
768
|
+
// src/agents/council.ts
|
|
769
|
+
var COUNCIL_AGENT_PROMPT = `You are the Council agent \u2014 a multi-LLM orchestration system that runs consensus across multiple models.
|
|
584
770
|
|
|
585
|
-
|
|
771
|
+
**Tool**: You have access to the \`council_session\` tool.
|
|
586
772
|
|
|
587
|
-
|
|
588
|
-
-
|
|
589
|
-
-
|
|
590
|
-
-
|
|
591
|
-
- **Delegate when:** Need to discover what exists before planning \u2022 Parallel searches speed discovery \u2022 Need summarized map vs full contents \u2022 Broad/uncertain scope
|
|
592
|
-
- **Don't delegate when:** Know the path and need actual content \u2022 Need full file anyway \u2022 Single specific lookup \u2022 About to edit the file
|
|
773
|
+
**When to use**:
|
|
774
|
+
- When invoked by a user with a request
|
|
775
|
+
- When you want multiple expert opinions on a complex problem
|
|
776
|
+
- When higher confidence is needed through model consensus
|
|
593
777
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
- **Don't delegate when:** Standard usage you're confident about (\`Array.map()\`, \`fetch()\`) \u2022 Simple stable APIs \u2022 General programming knowledge \u2022 Info already in conversation \u2022 Built-in language features
|
|
600
|
-
- **Rule of thumb:** "How does this library work?" \u2192 @librarian. "How does programming work?" \u2192 yourself.
|
|
778
|
+
**Usage**:
|
|
779
|
+
1. Call the \`council_session\` tool with the user's prompt
|
|
780
|
+
2. Optionally specify a preset (default: "default")
|
|
781
|
+
3. Receive the synthesized response from the council master
|
|
782
|
+
4. Present the result to the user
|
|
601
783
|
|
|
602
|
-
|
|
603
|
-
-
|
|
604
|
-
-
|
|
605
|
-
-
|
|
606
|
-
-
|
|
607
|
-
|
|
608
|
-
|
|
784
|
+
**Behavior**:
|
|
785
|
+
- Delegate requests directly to council_session
|
|
786
|
+
- Don't pre-analyze or filter the prompt
|
|
787
|
+
- Present the synthesized result verbatim \u2014 do not re-summarize or condense
|
|
788
|
+
- Briefly explain the consensus if requested`;
|
|
789
|
+
function createCouncilAgent(model, customPrompt, customAppendPrompt) {
|
|
790
|
+
const prompt = resolvePrompt(COUNCIL_AGENT_PROMPT, customPrompt, customAppendPrompt);
|
|
791
|
+
const definition = {
|
|
792
|
+
name: "council",
|
|
793
|
+
description: "Multi-LLM council agent that synthesizes responses from multiple models for higher-quality outputs",
|
|
794
|
+
config: {
|
|
795
|
+
temperature: 0.1,
|
|
796
|
+
prompt
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
if (model) {
|
|
800
|
+
definition.config.model = model;
|
|
801
|
+
}
|
|
802
|
+
return definition;
|
|
803
|
+
}
|
|
804
|
+
function formatCouncillorPrompt(userPrompt, councillorPrompt) {
|
|
805
|
+
if (!councillorPrompt)
|
|
806
|
+
return userPrompt;
|
|
807
|
+
return `${councillorPrompt}
|
|
609
808
|
|
|
610
|
-
|
|
611
|
-
- Role: UI/UX specialist for intentional, polished experiences
|
|
612
|
-
- Stats: 10x better UI/UX than orchestrator
|
|
613
|
-
- Capabilities: Visual direction, interactions, responsive layouts, design systems with aesthetic intent, UI/UX review
|
|
614
|
-
- **Delegate when:** User-facing interfaces needing polish \u2022 Responsive layouts \u2022 UX-critical components (forms, nav, dashboards) \u2022 Visual consistency systems \u2022 Animations/micro-interactions \u2022 Landing/marketing pages \u2022 Refining functional\u2192delightful \u2022 Reviewing existing UI/UX quality
|
|
615
|
-
- **Don't delegate when:** Backend/logic with no visual \u2022 Quick prototypes where design doesn't matter yet
|
|
616
|
-
- **Rule of thumb:** Users see it and polish matters? \u2192 @designer. Headless/functional? \u2192 yourself.
|
|
617
|
-
|
|
618
|
-
@fixer
|
|
619
|
-
- Role: Fast execution specialist for well-defined tasks, which empowers orchestrator with parallel, speedy executions
|
|
620
|
-
- Stats: 2x faster code edits, 1/2 cost of orchestrator, 0.8x quality of orchestrator
|
|
621
|
-
- Tools/Constraints: Execution-focused\u2014no research, no architectural decisions
|
|
622
|
-
- **Delegate when:** For implementation work, think and triage first. If the change is non-trivial or multi-file, hand bounded execution to @fixer \u2022 Writing or updating tests \u2022 Tasks that touch test files, fixtures, mocks, or test helpers
|
|
623
|
-
- **Don't delegate when:** Needs discovery/research/decisions \u2022 Single small change (<20 lines, one file) \u2022 Unclear requirements needing iteration \u2022 Explaining to fixer > doing \u2022 Tight integration with your current work \u2022 Sequential dependencies
|
|
624
|
-
- **Rule of thumb:** Explaining > doing? \u2192 yourself. Test file modifications and bounded implementation work usually go to @fixer. Orchestrator paths selection is vastly improved by Fixer. eg it can reduce overall speed if Orchestrator splits what's usually a single task into multiple subtasks and parallelize it with fixer.
|
|
625
|
-
|
|
626
|
-
@council
|
|
627
|
-
- Role: Multi-LLM consensus engine for high-confidence answers
|
|
628
|
-
- Stats: 3x slower than orchestrator, 3x or more cost of orchestrator
|
|
629
|
-
- Capabilities: Runs multiple models in parallel, synthesizes their responses via a council master
|
|
630
|
-
- **Delegate when:** Critical decisions needing diverse model perspectives \u2022 High-stakes architectural choices where consensus reduces risk \u2022 Ambiguous problems where multi-model disagreement is informative \u2022 Security-sensitive design reviews
|
|
631
|
-
- **Don't delegate when:** Straightforward tasks you're confident about \u2022 Speed matters more than confidence \u2022 Single-model answer is sufficient \u2022 Routine implementation work
|
|
632
|
-
- **Result handling:** Present the council's synthesized response verbatim. Do not re-summarize \u2014 the council master has already produced the final answer.
|
|
633
|
-
- **Rule of thumb:** Need second/third opinions from different models? \u2192 @council. One good answer enough? \u2192 yourself.
|
|
634
|
-
|
|
635
|
-
</Agents>
|
|
636
|
-
|
|
637
|
-
<Workflow>
|
|
638
|
-
|
|
639
|
-
## 1. Understand
|
|
640
|
-
Parse request: explicit requirements + implicit needs.
|
|
641
|
-
|
|
642
|
-
## 2. Path Selection
|
|
643
|
-
Evaluate approach by: quality, speed, cost, reliability.
|
|
644
|
-
Choose the path that optimizes all four.
|
|
645
|
-
|
|
646
|
-
## 3. Delegation Check
|
|
647
|
-
**STOP. Review specialists before acting.**
|
|
648
|
-
|
|
649
|
-
!!! Review available agents and delegation rules. Decide whether to delegate or do it yourself. !!!
|
|
650
|
-
|
|
651
|
-
**Delegation efficiency:**
|
|
652
|
-
- Reference paths/lines, don't paste files (\`src/app.ts:42\` not full contents)
|
|
653
|
-
- Provide context summaries, let specialists read what they need
|
|
654
|
-
- Brief user on delegation goal before each call
|
|
655
|
-
- Skip delegation if overhead \u2265 doing it yourself
|
|
656
|
-
|
|
657
|
-
## 4. Split and Parallelize
|
|
658
|
-
Can tasks be split into subtasks and run in parallel?
|
|
659
|
-
- Multiple @explorer searches across different domains?
|
|
660
|
-
- @explorer + @librarian research in parallel?
|
|
661
|
-
- Multiple @fixer instances for faster, scoped implementation?
|
|
662
|
-
|
|
663
|
-
Balance: respect dependencies, avoid parallelizing what must be sequential.
|
|
664
|
-
|
|
665
|
-
## 5. Execute
|
|
666
|
-
1. Break complex tasks into todos
|
|
667
|
-
2. Fire parallel research/implementation
|
|
668
|
-
3. Delegate to specialists or do it yourself based on step 3
|
|
669
|
-
4. Integrate results
|
|
670
|
-
5. Adjust if needed
|
|
671
|
-
|
|
672
|
-
### Auto-Continue
|
|
673
|
-
When working through multi-step tasks, consider enabling auto-continue to avoid stopping between batches:
|
|
674
|
-
- **Enable when:** User requests autonomous/batch work, or you create 4+ todos in a session
|
|
675
|
-
- **Don't enable when:** User is in an interactive/conversational flow, or each step needs explicit review
|
|
676
|
-
- Use the \`auto_continue\` tool with \`enabled: true\` to activate. The system will automatically resume you when incomplete todos remain after you stop.
|
|
677
|
-
- The user can toggle this anytime via the \`/auto-continue\` command.
|
|
678
|
-
|
|
679
|
-
### Validation routing
|
|
680
|
-
- Validation is a workflow stage owned by the Orchestrator, not a separate specialist
|
|
681
|
-
- Route UI/UX validation and review to @designer
|
|
682
|
-
- Route code review, simplification, maintainability review, and YAGNI checks to @oracle
|
|
683
|
-
- Route test writing, test updates, and changes touching test files to @fixer
|
|
684
|
-
- If a request spans multiple lanes, delegate only the lanes that add clear value
|
|
685
|
-
|
|
686
|
-
## 6. Verify
|
|
687
|
-
- Run \`lsp_diagnostics\` for errors
|
|
688
|
-
- Use validation routing when applicable instead of doing all review work yourself
|
|
689
|
-
- If test files are involved, prefer @fixer for bounded test changes and @oracle only for test strategy or quality review
|
|
690
|
-
- Confirm specialists completed successfully
|
|
691
|
-
- Verify solution meets requirements
|
|
692
|
-
|
|
693
|
-
</Workflow>
|
|
694
|
-
|
|
695
|
-
<Communication>
|
|
696
|
-
|
|
697
|
-
## Clarity Over Assumptions
|
|
698
|
-
- If request is vague or has multiple valid interpretations, ask a targeted question before proceeding
|
|
699
|
-
- Don't guess at critical details (file paths, API choices, architectural decisions)
|
|
700
|
-
- Do make reasonable assumptions for minor details and state them briefly
|
|
701
|
-
|
|
702
|
-
## Concise Execution
|
|
703
|
-
- Answer directly, no preamble
|
|
704
|
-
- Don't summarize what you did unless asked
|
|
705
|
-
- Don't explain code unless asked
|
|
706
|
-
- One-word answers are fine when appropriate
|
|
707
|
-
- Brief delegation notices: "Checking docs via @librarian..." not "I'm going to delegate to @librarian because..."
|
|
708
|
-
|
|
709
|
-
## No Flattery
|
|
710
|
-
Never: "Great question!" "Excellent idea!" "Smart choice!" or any praise of user input.
|
|
711
|
-
|
|
712
|
-
## Honest Pushback
|
|
713
|
-
When user's approach seems problematic:
|
|
714
|
-
- State concern + alternative concisely
|
|
715
|
-
- Ask if they want to proceed anyway
|
|
716
|
-
- Don't lecture, don't blindly implement
|
|
717
|
-
|
|
718
|
-
## Example
|
|
719
|
-
**Bad:** "Great question! Let me think about the best approach here. I'm going to delegate to @librarian to check the latest Next.js documentation for the App Router, and then I'll implement the solution for you."
|
|
720
|
-
|
|
721
|
-
**Good:** "Checking Next.js App Router docs via @librarian..."
|
|
722
|
-
[proceeds with implementation]
|
|
723
|
-
|
|
724
|
-
</Communication>
|
|
725
|
-
`;
|
|
726
|
-
function createOrchestratorAgent(model, customPrompt, customAppendPrompt) {
|
|
727
|
-
const prompt = resolvePrompt(ORCHESTRATOR_PROMPT, customPrompt, customAppendPrompt);
|
|
728
|
-
const definition = {
|
|
729
|
-
name: "orchestrator",
|
|
730
|
-
description: "AI coding orchestrator that delegates tasks to specialist agents for optimal quality, speed, and cost",
|
|
731
|
-
config: {
|
|
732
|
-
temperature: 0.1,
|
|
733
|
-
prompt
|
|
734
|
-
}
|
|
735
|
-
};
|
|
736
|
-
if (Array.isArray(model)) {
|
|
737
|
-
definition._modelArray = model.map((m) => typeof m === "string" ? { id: m } : m);
|
|
738
|
-
} else if (typeof model === "string" && model) {
|
|
739
|
-
definition.config.model = model;
|
|
740
|
-
}
|
|
741
|
-
return definition;
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
// src/agents/council.ts
|
|
745
|
-
var COUNCIL_AGENT_PROMPT = `You are the Council agent \u2014 a multi-LLM orchestration system that runs consensus across multiple models.
|
|
746
|
-
|
|
747
|
-
**Tool**: You have access to the \`council_session\` tool.
|
|
748
|
-
|
|
749
|
-
**When to use**:
|
|
750
|
-
- When invoked by a user with a request
|
|
751
|
-
- When you want multiple expert opinions on a complex problem
|
|
752
|
-
- When higher confidence is needed through model consensus
|
|
753
|
-
|
|
754
|
-
**Usage**:
|
|
755
|
-
1. Call the \`council_session\` tool with the user's prompt
|
|
756
|
-
2. Optionally specify a preset (default: "default")
|
|
757
|
-
3. Receive the synthesized response from the council master
|
|
758
|
-
4. Present the result to the user
|
|
759
|
-
|
|
760
|
-
**Behavior**:
|
|
761
|
-
- Delegate requests directly to council_session
|
|
762
|
-
- Don't pre-analyze or filter the prompt
|
|
763
|
-
- Present the synthesized result verbatim \u2014 do not re-summarize or condense
|
|
764
|
-
- Briefly explain the consensus if requested`;
|
|
765
|
-
function createCouncilAgent(model, customPrompt, customAppendPrompt) {
|
|
766
|
-
const prompt = resolvePrompt(COUNCIL_AGENT_PROMPT, customPrompt, customAppendPrompt);
|
|
767
|
-
const definition = {
|
|
768
|
-
name: "council",
|
|
769
|
-
description: "Multi-LLM council agent that synthesizes responses from multiple models for higher-quality outputs",
|
|
770
|
-
config: {
|
|
771
|
-
temperature: 0.1,
|
|
772
|
-
prompt
|
|
773
|
-
}
|
|
774
|
-
};
|
|
775
|
-
if (model) {
|
|
776
|
-
definition.config.model = model;
|
|
777
|
-
}
|
|
778
|
-
return definition;
|
|
779
|
-
}
|
|
780
|
-
function formatCouncillorPrompt(userPrompt, councillorPrompt) {
|
|
781
|
-
if (!councillorPrompt)
|
|
782
|
-
return userPrompt;
|
|
783
|
-
return `${councillorPrompt}
|
|
784
|
-
|
|
785
|
-
---
|
|
809
|
+
---
|
|
786
810
|
|
|
787
811
|
${userPrompt}`;
|
|
788
812
|
}
|
|
@@ -3259,7 +3283,7 @@ Version is pinned. Update your plugin config to apply.`, "info", 8000);
|
|
|
3259
3283
|
return;
|
|
3260
3284
|
}
|
|
3261
3285
|
invalidatePackage(PACKAGE_NAME);
|
|
3262
|
-
const installSuccess = await runBunInstallSafe(
|
|
3286
|
+
const installSuccess = await runBunInstallSafe();
|
|
3263
3287
|
if (installSuccess) {
|
|
3264
3288
|
showToast(ctx, "OMO-Slim Updated!", `v${currentVersion} \u2192 v${latestVersion}
|
|
3265
3289
|
Restart OpenCode to apply.`, "success", 8000);
|
|
@@ -3269,10 +3293,14 @@ Restart OpenCode to apply.`, "success", 8000);
|
|
|
3269
3293
|
log("[auto-update-checker] bun install failed; update not installed");
|
|
3270
3294
|
}
|
|
3271
3295
|
}
|
|
3272
|
-
|
|
3296
|
+
function getAutoUpdateInstallDir() {
|
|
3297
|
+
return CACHE_DIR;
|
|
3298
|
+
}
|
|
3299
|
+
async function runBunInstallSafe() {
|
|
3273
3300
|
try {
|
|
3301
|
+
const installDir = getAutoUpdateInstallDir();
|
|
3274
3302
|
const proc = Bun.spawn(["bun", "install"], {
|
|
3275
|
-
cwd:
|
|
3303
|
+
cwd: installDir,
|
|
3276
3304
|
stdout: "pipe",
|
|
3277
3305
|
stderr: "pipe"
|
|
3278
3306
|
});
|
|
@@ -3996,196 +4024,1657 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
3996
4024
|
const lastText = lastAssistantMessage.parts.map((p) => p.text ?? "").join(" ");
|
|
3997
4025
|
lastAssistantIsQuestion = isQuestion(lastText);
|
|
3998
4026
|
}
|
|
3999
|
-
log(`[${HOOK_NAME}] Fetched messages`, {
|
|
4000
|
-
sessionID,
|
|
4001
|
-
lastAssistantIsQuestion
|
|
4002
|
-
});
|
|
4003
|
-
} catch (error) {
|
|
4004
|
-
log(`[${HOOK_NAME}] Warning: failed to fetch messages`, {
|
|
4005
|
-
sessionID,
|
|
4006
|
-
error: error instanceof Error ? error.message : String(error)
|
|
4007
|
-
});
|
|
4008
|
-
return;
|
|
4027
|
+
log(`[${HOOK_NAME}] Fetched messages`, {
|
|
4028
|
+
sessionID,
|
|
4029
|
+
lastAssistantIsQuestion
|
|
4030
|
+
});
|
|
4031
|
+
} catch (error) {
|
|
4032
|
+
log(`[${HOOK_NAME}] Warning: failed to fetch messages`, {
|
|
4033
|
+
sessionID,
|
|
4034
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4035
|
+
});
|
|
4036
|
+
return;
|
|
4037
|
+
}
|
|
4038
|
+
if (lastAssistantIsQuestion) {
|
|
4039
|
+
log(`[${HOOK_NAME}] Skipped: last message is question`, {
|
|
4040
|
+
sessionID
|
|
4041
|
+
});
|
|
4042
|
+
return;
|
|
4043
|
+
}
|
|
4044
|
+
if (state.consecutiveContinuations >= maxContinuations) {
|
|
4045
|
+
log(`[${HOOK_NAME}] Skipped: max continuations reached`, {
|
|
4046
|
+
sessionID,
|
|
4047
|
+
consecutive: state.consecutiveContinuations,
|
|
4048
|
+
max: maxContinuations
|
|
4049
|
+
});
|
|
4050
|
+
return;
|
|
4051
|
+
}
|
|
4052
|
+
const now = Date.now();
|
|
4053
|
+
if (now < state.suppressUntil) {
|
|
4054
|
+
log(`[${HOOK_NAME}] Skipped: in suppress window`, {
|
|
4055
|
+
sessionID,
|
|
4056
|
+
suppressUntil: state.suppressUntil
|
|
4057
|
+
});
|
|
4058
|
+
return;
|
|
4059
|
+
}
|
|
4060
|
+
if (state.pendingTimer !== null || state.isAutoInjecting) {
|
|
4061
|
+
log(`[${HOOK_NAME}] Skipped: timer pending or injection in flight`, {
|
|
4062
|
+
sessionID
|
|
4063
|
+
});
|
|
4064
|
+
return;
|
|
4065
|
+
}
|
|
4066
|
+
log(`[${HOOK_NAME}] Scheduling continuation`, {
|
|
4067
|
+
sessionID,
|
|
4068
|
+
delayMs: cooldownMs
|
|
4069
|
+
});
|
|
4070
|
+
ctx.client.session.prompt({
|
|
4071
|
+
path: { id: sessionID },
|
|
4072
|
+
body: {
|
|
4073
|
+
noReply: true,
|
|
4074
|
+
parts: [
|
|
4075
|
+
{
|
|
4076
|
+
type: "text",
|
|
4077
|
+
text: [
|
|
4078
|
+
`\u2394 Auto-continue: ${incompleteCount} incomplete todos remaining \u2014 resuming in ${cooldownMs / 1000}s \u2014 Esc\xD72 to cancel`,
|
|
4079
|
+
"",
|
|
4080
|
+
"[system status: continue without acknowledging this notification]"
|
|
4081
|
+
].join(`
|
|
4082
|
+
`)
|
|
4083
|
+
}
|
|
4084
|
+
]
|
|
4085
|
+
}
|
|
4086
|
+
}).catch(() => {});
|
|
4087
|
+
state.pendingTimer = setTimeout(async () => {
|
|
4088
|
+
state.pendingTimer = null;
|
|
4089
|
+
if (!state.enabled) {
|
|
4090
|
+
log(`[${HOOK_NAME}] Cancelled: disabled during cooldown`, {
|
|
4091
|
+
sessionID
|
|
4092
|
+
});
|
|
4093
|
+
return;
|
|
4094
|
+
}
|
|
4095
|
+
state.isAutoInjecting = true;
|
|
4096
|
+
try {
|
|
4097
|
+
await ctx.client.session.prompt({
|
|
4098
|
+
path: { id: sessionID },
|
|
4099
|
+
body: {
|
|
4100
|
+
parts: [createInternalAgentTextPart(CONTINUATION_PROMPT)]
|
|
4101
|
+
}
|
|
4102
|
+
});
|
|
4103
|
+
state.consecutiveContinuations++;
|
|
4104
|
+
log(`[${HOOK_NAME}] Continuation injected`, {
|
|
4105
|
+
sessionID,
|
|
4106
|
+
consecutive: state.consecutiveContinuations
|
|
4107
|
+
});
|
|
4108
|
+
} catch (error) {
|
|
4109
|
+
log(`[${HOOK_NAME}] Error: failed to inject continuation`, {
|
|
4110
|
+
sessionID,
|
|
4111
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4112
|
+
});
|
|
4113
|
+
} finally {
|
|
4114
|
+
state.isAutoInjecting = false;
|
|
4115
|
+
}
|
|
4116
|
+
}, cooldownMs);
|
|
4117
|
+
} else if (event.type === "session.status") {
|
|
4118
|
+
const status = properties.status;
|
|
4119
|
+
const sessionID = properties.sessionID;
|
|
4120
|
+
if (status?.type === "busy") {
|
|
4121
|
+
const isOrchestrator = sessionID === state.orchestratorSessionId;
|
|
4122
|
+
if (isOrchestrator) {
|
|
4123
|
+
cancelPendingTimer(state);
|
|
4124
|
+
}
|
|
4125
|
+
if (!state.isAutoInjecting && isOrchestrator && state.consecutiveContinuations > 0) {
|
|
4126
|
+
state.consecutiveContinuations = 0;
|
|
4127
|
+
log(`[${HOOK_NAME}] Reset consecutive count on user activity`, {
|
|
4128
|
+
sessionID
|
|
4129
|
+
});
|
|
4130
|
+
}
|
|
4131
|
+
}
|
|
4132
|
+
} else if (event.type === "session.error") {
|
|
4133
|
+
const error = properties.error;
|
|
4134
|
+
const sessionID = properties.sessionID;
|
|
4135
|
+
const errorName = error?.name;
|
|
4136
|
+
const isOrchestrator = sessionID === state.orchestratorSessionId;
|
|
4137
|
+
if (isOrchestrator && (errorName === "MessageAbortedError" || errorName === "AbortError")) {
|
|
4138
|
+
state.suppressUntil = Date.now() + SUPPRESS_AFTER_ABORT_MS;
|
|
4139
|
+
log(`[${HOOK_NAME}] Suppressed continuation after abort`, {
|
|
4140
|
+
sessionID,
|
|
4141
|
+
errorName
|
|
4142
|
+
});
|
|
4143
|
+
}
|
|
4144
|
+
if (isOrchestrator) {
|
|
4145
|
+
cancelPendingTimer(state);
|
|
4146
|
+
log(`[${HOOK_NAME}] Cancelled pending timer on error`, {
|
|
4147
|
+
sessionID
|
|
4148
|
+
});
|
|
4149
|
+
}
|
|
4150
|
+
} else if (event.type === "session.deleted") {
|
|
4151
|
+
const deletedSessionId = properties.info?.id ?? properties.sessionID;
|
|
4152
|
+
if (state.orchestratorSessionId === deletedSessionId) {
|
|
4153
|
+
cancelPendingTimer(state);
|
|
4154
|
+
log(`[${HOOK_NAME}] Cancelled pending timer on orchestrator delete`, {
|
|
4155
|
+
sessionID: deletedSessionId
|
|
4156
|
+
});
|
|
4157
|
+
resetState(state);
|
|
4158
|
+
state.orchestratorSessionId = null;
|
|
4159
|
+
log(`[${HOOK_NAME}] Reset orchestrator session on delete`, {
|
|
4160
|
+
sessionID: deletedSessionId
|
|
4161
|
+
});
|
|
4162
|
+
}
|
|
4163
|
+
}
|
|
4164
|
+
}
|
|
4165
|
+
async function handleCommandExecuteBefore(input, output) {
|
|
4166
|
+
if (input.command !== COMMAND_NAME) {
|
|
4167
|
+
return;
|
|
4168
|
+
}
|
|
4169
|
+
if (!state.orchestratorSessionId) {
|
|
4170
|
+
state.orchestratorSessionId = input.sessionID;
|
|
4171
|
+
}
|
|
4172
|
+
output.parts.length = 0;
|
|
4173
|
+
const arg = input.arguments.trim().toLowerCase();
|
|
4174
|
+
let newEnabled;
|
|
4175
|
+
if (arg === "on") {
|
|
4176
|
+
newEnabled = true;
|
|
4177
|
+
} else if (arg === "off") {
|
|
4178
|
+
newEnabled = false;
|
|
4179
|
+
} else {
|
|
4180
|
+
newEnabled = !state.enabled;
|
|
4181
|
+
}
|
|
4182
|
+
state.enabled = newEnabled;
|
|
4183
|
+
state.consecutiveContinuations = 0;
|
|
4184
|
+
if (!newEnabled) {
|
|
4185
|
+
cancelPendingTimer(state);
|
|
4186
|
+
output.parts.push(createInternalAgentTextPart("[Auto-continue: disabled by user command.]"));
|
|
4187
|
+
log(`[${HOOK_NAME}] Disabled via /${COMMAND_NAME} command`);
|
|
4188
|
+
return;
|
|
4189
|
+
}
|
|
4190
|
+
state.suppressUntil = 0;
|
|
4191
|
+
log(`[${HOOK_NAME}] Enabled via /${COMMAND_NAME} command`, {
|
|
4192
|
+
maxContinuations
|
|
4193
|
+
});
|
|
4194
|
+
let hasIncompleteTodos = false;
|
|
4195
|
+
try {
|
|
4196
|
+
const todosResult = await ctx.client.session.todo({
|
|
4197
|
+
path: { id: input.sessionID }
|
|
4198
|
+
});
|
|
4199
|
+
const todos = todosResult.data;
|
|
4200
|
+
hasIncompleteTodos = todos.some((t) => !TERMINAL_TODO_STATUSES.includes(t.status));
|
|
4201
|
+
} catch (error) {
|
|
4202
|
+
log(`[${HOOK_NAME}] Warning: failed to fetch todos in command hook`, {
|
|
4203
|
+
sessionID: input.sessionID,
|
|
4204
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4205
|
+
});
|
|
4206
|
+
}
|
|
4207
|
+
if (hasIncompleteTodos) {
|
|
4208
|
+
output.parts.push(createInternalAgentTextPart(`${CONTINUATION_PROMPT} [Auto-continue enabled: up to ${maxContinuations} continuations.]`));
|
|
4209
|
+
} else {
|
|
4210
|
+
output.parts.push(createInternalAgentTextPart(`[Auto-continue: enabled for up to ${maxContinuations} continuations. No incomplete todos right now.]`));
|
|
4211
|
+
}
|
|
4212
|
+
}
|
|
4213
|
+
return {
|
|
4214
|
+
tool: { auto_continue: autoContinue },
|
|
4215
|
+
handleEvent,
|
|
4216
|
+
handleCommandExecuteBefore
|
|
4217
|
+
};
|
|
4218
|
+
}
|
|
4219
|
+
// src/interview/server.ts
|
|
4220
|
+
import {
|
|
4221
|
+
createServer
|
|
4222
|
+
} from "http";
|
|
4223
|
+
import { URL as URL2 } from "url";
|
|
4224
|
+
|
|
4225
|
+
// src/interview/ui.ts
|
|
4226
|
+
function renderInterviewPage(interviewId) {
|
|
4227
|
+
const safeTitle = interviewId.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
4228
|
+
return `<!doctype html>
|
|
4229
|
+
<html lang="en">
|
|
4230
|
+
<head>
|
|
4231
|
+
<meta charset="utf-8" />
|
|
4232
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
4233
|
+
<title>Interview ${safeTitle}</title>
|
|
4234
|
+
<style>
|
|
4235
|
+
:root { color-scheme: dark; }
|
|
4236
|
+
body {
|
|
4237
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
|
4238
|
+
margin: 0;
|
|
4239
|
+
background: #000000;
|
|
4240
|
+
color: #ffffff;
|
|
4241
|
+
line-height: 1.6;
|
|
4242
|
+
-webkit-font-smoothing: antialiased;
|
|
4243
|
+
font-size: 16px;
|
|
4244
|
+
}
|
|
4245
|
+
.wrap { max-width: 680px; margin: 0 auto; padding: 56px 24px; }
|
|
4246
|
+
.brand-header {
|
|
4247
|
+
display: flex;
|
|
4248
|
+
flex-direction: column;
|
|
4249
|
+
align-items: center;
|
|
4250
|
+
gap: 16px;
|
|
4251
|
+
margin-bottom: 32px;
|
|
4252
|
+
text-align: center;
|
|
4253
|
+
}
|
|
4254
|
+
.brand-mark {
|
|
4255
|
+
width: 144px;
|
|
4256
|
+
height: 144px;
|
|
4257
|
+
object-fit: contain;
|
|
4258
|
+
filter: drop-shadow(0 10px 30px rgba(255,255,255,0.1));
|
|
4259
|
+
}
|
|
4260
|
+
h1 { font-size: 32px; font-weight: 600; letter-spacing: -0.02em; margin-bottom: 12px; line-height: 1.2; }
|
|
4261
|
+
h2 { font-size: 18px; font-weight: 500; letter-spacing: 0.05em; text-transform: uppercase; color: rgba(255,255,255,0.4); margin-bottom: 24px; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 12px; }
|
|
4262
|
+
h3 { font-size: 18px; font-weight: 500; margin-bottom: 16px; line-height: 1.4; }
|
|
4263
|
+
p { margin-top: 0; }
|
|
4264
|
+
.muted { color: rgba(255,255,255,0.5); font-size: 16px; }
|
|
4265
|
+
.meta { display: flex; align-items: center; justify-content: space-between; font-size: 13px; color: rgba(255,255,255,0.4); margin-bottom: 16px; letter-spacing: 0.05em; text-transform: uppercase; }
|
|
4266
|
+
|
|
4267
|
+
.file-path-container {
|
|
4268
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
4269
|
+
font-size: 13px;
|
|
4270
|
+
color: rgba(255,255,255,0.6);
|
|
4271
|
+
background: rgba(255,255,255,0.05);
|
|
4272
|
+
padding: 8px 12px;
|
|
4273
|
+
border-radius: 6px;
|
|
4274
|
+
margin-bottom: 36px;
|
|
4275
|
+
display: flex;
|
|
4276
|
+
align-items: center;
|
|
4277
|
+
gap: 8px;
|
|
4278
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
4279
|
+
}
|
|
4280
|
+
.file-path-icon {
|
|
4281
|
+
opacity: 0.5;
|
|
4282
|
+
}
|
|
4283
|
+
|
|
4284
|
+
.question {
|
|
4285
|
+
background: rgba(255,255,255,0.02);
|
|
4286
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
4287
|
+
border-left: 1px solid rgba(255,255,255,0.1);
|
|
4288
|
+
border-radius: 6px;
|
|
4289
|
+
padding: 28px;
|
|
4290
|
+
margin-bottom: 32px;
|
|
4291
|
+
transition: all 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
4292
|
+
}
|
|
4293
|
+
.question:focus-within {
|
|
4294
|
+
border-color: rgba(255,255,255,0.3);
|
|
4295
|
+
}
|
|
4296
|
+
|
|
4297
|
+
/* Make active question much clearer */
|
|
4298
|
+
.question.active-question {
|
|
4299
|
+
background: rgba(255,255,255,0.04);
|
|
4300
|
+
border-color: rgba(255,255,255,0.4);
|
|
4301
|
+
border-left: 4px solid #ffffff;
|
|
4302
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.1);
|
|
4303
|
+
transform: translateX(4px);
|
|
4304
|
+
}
|
|
4305
|
+
|
|
4306
|
+
.options { display: flex; flex-direction: column; gap: 10px; margin-bottom: 20px; }
|
|
4307
|
+
|
|
4308
|
+
.option {
|
|
4309
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
4310
|
+
background: transparent;
|
|
4311
|
+
color: inherit;
|
|
4312
|
+
border-radius: 6px;
|
|
4313
|
+
padding: 14px 18px;
|
|
4314
|
+
cursor: pointer;
|
|
4315
|
+
text-align: left;
|
|
4316
|
+
font-size: 16px;
|
|
4317
|
+
transition: all 0.2s ease;
|
|
4318
|
+
display: flex;
|
|
4319
|
+
align-items: center;
|
|
4320
|
+
}
|
|
4321
|
+
.option:hover {
|
|
4322
|
+
background: rgba(255,255,255,0.06);
|
|
4323
|
+
border-color: rgba(255,255,255,0.3);
|
|
4324
|
+
}
|
|
4325
|
+
.option.selected {
|
|
4326
|
+
background: #ffffff;
|
|
4327
|
+
color: #000000;
|
|
4328
|
+
border-color: #ffffff;
|
|
4329
|
+
font-weight: 500;
|
|
4330
|
+
}
|
|
4331
|
+
|
|
4332
|
+
.shortcut {
|
|
4333
|
+
display: inline-flex;
|
|
4334
|
+
align-items: center;
|
|
4335
|
+
justify-content: center;
|
|
4336
|
+
background: rgba(255,255,255,0.1);
|
|
4337
|
+
color: rgba(255,255,255,0.8);
|
|
4338
|
+
border-radius: 4px;
|
|
4339
|
+
min-width: 20px;
|
|
4340
|
+
height: 20px;
|
|
4341
|
+
padding: 0 4px;
|
|
4342
|
+
font-size: 12px;
|
|
4343
|
+
margin-right: 12px;
|
|
4344
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
4345
|
+
}
|
|
4346
|
+
.option.selected .shortcut {
|
|
4347
|
+
background: rgba(0,0,0,0.15);
|
|
4348
|
+
color: rgba(0,0,0,0.9);
|
|
4349
|
+
}
|
|
4350
|
+
|
|
4351
|
+
.option-text {
|
|
4352
|
+
flex: 1;
|
|
4353
|
+
line-height: 1.4;
|
|
4354
|
+
}
|
|
4355
|
+
|
|
4356
|
+
.recommended-badge {
|
|
4357
|
+
font-size: 11px;
|
|
4358
|
+
text-transform: uppercase;
|
|
4359
|
+
letter-spacing: 0.05em;
|
|
4360
|
+
background: rgba(255,255,255,0.15);
|
|
4361
|
+
color: rgba(255,255,255,0.9);
|
|
4362
|
+
padding: 4px 8px;
|
|
4363
|
+
border-radius: 999px;
|
|
4364
|
+
margin-left: 12px;
|
|
4365
|
+
font-weight: 600;
|
|
4366
|
+
}
|
|
4367
|
+
.option.selected .recommended-badge {
|
|
4368
|
+
background: rgba(0,0,0,0.15);
|
|
4369
|
+
color: rgba(0,0,0,0.8);
|
|
4370
|
+
}
|
|
4371
|
+
|
|
4372
|
+
.submit-shortcut {
|
|
4373
|
+
display: inline-block;
|
|
4374
|
+
margin-left: 10px;
|
|
4375
|
+
padding: 3px 8px;
|
|
4376
|
+
border-radius: 999px;
|
|
4377
|
+
background: rgba(0,0,0,0.08);
|
|
4378
|
+
color: rgba(0,0,0,0.7);
|
|
4379
|
+
font-size: 12px;
|
|
4380
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
4381
|
+
}
|
|
4382
|
+
|
|
4383
|
+
textarea {
|
|
4384
|
+
width: 100%;
|
|
4385
|
+
box-sizing: border-box;
|
|
4386
|
+
min-height: 140px;
|
|
4387
|
+
border-radius: 6px;
|
|
4388
|
+
border: 1px solid rgba(255,255,255,0.15);
|
|
4389
|
+
background: rgba(0,0,0,0.6);
|
|
4390
|
+
color: inherit;
|
|
4391
|
+
padding: 16px;
|
|
4392
|
+
font-family: inherit;
|
|
4393
|
+
font-size: 16px;
|
|
4394
|
+
line-height: 1.5;
|
|
4395
|
+
resize: vertical;
|
|
4396
|
+
outline: none;
|
|
4397
|
+
transition: border-color 0.2s ease;
|
|
4398
|
+
}
|
|
4399
|
+
textarea:focus {
|
|
4400
|
+
border-color: rgba(255,255,255,0.5);
|
|
4401
|
+
box-shadow: 0 0 0 1px rgba(255,255,255,0.1);
|
|
4402
|
+
}
|
|
4403
|
+
|
|
4404
|
+
.hidden-textarea {
|
|
4405
|
+
display: none;
|
|
4406
|
+
}
|
|
4407
|
+
|
|
4408
|
+
button.primary {
|
|
4409
|
+
background: #ffffff;
|
|
4410
|
+
color: #000000;
|
|
4411
|
+
border: 0;
|
|
4412
|
+
border-radius: 6px;
|
|
4413
|
+
padding: 16px 24px;
|
|
4414
|
+
font-size: 16px;
|
|
4415
|
+
font-weight: 600;
|
|
4416
|
+
cursor: pointer;
|
|
4417
|
+
width: 100%;
|
|
4418
|
+
transition: opacity 0.2s ease, transform 0.1s ease;
|
|
4419
|
+
}
|
|
4420
|
+
button.primary:hover:not(:disabled) {
|
|
4421
|
+
opacity: 0.9;
|
|
4422
|
+
transform: translateY(-1px);
|
|
4423
|
+
}
|
|
4424
|
+
button.primary:active:not(:disabled) {
|
|
4425
|
+
transform: translateY(1px);
|
|
4426
|
+
}
|
|
4427
|
+
button.primary:disabled {
|
|
4428
|
+
opacity: 0.3;
|
|
4429
|
+
cursor: not-allowed;
|
|
4430
|
+
}
|
|
4431
|
+
|
|
4432
|
+
.footer {
|
|
4433
|
+
margin-top: 32px;
|
|
4434
|
+
text-align: center;
|
|
4435
|
+
font-size: 13px;
|
|
4436
|
+
color: rgba(255,255,255,0.4);
|
|
4437
|
+
}
|
|
4438
|
+
|
|
4439
|
+
/* Loading State Overlay */
|
|
4440
|
+
.loading-overlay {
|
|
4441
|
+
position: fixed;
|
|
4442
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
4443
|
+
background: rgba(0, 0, 0, 0.85);
|
|
4444
|
+
display: flex;
|
|
4445
|
+
flex-direction: column;
|
|
4446
|
+
align-items: center;
|
|
4447
|
+
justify-content: center;
|
|
4448
|
+
z-index: 100;
|
|
4449
|
+
opacity: 0;
|
|
4450
|
+
pointer-events: none;
|
|
4451
|
+
backdrop-filter: blur(8px);
|
|
4452
|
+
transition: opacity 0.3s ease;
|
|
4453
|
+
}
|
|
4454
|
+
.loading-overlay.active {
|
|
4455
|
+
opacity: 1;
|
|
4456
|
+
pointer-events: all;
|
|
4457
|
+
}
|
|
4458
|
+
|
|
4459
|
+
.loading-overlay .status-text {
|
|
4460
|
+
font-size: 15px;
|
|
4461
|
+
letter-spacing: 0.1em;
|
|
4462
|
+
text-transform: uppercase;
|
|
4463
|
+
color: #ffffff;
|
|
4464
|
+
font-weight: 500;
|
|
4465
|
+
}
|
|
4466
|
+
</style>
|
|
4467
|
+
</head>
|
|
4468
|
+
<body>
|
|
4469
|
+
<div class="wrap">
|
|
4470
|
+
<div class="brand-header">
|
|
4471
|
+
<svg
|
|
4472
|
+
class="brand-mark"
|
|
4473
|
+
viewBox="0 0 144 144"
|
|
4474
|
+
role="img"
|
|
4475
|
+
aria-label="Oh My Opencode Slim"
|
|
4476
|
+
>
|
|
4477
|
+
<rect
|
|
4478
|
+
x="12"
|
|
4479
|
+
y="12"
|
|
4480
|
+
width="120"
|
|
4481
|
+
height="120"
|
|
4482
|
+
rx="32"
|
|
4483
|
+
fill="rgba(255,255,255,0.08)"
|
|
4484
|
+
stroke="rgba(255,255,255,0.18)"
|
|
4485
|
+
stroke-width="2"
|
|
4486
|
+
/>
|
|
4487
|
+
<path
|
|
4488
|
+
d="M50 48h18c16 0 26 10 26 24s-10 24-26 24H50z"
|
|
4489
|
+
fill="none"
|
|
4490
|
+
stroke="white"
|
|
4491
|
+
stroke-width="8"
|
|
4492
|
+
stroke-linecap="round"
|
|
4493
|
+
stroke-linejoin="round"
|
|
4494
|
+
/>
|
|
4495
|
+
<path
|
|
4496
|
+
d="M74 48h20c10 0 18 8 18 18v12c0 10-8 18-18 18H74"
|
|
4497
|
+
fill="none"
|
|
4498
|
+
stroke="white"
|
|
4499
|
+
stroke-width="8"
|
|
4500
|
+
stroke-linecap="round"
|
|
4501
|
+
stroke-linejoin="round"
|
|
4502
|
+
opacity="0.65"
|
|
4503
|
+
/>
|
|
4504
|
+
</svg>
|
|
4505
|
+
</div>
|
|
4506
|
+
<h1 id="idea">Connecting...</h1>
|
|
4507
|
+
<p class="muted" id="summary">Preparing interview session</p>
|
|
4508
|
+
|
|
4509
|
+
<div class="meta">
|
|
4510
|
+
<span id="status">INITIALIZING</span>
|
|
4511
|
+
<span>OH MY OPENCODE SLIM</span>
|
|
4512
|
+
</div>
|
|
4513
|
+
|
|
4514
|
+
<div id="filePathContainer" class="file-path-container" style="display: none;">
|
|
4515
|
+
<span class="file-path-icon">\uD83D\uDCC4</span>
|
|
4516
|
+
<span id="markdownPath"></span>
|
|
4517
|
+
</div>
|
|
4518
|
+
|
|
4519
|
+
<div id="questions"></div>
|
|
4520
|
+
|
|
4521
|
+
<button class="primary" id="submitButton" disabled>Submit Answers <span class="submit-shortcut">\u2318\u21B5</span></button>
|
|
4522
|
+
|
|
4523
|
+
<div class="footer" id="submitStatus"></div>
|
|
4524
|
+
</div>
|
|
4525
|
+
|
|
4526
|
+
<div class="loading-overlay" id="loadingOverlay">
|
|
4527
|
+
<div class="status-text" id="loadingText">Processing...</div>
|
|
4528
|
+
</div>
|
|
4529
|
+
|
|
4530
|
+
<script>
|
|
4531
|
+
const interviewId = ${JSON.stringify(interviewId)};
|
|
4532
|
+
const state = { data: null, answers: {}, activeQuestionIndex: 0, lastSig: null, customMode: {} };
|
|
4533
|
+
|
|
4534
|
+
function updateSubmitButton() {
|
|
4535
|
+
const button = document.getElementById('submitButton');
|
|
4536
|
+
if (!state.data) {
|
|
4537
|
+
button.disabled = true;
|
|
4538
|
+
return;
|
|
4539
|
+
}
|
|
4540
|
+
|
|
4541
|
+
const questions = state.data.questions || [];
|
|
4542
|
+
const allAnswered = questions.every((question) =>
|
|
4543
|
+
(state.answers[question.id] || '').trim().length > 0,
|
|
4544
|
+
);
|
|
4545
|
+
button.disabled = state.data.isBusy || !questions.length || !allAnswered;
|
|
4546
|
+
|
|
4547
|
+
const overlay = document.getElementById('loadingOverlay');
|
|
4548
|
+
const overlayText = document.getElementById('loadingText');
|
|
4549
|
+
if (state.data.isBusy) {
|
|
4550
|
+
overlay.classList.add('active');
|
|
4551
|
+
overlayText.textContent = "Agent Thinking...";
|
|
4552
|
+
} else {
|
|
4553
|
+
overlay.classList.remove('active');
|
|
4554
|
+
}
|
|
4555
|
+
}
|
|
4556
|
+
|
|
4557
|
+
function getOptionButtonId(questionId, index) {
|
|
4558
|
+
return 'opt-' + questionId + '-' + index;
|
|
4559
|
+
}
|
|
4560
|
+
|
|
4561
|
+
function createOption(question, option, index, isCustom) {
|
|
4562
|
+
const button = document.createElement('button');
|
|
4563
|
+
button.type = 'button';
|
|
4564
|
+
button.className = 'option';
|
|
4565
|
+
button.id = getOptionButtonId(question.id, index);
|
|
4566
|
+
|
|
4567
|
+
const shortcut = index < 9 ? (index + 1) : '';
|
|
4568
|
+
if (shortcut) {
|
|
4569
|
+
const kbd = document.createElement('span');
|
|
4570
|
+
kbd.className = 'shortcut';
|
|
4571
|
+
kbd.textContent = shortcut;
|
|
4572
|
+
button.appendChild(kbd);
|
|
4573
|
+
}
|
|
4574
|
+
|
|
4575
|
+
const text = document.createElement('span');
|
|
4576
|
+
text.className = 'option-text';
|
|
4577
|
+
text.textContent = isCustom ? 'Custom' : option;
|
|
4578
|
+
button.appendChild(text);
|
|
4579
|
+
|
|
4580
|
+
// Visual marking for suggested/recommended answers
|
|
4581
|
+
if (!isCustom && question.suggested === option) {
|
|
4582
|
+
const badge = document.createElement('span');
|
|
4583
|
+
badge.className = 'recommended-badge';
|
|
4584
|
+
badge.textContent = 'Recommended';
|
|
4585
|
+
button.appendChild(badge);
|
|
4586
|
+
}
|
|
4587
|
+
|
|
4588
|
+
button.addEventListener('click', () => {
|
|
4589
|
+
const questions = state.data?.questions || [];
|
|
4590
|
+
const qIdx = questions.findIndex(q => q.id === question.id);
|
|
4591
|
+
if (qIdx !== -1) {
|
|
4592
|
+
state.activeQuestionIndex = qIdx;
|
|
4593
|
+
updateActiveQuestionFocus();
|
|
4594
|
+
}
|
|
4595
|
+
handleOptionSelect(question, option, isCustom);
|
|
4596
|
+
});
|
|
4597
|
+
|
|
4598
|
+
return button;
|
|
4599
|
+
}
|
|
4600
|
+
|
|
4601
|
+
function handleOptionSelect(question, option, isCustom) {
|
|
4602
|
+
const textarea = document.getElementById('answer-' + question.id);
|
|
4603
|
+
|
|
4604
|
+
if (isCustom) {
|
|
4605
|
+
state.customMode[question.id] = true;
|
|
4606
|
+
state.answers[question.id] = state.customMode[question.id]
|
|
4607
|
+
? state.answers[question.id] || ''
|
|
4608
|
+
: '';
|
|
4609
|
+
updateTextareaVisibility(question.id);
|
|
4610
|
+
updateOptionsDOM(question.id);
|
|
4611
|
+
if (textarea) {
|
|
4612
|
+
textarea.focus();
|
|
4613
|
+
}
|
|
4614
|
+
} else {
|
|
4615
|
+
state.customMode[question.id] = false;
|
|
4616
|
+
state.answers[question.id] = option;
|
|
4617
|
+
updateTextareaVisibility(question.id);
|
|
4618
|
+
advanceToNextQuestion(question.id);
|
|
4619
|
+
}
|
|
4620
|
+
|
|
4621
|
+
updateSubmitButton();
|
|
4622
|
+
updateOptionsDOM(question.id);
|
|
4623
|
+
}
|
|
4624
|
+
|
|
4625
|
+
function updateTextareaVisibility(questionId) {
|
|
4626
|
+
const textarea = document.getElementById('answer-' + questionId);
|
|
4627
|
+
if (!textarea) return;
|
|
4628
|
+
if (state.customMode[questionId]) {
|
|
4629
|
+
textarea.classList.remove('hidden-textarea');
|
|
4630
|
+
} else {
|
|
4631
|
+
textarea.classList.add('hidden-textarea');
|
|
4632
|
+
}
|
|
4633
|
+
}
|
|
4634
|
+
|
|
4635
|
+
function advanceToNextQuestion(currentQuestionId) {
|
|
4636
|
+
const questions = state.data?.questions || [];
|
|
4637
|
+
const currentIndex = questions.findIndex(q => q.id === currentQuestionId);
|
|
4638
|
+
|
|
4639
|
+
if (currentIndex >= 0 && currentIndex < questions.length - 1) {
|
|
4640
|
+
state.activeQuestionIndex = currentIndex + 1;
|
|
4641
|
+
updateActiveQuestionFocus();
|
|
4642
|
+
const nextQuestion = questions[currentIndex + 1];
|
|
4643
|
+
const nextEl = document.getElementById('question-' + nextQuestion.id);
|
|
4644
|
+
if (nextEl) {
|
|
4645
|
+
nextEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
4646
|
+
}
|
|
4647
|
+
} else if (currentIndex === questions.length - 1) {
|
|
4648
|
+
const submitBtn = document.getElementById('submitButton');
|
|
4649
|
+
if (submitBtn && !submitBtn.disabled) {
|
|
4650
|
+
submitBtn.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
4651
|
+
}
|
|
4652
|
+
}
|
|
4653
|
+
}
|
|
4654
|
+
|
|
4655
|
+
function updateOptionsDOM(questionId) {
|
|
4656
|
+
const question = (state.data?.questions || []).find(q => q.id === questionId);
|
|
4657
|
+
if (!question) return;
|
|
4658
|
+
|
|
4659
|
+
const predefined = question.options || [];
|
|
4660
|
+
const currentAnswer = state.answers[question.id];
|
|
4661
|
+
|
|
4662
|
+
predefined.forEach((opt, idx) => {
|
|
4663
|
+
const btn = document.getElementById(getOptionButtonId(questionId, idx));
|
|
4664
|
+
if (btn) {
|
|
4665
|
+
if (currentAnswer === opt) btn.classList.add('selected');
|
|
4666
|
+
else btn.classList.remove('selected');
|
|
4667
|
+
}
|
|
4668
|
+
});
|
|
4669
|
+
|
|
4670
|
+
const customBtn = document.getElementById(getOptionButtonId(questionId, predefined.length));
|
|
4671
|
+
if (customBtn) {
|
|
4672
|
+
if (state.customMode[questionId]) {
|
|
4673
|
+
customBtn.classList.add('selected');
|
|
4674
|
+
} else {
|
|
4675
|
+
customBtn.classList.remove('selected');
|
|
4676
|
+
}
|
|
4677
|
+
}
|
|
4678
|
+
}
|
|
4679
|
+
|
|
4680
|
+
function updateActiveQuestionFocus() {
|
|
4681
|
+
const questions = state.data?.questions || [];
|
|
4682
|
+
questions.forEach((q, idx) => {
|
|
4683
|
+
const wrapper = document.getElementById('question-' + q.id);
|
|
4684
|
+
if (wrapper) {
|
|
4685
|
+
if (idx === state.activeQuestionIndex) {
|
|
4686
|
+
wrapper.classList.add('active-question');
|
|
4687
|
+
} else {
|
|
4688
|
+
wrapper.classList.remove('active-question');
|
|
4689
|
+
}
|
|
4690
|
+
}
|
|
4691
|
+
});
|
|
4692
|
+
}
|
|
4693
|
+
|
|
4694
|
+
document.addEventListener('keydown', (e) => {
|
|
4695
|
+
const isSubmitShortcut =
|
|
4696
|
+
(e.key === 'Enter' && (e.metaKey || e.ctrlKey)) ||
|
|
4697
|
+
(e.key === 's' && (e.metaKey || e.ctrlKey));
|
|
4698
|
+
if (isSubmitShortcut) {
|
|
4699
|
+
const submitBtn = document.getElementById('submitButton');
|
|
4700
|
+
if (submitBtn && !submitBtn.disabled) {
|
|
4701
|
+
submitBtn.click();
|
|
4702
|
+
e.preventDefault();
|
|
4703
|
+
}
|
|
4704
|
+
return;
|
|
4705
|
+
}
|
|
4706
|
+
|
|
4707
|
+
if (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT') return;
|
|
4708
|
+
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
|
4709
|
+
|
|
4710
|
+
const questions = state.data?.questions || [];
|
|
4711
|
+
if (!questions.length) return;
|
|
4712
|
+
|
|
4713
|
+
const num = parseInt(e.key, 10);
|
|
4714
|
+
if (num >= 1 && num <= 9) {
|
|
4715
|
+
const activeQ = questions[state.activeQuestionIndex];
|
|
4716
|
+
if (!activeQ) return;
|
|
4717
|
+
|
|
4718
|
+
const options = activeQ.options || [];
|
|
4719
|
+
if (!options.length) return;
|
|
4720
|
+
|
|
4721
|
+
const idx = num - 1;
|
|
4722
|
+
|
|
4723
|
+
if (idx < options.length) {
|
|
4724
|
+
handleOptionSelect(activeQ, options[idx], false);
|
|
4725
|
+
e.preventDefault();
|
|
4726
|
+
} else if (idx === options.length) {
|
|
4727
|
+
handleOptionSelect(activeQ, 'Custom', true);
|
|
4728
|
+
e.preventDefault();
|
|
4729
|
+
}
|
|
4730
|
+
|
|
4731
|
+
}
|
|
4732
|
+
|
|
4733
|
+
if (e.key === 'ArrowDown') {
|
|
4734
|
+
if (state.activeQuestionIndex < questions.length - 1) {
|
|
4735
|
+
state.activeQuestionIndex++;
|
|
4736
|
+
updateActiveQuestionFocus();
|
|
4737
|
+
const wrapper = document.getElementById('question-' + questions[state.activeQuestionIndex].id);
|
|
4738
|
+
if (wrapper) wrapper.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
4739
|
+
e.preventDefault();
|
|
4740
|
+
}
|
|
4741
|
+
}
|
|
4742
|
+
if (e.key === 'ArrowUp') {
|
|
4743
|
+
if (state.activeQuestionIndex > 0) {
|
|
4744
|
+
state.activeQuestionIndex--;
|
|
4745
|
+
updateActiveQuestionFocus();
|
|
4746
|
+
const wrapper = document.getElementById('question-' + questions[state.activeQuestionIndex].id);
|
|
4747
|
+
if (wrapper) wrapper.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
4748
|
+
e.preventDefault();
|
|
4749
|
+
}
|
|
4750
|
+
}
|
|
4751
|
+
});
|
|
4752
|
+
|
|
4753
|
+
function renderQuestions(questions) {
|
|
4754
|
+
const sig = JSON.stringify(questions);
|
|
4755
|
+
const container = document.getElementById('questions');
|
|
4756
|
+
|
|
4757
|
+
if (state.lastSig === sig) {
|
|
4758
|
+
questions.forEach((q) => updateOptionsDOM(q.id));
|
|
4759
|
+
updateActiveQuestionFocus();
|
|
4760
|
+
return;
|
|
4761
|
+
}
|
|
4762
|
+
|
|
4763
|
+
state.lastSig = sig;
|
|
4764
|
+
container.replaceChildren();
|
|
4765
|
+
|
|
4766
|
+
if (!questions.length && !state.data?.isBusy) {
|
|
4767
|
+
const empty = document.createElement('p');
|
|
4768
|
+
empty.className = 'muted';
|
|
4769
|
+
empty.style.textAlign = 'center';
|
|
4770
|
+
empty.style.padding = '48px 0';
|
|
4771
|
+
empty.textContent = 'No active questions right now.';
|
|
4772
|
+
container.appendChild(empty);
|
|
4773
|
+
return;
|
|
4774
|
+
}
|
|
4775
|
+
|
|
4776
|
+
questions.forEach((question, idx) => {
|
|
4777
|
+
const wrapper = document.createElement('div');
|
|
4778
|
+
wrapper.className = 'question';
|
|
4779
|
+
wrapper.id = 'question-' + question.id;
|
|
4780
|
+
|
|
4781
|
+
if (question.suggested && !state.answers[question.id]) {
|
|
4782
|
+
state.answers[question.id] = question.suggested;
|
|
4783
|
+
state.customMode[question.id] = false;
|
|
4784
|
+
}
|
|
4785
|
+
|
|
4786
|
+
const title = document.createElement('h3');
|
|
4787
|
+
title.textContent = question.question;
|
|
4788
|
+
wrapper.appendChild(title);
|
|
4789
|
+
|
|
4790
|
+
const predefined = question.options || [];
|
|
4791
|
+
if (predefined.length) {
|
|
4792
|
+
const options = document.createElement('div');
|
|
4793
|
+
options.className = 'options';
|
|
4794
|
+
predefined.forEach((option, optIdx) => {
|
|
4795
|
+
options.appendChild(createOption(question, option, optIdx, false));
|
|
4796
|
+
});
|
|
4797
|
+
options.appendChild(createOption(question, 'Custom', predefined.length, true));
|
|
4798
|
+
wrapper.appendChild(options);
|
|
4799
|
+
}
|
|
4800
|
+
|
|
4801
|
+
const textarea = document.createElement('textarea');
|
|
4802
|
+
textarea.id = 'answer-' + question.id;
|
|
4803
|
+
textarea.placeholder = 'Type your answer here...';
|
|
4804
|
+
textarea.value = state.customMode[question.id] ? (state.answers[question.id] || '') : '';
|
|
4805
|
+
if (!state.customMode[question.id]) {
|
|
4806
|
+
textarea.classList.add('hidden-textarea');
|
|
4807
|
+
}
|
|
4808
|
+
|
|
4809
|
+
textarea.addEventListener('focus', () => {
|
|
4810
|
+
state.activeQuestionIndex = idx;
|
|
4811
|
+
updateActiveQuestionFocus();
|
|
4812
|
+
});
|
|
4813
|
+
|
|
4814
|
+
textarea.addEventListener('input', () => {
|
|
4815
|
+
state.answers[question.id] = textarea.value;
|
|
4816
|
+
updateSubmitButton();
|
|
4817
|
+
updateOptionsDOM(question.id);
|
|
4818
|
+
});
|
|
4819
|
+
wrapper.appendChild(textarea);
|
|
4820
|
+
|
|
4821
|
+
container.appendChild(wrapper);
|
|
4822
|
+
});
|
|
4823
|
+
|
|
4824
|
+
updateActiveQuestionFocus();
|
|
4825
|
+
questions.forEach(q => updateOptionsDOM(q.id));
|
|
4826
|
+
}
|
|
4827
|
+
|
|
4828
|
+
function render(data) {
|
|
4829
|
+
state.data = data;
|
|
4830
|
+
document.getElementById('idea').textContent = data.interview.idea || 'Interview';
|
|
4831
|
+
document.getElementById('summary').textContent = data.summary || 'Session in progress.';
|
|
4832
|
+
document.getElementById('status').textContent = data.mode.toUpperCase();
|
|
4833
|
+
|
|
4834
|
+
// Render Markdown Path
|
|
4835
|
+
const pathContainer = document.getElementById('filePathContainer');
|
|
4836
|
+
const pathElement = document.getElementById('markdownPath');
|
|
4837
|
+
const mdPath = data.markdownPath || (data.interview && data.interview.markdownPath);
|
|
4838
|
+
if (mdPath) {
|
|
4839
|
+
pathElement.textContent = mdPath;
|
|
4840
|
+
pathContainer.style.display = 'flex';
|
|
4841
|
+
} else {
|
|
4842
|
+
pathContainer.style.display = 'none';
|
|
4843
|
+
}
|
|
4844
|
+
|
|
4845
|
+
renderQuestions(data.questions || []);
|
|
4846
|
+
updateSubmitButton();
|
|
4847
|
+
}
|
|
4848
|
+
|
|
4849
|
+
async function refresh() {
|
|
4850
|
+
const response = await fetch('/api/interviews/' + encodeURIComponent(interviewId) + '/state');
|
|
4851
|
+
const data = await response.json();
|
|
4852
|
+
if (!response.ok) throw new Error(data.error || 'Failed to load state');
|
|
4853
|
+
render(data);
|
|
4854
|
+
}
|
|
4855
|
+
|
|
4856
|
+
document.getElementById('submitButton').addEventListener('click', async () => {
|
|
4857
|
+
if (!state.data) return;
|
|
4858
|
+
const answers = (state.data.questions || []).map((question) => {
|
|
4859
|
+
return {
|
|
4860
|
+
questionId: question.id,
|
|
4861
|
+
answer: (state.answers[question.id] || '').trim(),
|
|
4862
|
+
};
|
|
4863
|
+
});
|
|
4864
|
+
|
|
4865
|
+
const overlay = document.getElementById('loadingOverlay');
|
|
4866
|
+
const overlayText = document.getElementById('loadingText');
|
|
4867
|
+
overlay.classList.add('active');
|
|
4868
|
+
overlayText.textContent = "Submitting Answers...";
|
|
4869
|
+
|
|
4870
|
+
try {
|
|
4871
|
+
const response = await fetch('/api/interviews/' + encodeURIComponent(interviewId) + '/answers', {
|
|
4872
|
+
method: 'POST',
|
|
4873
|
+
headers: { 'content-type': 'application/json' },
|
|
4874
|
+
body: JSON.stringify({ answers }),
|
|
4875
|
+
});
|
|
4876
|
+
const payload = await response.json();
|
|
4877
|
+
document.getElementById('submitStatus').textContent = payload.message || (response.ok ? 'Answers submitted successfully.' : 'Submission failed.');
|
|
4878
|
+
} catch (err) {
|
|
4879
|
+
document.getElementById('submitStatus').textContent = 'Error submitting answers.';
|
|
4880
|
+
}
|
|
4881
|
+
try {
|
|
4882
|
+
await refresh();
|
|
4883
|
+
} catch (_error) {
|
|
4884
|
+
overlay.classList.remove('active');
|
|
4885
|
+
}
|
|
4886
|
+
});
|
|
4887
|
+
|
|
4888
|
+
refresh().catch((error) => {
|
|
4889
|
+
document.getElementById('submitStatus').textContent = error.message || 'Failed to load interview.';
|
|
4890
|
+
});
|
|
4891
|
+
setInterval(() => {
|
|
4892
|
+
refresh().catch(() => {});
|
|
4893
|
+
}, 2500);
|
|
4894
|
+
</script>
|
|
4895
|
+
</body>
|
|
4896
|
+
</html>`;
|
|
4897
|
+
}
|
|
4898
|
+
|
|
4899
|
+
// src/interview/server.ts
|
|
4900
|
+
function getSubmissionStatus(error) {
|
|
4901
|
+
if (error instanceof SyntaxError) {
|
|
4902
|
+
return 400;
|
|
4903
|
+
}
|
|
4904
|
+
const message = error instanceof Error ? error.message : "";
|
|
4905
|
+
if (message === "Interview not found") {
|
|
4906
|
+
return 404;
|
|
4907
|
+
}
|
|
4908
|
+
if (message.includes("busy")) {
|
|
4909
|
+
return 409;
|
|
4910
|
+
}
|
|
4911
|
+
if (message.includes("waiting for a valid agent update") || message.includes("There are no active interview questions") || message.includes("Answer every active interview question") || message.includes("Answers do not match") || message.includes("Request body too large") || message.includes("Invalid answers payload") || message.includes("no longer active")) {
|
|
4912
|
+
return 400;
|
|
4913
|
+
}
|
|
4914
|
+
return 500;
|
|
4915
|
+
}
|
|
4916
|
+
function parseAnswersPayload(value) {
|
|
4917
|
+
if (!value || typeof value !== "object") {
|
|
4918
|
+
throw new Error("Invalid answers payload.");
|
|
4919
|
+
}
|
|
4920
|
+
const answersRaw = value.answers;
|
|
4921
|
+
if (!Array.isArray(answersRaw)) {
|
|
4922
|
+
throw new Error("Invalid answers payload.");
|
|
4923
|
+
}
|
|
4924
|
+
return {
|
|
4925
|
+
answers: answersRaw.map((answer) => {
|
|
4926
|
+
if (!answer || typeof answer !== "object") {
|
|
4927
|
+
throw new Error("Invalid answers payload.");
|
|
4928
|
+
}
|
|
4929
|
+
const record = answer;
|
|
4930
|
+
if (typeof record.questionId !== "string" || typeof record.answer !== "string") {
|
|
4931
|
+
throw new Error("Invalid answers payload.");
|
|
4932
|
+
}
|
|
4933
|
+
return {
|
|
4934
|
+
questionId: record.questionId.trim(),
|
|
4935
|
+
answer: record.answer.trim()
|
|
4936
|
+
};
|
|
4937
|
+
})
|
|
4938
|
+
};
|
|
4939
|
+
}
|
|
4940
|
+
async function readJsonBody(request) {
|
|
4941
|
+
const chunks = [];
|
|
4942
|
+
let size = 0;
|
|
4943
|
+
for await (const chunk of request) {
|
|
4944
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
4945
|
+
size += buffer.length;
|
|
4946
|
+
if (size > 64 * 1024) {
|
|
4947
|
+
throw new Error("Request body too large");
|
|
4948
|
+
}
|
|
4949
|
+
chunks.push(buffer);
|
|
4950
|
+
}
|
|
4951
|
+
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
4952
|
+
return raw ? JSON.parse(raw) : {};
|
|
4953
|
+
}
|
|
4954
|
+
function sendJson(response, status, value) {
|
|
4955
|
+
response.statusCode = status;
|
|
4956
|
+
response.setHeader("content-type", "application/json; charset=utf-8");
|
|
4957
|
+
response.end(`${JSON.stringify(value)}
|
|
4958
|
+
`);
|
|
4959
|
+
}
|
|
4960
|
+
function sendHtml(response, html) {
|
|
4961
|
+
response.statusCode = 200;
|
|
4962
|
+
response.setHeader("content-type", "text/html; charset=utf-8");
|
|
4963
|
+
response.end(html);
|
|
4964
|
+
}
|
|
4965
|
+
function createInterviewServer(deps) {
|
|
4966
|
+
let baseUrl = null;
|
|
4967
|
+
let startPromise = null;
|
|
4968
|
+
async function handle(request, response) {
|
|
4969
|
+
const url = new URL2(request.url ?? "/", "http://127.0.0.1");
|
|
4970
|
+
const pathname = url.pathname;
|
|
4971
|
+
if (request.method === "GET" && pathname.startsWith("/interview/")) {
|
|
4972
|
+
sendHtml(response, renderInterviewPage(pathname.split("/").pop() ?? "unknown"));
|
|
4973
|
+
return;
|
|
4974
|
+
}
|
|
4975
|
+
const stateMatch = pathname.match(/^\/api\/interviews\/([^/]+)\/state$/);
|
|
4976
|
+
if (request.method === "GET" && stateMatch) {
|
|
4977
|
+
try {
|
|
4978
|
+
const state = await deps.getState(stateMatch[1]);
|
|
4979
|
+
sendJson(response, 200, state);
|
|
4980
|
+
} catch (error) {
|
|
4981
|
+
sendJson(response, 404, {
|
|
4982
|
+
error: error instanceof Error ? error.message : "Interview not found"
|
|
4983
|
+
});
|
|
4984
|
+
}
|
|
4985
|
+
return;
|
|
4986
|
+
}
|
|
4987
|
+
const answersMatch = pathname.match(/^\/api\/interviews\/([^/]+)\/answers$/);
|
|
4988
|
+
if (request.method === "POST" && answersMatch) {
|
|
4989
|
+
try {
|
|
4990
|
+
const body = parseAnswersPayload(await readJsonBody(request));
|
|
4991
|
+
await deps.submitAnswers(answersMatch[1], body.answers);
|
|
4992
|
+
sendJson(response, 200, {
|
|
4993
|
+
ok: true,
|
|
4994
|
+
message: "Answers submitted to the OpenCode session."
|
|
4995
|
+
});
|
|
4996
|
+
} catch (error) {
|
|
4997
|
+
const message = error instanceof Error ? error.message : "Failed to submit answers.";
|
|
4998
|
+
const status = getSubmissionStatus(error);
|
|
4999
|
+
sendJson(response, status, {
|
|
5000
|
+
ok: false,
|
|
5001
|
+
message
|
|
5002
|
+
});
|
|
5003
|
+
}
|
|
5004
|
+
return;
|
|
5005
|
+
}
|
|
5006
|
+
sendJson(response, 404, { error: "Not found" });
|
|
5007
|
+
}
|
|
5008
|
+
async function ensureStarted() {
|
|
5009
|
+
if (baseUrl) {
|
|
5010
|
+
return baseUrl;
|
|
5011
|
+
}
|
|
5012
|
+
if (startPromise) {
|
|
5013
|
+
return startPromise;
|
|
5014
|
+
}
|
|
5015
|
+
startPromise = new Promise((resolve, reject) => {
|
|
5016
|
+
const server = createServer((request, response) => {
|
|
5017
|
+
handle(request, response).catch((error) => {
|
|
5018
|
+
sendJson(response, 500, {
|
|
5019
|
+
error: error instanceof Error ? error.message : "Internal server error"
|
|
5020
|
+
});
|
|
5021
|
+
});
|
|
5022
|
+
});
|
|
5023
|
+
server.on("error", (error) => {
|
|
5024
|
+
startPromise = null;
|
|
5025
|
+
reject(error);
|
|
5026
|
+
});
|
|
5027
|
+
server.listen(0, "127.0.0.1", () => {
|
|
5028
|
+
const address = server.address();
|
|
5029
|
+
if (!address || typeof address === "string") {
|
|
5030
|
+
startPromise = null;
|
|
5031
|
+
reject(new Error("Failed to start interview server"));
|
|
5032
|
+
return;
|
|
5033
|
+
}
|
|
5034
|
+
baseUrl = `http://127.0.0.1:${address.port}`;
|
|
5035
|
+
resolve(baseUrl);
|
|
5036
|
+
});
|
|
5037
|
+
});
|
|
5038
|
+
return startPromise;
|
|
5039
|
+
}
|
|
5040
|
+
return {
|
|
5041
|
+
ensureStarted
|
|
5042
|
+
};
|
|
5043
|
+
}
|
|
5044
|
+
|
|
5045
|
+
// src/interview/service.ts
|
|
5046
|
+
import { spawn as spawn4 } from "child_process";
|
|
5047
|
+
import * as fsSync from "fs";
|
|
5048
|
+
import * as fs5 from "fs/promises";
|
|
5049
|
+
import * as path6 from "path";
|
|
5050
|
+
|
|
5051
|
+
// src/interview/parser.ts
|
|
5052
|
+
var INTERVIEW_BLOCK_REGEX = /<interview_state>\s*([\s\S]*?)\s*<\/interview_state>/i;
|
|
5053
|
+
function normalizeQuestion(value, index) {
|
|
5054
|
+
const question = typeof value.question === "string" ? value.question.trim() : "";
|
|
5055
|
+
if (!question) {
|
|
5056
|
+
return null;
|
|
5057
|
+
}
|
|
5058
|
+
const options = Array.isArray(value.options) ? value.options.filter((option) => typeof option === "string").map((option) => option.trim()).filter(Boolean).slice(0, 4) : [];
|
|
5059
|
+
return {
|
|
5060
|
+
id: typeof value.id === "string" && value.id.trim().length > 0 ? value.id.trim() : `q-${index + 1}`,
|
|
5061
|
+
question,
|
|
5062
|
+
options,
|
|
5063
|
+
suggested: typeof value.suggested === "string" && value.suggested.trim().length > 0 ? value.suggested.trim() : undefined
|
|
5064
|
+
};
|
|
5065
|
+
}
|
|
5066
|
+
function flattenMessage(message) {
|
|
5067
|
+
return (message.parts ?? []).map((part) => part.text ?? "").join(`
|
|
5068
|
+
`).trim();
|
|
5069
|
+
}
|
|
5070
|
+
function buildFallbackState(messages) {
|
|
5071
|
+
const answerCount = messages.filter((message) => message.info?.role === "user").length;
|
|
5072
|
+
return {
|
|
5073
|
+
summary: answerCount > 0 ? "Interview in progress." : "Waiting for the first interview response.",
|
|
5074
|
+
questions: []
|
|
5075
|
+
};
|
|
5076
|
+
}
|
|
5077
|
+
function parseAssistantState(text, maxQuestions = 2) {
|
|
5078
|
+
const match = text.match(INTERVIEW_BLOCK_REGEX);
|
|
5079
|
+
if (!match) {
|
|
5080
|
+
return { state: null };
|
|
5081
|
+
}
|
|
5082
|
+
try {
|
|
5083
|
+
const parsed = JSON.parse(match[1]);
|
|
5084
|
+
const summary = typeof parsed.summary === "string" ? parsed.summary.trim() : "";
|
|
5085
|
+
const title = typeof parsed.title === "string" && parsed.title.trim().length > 0 ? parsed.title.trim() : undefined;
|
|
5086
|
+
const questions = Array.isArray(parsed.questions) ? parsed.questions.filter((value) => typeof value === "object" && value !== null).map((value, index) => normalizeQuestion(value, index)).filter((value) => value !== null).slice(0, maxQuestions) : [];
|
|
5087
|
+
return {
|
|
5088
|
+
state: {
|
|
5089
|
+
summary,
|
|
5090
|
+
title,
|
|
5091
|
+
questions
|
|
5092
|
+
}
|
|
5093
|
+
};
|
|
5094
|
+
} catch (error) {
|
|
5095
|
+
return {
|
|
5096
|
+
state: null,
|
|
5097
|
+
error: error instanceof Error ? error.message : "Failed to parse interview state"
|
|
5098
|
+
};
|
|
5099
|
+
}
|
|
5100
|
+
}
|
|
5101
|
+
function findLatestAssistantState(messages, maxQuestions = 2) {
|
|
5102
|
+
for (let index = messages.length - 1;index >= 0; index -= 1) {
|
|
5103
|
+
const message = messages[index];
|
|
5104
|
+
if (message.info?.role !== "assistant") {
|
|
5105
|
+
continue;
|
|
5106
|
+
}
|
|
5107
|
+
const parsed = parseAssistantState(flattenMessage(message), maxQuestions);
|
|
5108
|
+
if (parsed.state) {
|
|
5109
|
+
return {
|
|
5110
|
+
state: parsed.state
|
|
5111
|
+
};
|
|
5112
|
+
}
|
|
5113
|
+
return {
|
|
5114
|
+
state: null,
|
|
5115
|
+
latestAssistantError: parsed.error ?? "Missing <interview_state> block"
|
|
5116
|
+
};
|
|
5117
|
+
}
|
|
5118
|
+
return {
|
|
5119
|
+
state: null
|
|
5120
|
+
};
|
|
5121
|
+
}
|
|
5122
|
+
|
|
5123
|
+
// src/interview/prompts.ts
|
|
5124
|
+
function formatQuestionContext(questions) {
|
|
5125
|
+
if (questions.length === 0) {
|
|
5126
|
+
return "No current interview questions were parsed.";
|
|
5127
|
+
}
|
|
5128
|
+
return questions.map((question, index) => {
|
|
5129
|
+
const options = question.options.length ? `Options: ${question.options.join(" | ")}` : "Options: freeform";
|
|
5130
|
+
const suggested = question.suggested ? `Suggested: ${question.suggested}` : "Suggested: none";
|
|
5131
|
+
return `${index + 1}. ${question.question}
|
|
5132
|
+
${options}
|
|
5133
|
+
${suggested}`;
|
|
5134
|
+
}).join(`
|
|
5135
|
+
|
|
5136
|
+
`);
|
|
5137
|
+
}
|
|
5138
|
+
function buildKickoffPrompt(idea, maxQuestions) {
|
|
5139
|
+
return [
|
|
5140
|
+
"You are running an interview q&a session for the user inside their repository.",
|
|
5141
|
+
`Initial idea: ${idea}`,
|
|
5142
|
+
`Clarify the idea through short rounds of at most ${maxQuestions} questions at a time.`,
|
|
5143
|
+
"When useful, each question may include 2 to 4 answer options and one suggested option.",
|
|
5144
|
+
"Be practical. Focus on the highest-ambiguity and highest-risk decisions first.",
|
|
5145
|
+
"After any short human-friendly preface, you MUST include a machine-readable block in this exact format:",
|
|
5146
|
+
"<interview_state>",
|
|
5147
|
+
"{",
|
|
5148
|
+
' "summary": "one short paragraph about the current understanding",',
|
|
5149
|
+
' "title": "concise-kebab-case-title-for-filename",',
|
|
5150
|
+
' "questions": [',
|
|
5151
|
+
" {",
|
|
5152
|
+
' "id": "short-kebab-id-2",',
|
|
5153
|
+
' "question": "question text",',
|
|
5154
|
+
' "options": ["option 1", "option 2", "option 3"],',
|
|
5155
|
+
' "suggested": "best suggested option"',
|
|
5156
|
+
" }",
|
|
5157
|
+
" ]",
|
|
5158
|
+
"}",
|
|
5159
|
+
"</interview_state>",
|
|
5160
|
+
"Rules:",
|
|
5161
|
+
`- Return 0 to ${maxQuestions} questions.`,
|
|
5162
|
+
"- If there are no more useful questions, return zero questions.",
|
|
5163
|
+
`- Do not ask more than ${maxQuestions} questions in one round.`,
|
|
5164
|
+
'- Provide a concise "title" field (kebab-case, 3-6 words) suitable for a filename.'
|
|
5165
|
+
].join(`
|
|
5166
|
+
`);
|
|
5167
|
+
}
|
|
5168
|
+
function buildResumePrompt(document, maxQuestions) {
|
|
5169
|
+
return [
|
|
5170
|
+
"Resume the interview from this existing markdown document.",
|
|
5171
|
+
"Use the current spec and Q&A history as ground truth so far.",
|
|
5172
|
+
"Do not restart from scratch.",
|
|
5173
|
+
"",
|
|
5174
|
+
document,
|
|
5175
|
+
"",
|
|
5176
|
+
`Ask the next highest-value clarifying questions, up to ${maxQuestions} at a time.`,
|
|
5177
|
+
"If there are no more useful questions, return zero questions.",
|
|
5178
|
+
"Return the same <interview_state> JSON block format as before."
|
|
5179
|
+
].join(`
|
|
5180
|
+
`);
|
|
5181
|
+
}
|
|
5182
|
+
function buildAnswerPrompt(answers, questions, maxQuestions) {
|
|
5183
|
+
const answerText = answers.map((answer, index) => `${index + 1}. ${answer.questionId}: ${answer.answer.trim()}`).join(`
|
|
5184
|
+
`);
|
|
5185
|
+
return [
|
|
5186
|
+
"Continue the same interview.",
|
|
5187
|
+
"These were the active questions:",
|
|
5188
|
+
formatQuestionContext(questions),
|
|
5189
|
+
"The user answered:",
|
|
5190
|
+
answerText,
|
|
5191
|
+
"Now update your understanding and ask the next highest-value clarifying questions.",
|
|
5192
|
+
`Return 0 to ${maxQuestions} questions. If there are no more useful questions, return zero questions.`,
|
|
5193
|
+
"Return the same <interview_state> JSON block format as before."
|
|
5194
|
+
].join(`
|
|
5195
|
+
|
|
5196
|
+
`);
|
|
5197
|
+
}
|
|
5198
|
+
|
|
5199
|
+
// src/interview/service.ts
|
|
5200
|
+
var COMMAND_NAME2 = "interview";
|
|
5201
|
+
var DEFAULT_MAX_QUESTIONS = 2;
|
|
5202
|
+
var DEFAULT_OUTPUT_FOLDER = "interview";
|
|
5203
|
+
var DEFAULT_AUTO_OPEN_BROWSER = true;
|
|
5204
|
+
function slugify(value) {
|
|
5205
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
|
|
5206
|
+
}
|
|
5207
|
+
function openBrowser(url) {
|
|
5208
|
+
const platform = process.platform;
|
|
5209
|
+
let command;
|
|
5210
|
+
let args;
|
|
5211
|
+
if (platform === "darwin") {
|
|
5212
|
+
command = "open";
|
|
5213
|
+
args = [url];
|
|
5214
|
+
} else if (platform === "win32") {
|
|
5215
|
+
command = "cmd";
|
|
5216
|
+
args = ["/c", "start", "", url];
|
|
5217
|
+
} else {
|
|
5218
|
+
command = "xdg-open";
|
|
5219
|
+
args = [url];
|
|
5220
|
+
}
|
|
5221
|
+
try {
|
|
5222
|
+
const child = spawn4(command, args, { detached: true, stdio: "ignore" });
|
|
5223
|
+
child.on("error", (error) => {
|
|
5224
|
+
log("[interview] failed to open browser:", { error: error.message, url });
|
|
5225
|
+
});
|
|
5226
|
+
child.unref();
|
|
5227
|
+
} catch (error) {
|
|
5228
|
+
log("[interview] failed to spawn browser opener:", {
|
|
5229
|
+
error: error instanceof Error ? error.message : String(error),
|
|
5230
|
+
url
|
|
5231
|
+
});
|
|
5232
|
+
}
|
|
5233
|
+
}
|
|
5234
|
+
function nowIso() {
|
|
5235
|
+
return new Date().toISOString();
|
|
5236
|
+
}
|
|
5237
|
+
function normalizeOutputFolder(outputFolder) {
|
|
5238
|
+
const normalized = outputFolder.trim().replace(/^\/+|\/+$/g, "");
|
|
5239
|
+
return normalized || DEFAULT_OUTPUT_FOLDER;
|
|
5240
|
+
}
|
|
5241
|
+
function createInterviewDirectoryPath(directory, outputFolder) {
|
|
5242
|
+
return path6.join(directory, normalizeOutputFolder(outputFolder));
|
|
5243
|
+
}
|
|
5244
|
+
function createInterviewFilePath(directory, outputFolder, idea) {
|
|
5245
|
+
const fileName = `${slugify(idea) || "interview"}.md`;
|
|
5246
|
+
return path6.join(createInterviewDirectoryPath(directory, outputFolder), fileName);
|
|
5247
|
+
}
|
|
5248
|
+
function relativeInterviewPath(directory, filePath) {
|
|
5249
|
+
return path6.relative(directory, filePath) || path6.basename(filePath);
|
|
5250
|
+
}
|
|
5251
|
+
function extractHistorySection(document) {
|
|
5252
|
+
const marker = `## Q&A history
|
|
5253
|
+
|
|
5254
|
+
`;
|
|
5255
|
+
const index = document.indexOf(marker);
|
|
5256
|
+
return index >= 0 ? document.slice(index + marker.length).trim() : "";
|
|
5257
|
+
}
|
|
5258
|
+
function extractSummarySection(document) {
|
|
5259
|
+
const marker = `## Current spec
|
|
5260
|
+
|
|
5261
|
+
`;
|
|
5262
|
+
const historyMarker = `
|
|
5263
|
+
|
|
5264
|
+
## Q&A history`;
|
|
5265
|
+
const start = document.indexOf(marker);
|
|
5266
|
+
if (start < 0) {
|
|
5267
|
+
return "";
|
|
5268
|
+
}
|
|
5269
|
+
const summaryStart = start + marker.length;
|
|
5270
|
+
const summaryEnd = document.indexOf(historyMarker, summaryStart);
|
|
5271
|
+
return document.slice(summaryStart, summaryEnd >= 0 ? summaryEnd : undefined).trim();
|
|
5272
|
+
}
|
|
5273
|
+
function extractTitle(document) {
|
|
5274
|
+
const match = document.match(/^#\s+(.+)$/m);
|
|
5275
|
+
return match?.[1]?.trim() ?? "";
|
|
5276
|
+
}
|
|
5277
|
+
function buildInterviewDocument(idea, summary, history) {
|
|
5278
|
+
const normalizedSummary = summary.trim() || "Waiting for interview answers.";
|
|
5279
|
+
const normalizedHistory = history.trim() || "No answers yet.";
|
|
5280
|
+
return [
|
|
5281
|
+
`# ${idea}`,
|
|
5282
|
+
"",
|
|
5283
|
+
"## Current spec",
|
|
5284
|
+
"",
|
|
5285
|
+
normalizedSummary,
|
|
5286
|
+
"",
|
|
5287
|
+
"## Q&A history",
|
|
5288
|
+
"",
|
|
5289
|
+
normalizedHistory,
|
|
5290
|
+
""
|
|
5291
|
+
].join(`
|
|
5292
|
+
`);
|
|
5293
|
+
}
|
|
5294
|
+
async function ensureInterviewFile(record) {
|
|
5295
|
+
await fs5.mkdir(path6.dirname(record.markdownPath), { recursive: true });
|
|
5296
|
+
try {
|
|
5297
|
+
await fs5.access(record.markdownPath);
|
|
5298
|
+
} catch {
|
|
5299
|
+
await fs5.writeFile(record.markdownPath, buildInterviewDocument(record.idea, "", ""), "utf8");
|
|
5300
|
+
}
|
|
5301
|
+
}
|
|
5302
|
+
async function readInterviewDocument(record) {
|
|
5303
|
+
await ensureInterviewFile(record);
|
|
5304
|
+
return fs5.readFile(record.markdownPath, "utf8");
|
|
5305
|
+
}
|
|
5306
|
+
async function rewriteInterviewDocument(record, summary) {
|
|
5307
|
+
const existing = await readInterviewDocument(record);
|
|
5308
|
+
const history = extractHistorySection(existing);
|
|
5309
|
+
const next = buildInterviewDocument(record.idea, summary, history);
|
|
5310
|
+
await fs5.writeFile(record.markdownPath, next, "utf8");
|
|
5311
|
+
return next;
|
|
5312
|
+
}
|
|
5313
|
+
async function appendInterviewAnswers(record, questions, answers) {
|
|
5314
|
+
const existing = await readInterviewDocument(record);
|
|
5315
|
+
const summary = extractSummarySection(existing);
|
|
5316
|
+
const history = extractHistorySection(existing);
|
|
5317
|
+
const questionMap = new Map(questions.map((question) => [question.id, question]));
|
|
5318
|
+
const appended = answers.map((answer) => {
|
|
5319
|
+
const question = questionMap.get(answer.questionId);
|
|
5320
|
+
return question ? `Q: ${question.question}
|
|
5321
|
+
A: ${answer.answer.trim()}` : null;
|
|
5322
|
+
}).filter((value) => value !== null).join(`
|
|
5323
|
+
|
|
5324
|
+
`);
|
|
5325
|
+
const nextHistory = [history === "No answers yet." ? "" : history, appended].filter(Boolean).join(`
|
|
5326
|
+
|
|
5327
|
+
`);
|
|
5328
|
+
await fs5.writeFile(record.markdownPath, buildInterviewDocument(record.idea, summary, nextHistory), "utf8");
|
|
5329
|
+
}
|
|
5330
|
+
function resolveExistingInterviewPath(directory, outputFolder, value) {
|
|
5331
|
+
const trimmed = value.trim();
|
|
5332
|
+
if (!trimmed) {
|
|
5333
|
+
return null;
|
|
5334
|
+
}
|
|
5335
|
+
const outputDir = createInterviewDirectoryPath(directory, outputFolder);
|
|
5336
|
+
const candidates = new Set;
|
|
5337
|
+
if (path6.isAbsolute(trimmed)) {
|
|
5338
|
+
candidates.add(trimmed);
|
|
5339
|
+
} else {
|
|
5340
|
+
candidates.add(path6.resolve(directory, trimmed));
|
|
5341
|
+
candidates.add(path6.join(outputDir, trimmed));
|
|
5342
|
+
if (!trimmed.endsWith(".md")) {
|
|
5343
|
+
candidates.add(path6.join(outputDir, `${trimmed}.md`));
|
|
5344
|
+
}
|
|
5345
|
+
}
|
|
5346
|
+
for (const candidate of candidates) {
|
|
5347
|
+
if (path6.extname(candidate) !== ".md") {
|
|
5348
|
+
continue;
|
|
5349
|
+
}
|
|
5350
|
+
if (fsSync.existsSync(candidate)) {
|
|
5351
|
+
return candidate;
|
|
5352
|
+
}
|
|
5353
|
+
}
|
|
5354
|
+
return null;
|
|
5355
|
+
}
|
|
5356
|
+
function createInterviewService(ctx, config, deps) {
|
|
5357
|
+
const maxQuestions = config?.maxQuestions ?? DEFAULT_MAX_QUESTIONS;
|
|
5358
|
+
const outputFolder = normalizeOutputFolder(config?.outputFolder ?? DEFAULT_OUTPUT_FOLDER);
|
|
5359
|
+
const autoOpenBrowser = config?.autoOpenBrowser ?? DEFAULT_AUTO_OPEN_BROWSER;
|
|
5360
|
+
const browserOpener = deps?.openBrowser ?? openBrowser;
|
|
5361
|
+
const activeInterviewIds = new Map;
|
|
5362
|
+
const interviewsById = new Map;
|
|
5363
|
+
const sessionBusy = new Map;
|
|
5364
|
+
const browserOpened = new Set;
|
|
5365
|
+
let resolveBaseUrl = null;
|
|
5366
|
+
function setBaseUrlResolver(resolver) {
|
|
5367
|
+
resolveBaseUrl = resolver;
|
|
5368
|
+
}
|
|
5369
|
+
async function ensureServer() {
|
|
5370
|
+
if (!resolveBaseUrl) {
|
|
5371
|
+
throw new Error("Interview server is not attached");
|
|
5372
|
+
}
|
|
5373
|
+
return resolveBaseUrl();
|
|
5374
|
+
}
|
|
5375
|
+
function maybeOpenBrowser(interviewId, url) {
|
|
5376
|
+
if (!autoOpenBrowser) {
|
|
5377
|
+
return;
|
|
5378
|
+
}
|
|
5379
|
+
if (browserOpened.has(interviewId)) {
|
|
5380
|
+
return;
|
|
5381
|
+
}
|
|
5382
|
+
browserOpened.add(interviewId);
|
|
5383
|
+
browserOpener(url);
|
|
5384
|
+
}
|
|
5385
|
+
async function maybeRenameWithTitle(interview, assistantTitle) {
|
|
5386
|
+
if (!assistantTitle) {
|
|
5387
|
+
return;
|
|
5388
|
+
}
|
|
5389
|
+
const newSlug = slugify(assistantTitle);
|
|
5390
|
+
if (!newSlug) {
|
|
5391
|
+
return;
|
|
5392
|
+
}
|
|
5393
|
+
const currentFileName = path6.basename(interview.markdownPath, ".md");
|
|
5394
|
+
if (currentFileName === newSlug) {
|
|
5395
|
+
return;
|
|
5396
|
+
}
|
|
5397
|
+
const dir = path6.dirname(interview.markdownPath);
|
|
5398
|
+
const newPath = path6.join(dir, `${newSlug}.md`);
|
|
5399
|
+
try {
|
|
5400
|
+
await fs5.access(newPath);
|
|
5401
|
+
return;
|
|
5402
|
+
} catch {}
|
|
5403
|
+
try {
|
|
5404
|
+
await fs5.rename(interview.markdownPath, newPath);
|
|
5405
|
+
interview.markdownPath = newPath;
|
|
5406
|
+
log("[interview] renamed file with assistant title:", {
|
|
5407
|
+
from: currentFileName,
|
|
5408
|
+
to: newSlug
|
|
5409
|
+
});
|
|
5410
|
+
} catch (error) {
|
|
5411
|
+
log("[interview] failed to rename file:", {
|
|
5412
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5413
|
+
});
|
|
5414
|
+
}
|
|
5415
|
+
}
|
|
5416
|
+
async function loadMessages(sessionID) {
|
|
5417
|
+
const result = await ctx.client.session.messages({
|
|
5418
|
+
path: { id: sessionID }
|
|
5419
|
+
});
|
|
5420
|
+
return result.data;
|
|
5421
|
+
}
|
|
5422
|
+
function isUserVisibleMessage(message) {
|
|
5423
|
+
return !(message.parts ?? []).some((part) => hasInternalInitiatorMarker(part));
|
|
5424
|
+
}
|
|
5425
|
+
function getInterviewById(interviewId) {
|
|
5426
|
+
return interviewsById.get(interviewId) ?? null;
|
|
5427
|
+
}
|
|
5428
|
+
async function createInterview(sessionID, idea) {
|
|
5429
|
+
const normalizedIdea = idea.trim();
|
|
5430
|
+
const activeId = activeInterviewIds.get(sessionID);
|
|
5431
|
+
if (activeId) {
|
|
5432
|
+
const active = interviewsById.get(activeId);
|
|
5433
|
+
if (active && active.status === "active") {
|
|
5434
|
+
if (active.idea === normalizedIdea) {
|
|
5435
|
+
return active;
|
|
5436
|
+
}
|
|
5437
|
+
active.status = "abandoned";
|
|
5438
|
+
}
|
|
5439
|
+
}
|
|
5440
|
+
const messages = await loadMessages(sessionID);
|
|
5441
|
+
const record = {
|
|
5442
|
+
id: `${Date.now()}-${slugify(idea) || "interview"}`,
|
|
5443
|
+
sessionID,
|
|
5444
|
+
idea: normalizedIdea,
|
|
5445
|
+
markdownPath: createInterviewFilePath(ctx.directory, outputFolder, idea),
|
|
5446
|
+
createdAt: nowIso(),
|
|
5447
|
+
status: "active",
|
|
5448
|
+
baseMessageCount: messages.length
|
|
5449
|
+
};
|
|
5450
|
+
await ensureInterviewFile(record);
|
|
5451
|
+
activeInterviewIds.set(sessionID, record.id);
|
|
5452
|
+
interviewsById.set(record.id, record);
|
|
5453
|
+
return record;
|
|
5454
|
+
}
|
|
5455
|
+
async function resumeInterview(sessionID, markdownPath) {
|
|
5456
|
+
const activeId = activeInterviewIds.get(sessionID);
|
|
5457
|
+
if (activeId) {
|
|
5458
|
+
const active = interviewsById.get(activeId);
|
|
5459
|
+
if (active && active.status === "active") {
|
|
5460
|
+
if (active.markdownPath === markdownPath) {
|
|
5461
|
+
return active;
|
|
5462
|
+
}
|
|
5463
|
+
active.status = "abandoned";
|
|
5464
|
+
}
|
|
5465
|
+
}
|
|
5466
|
+
const document = await fs5.readFile(markdownPath, "utf8");
|
|
5467
|
+
const messages = await loadMessages(sessionID);
|
|
5468
|
+
const title = extractTitle(document);
|
|
5469
|
+
const record = {
|
|
5470
|
+
id: `${Date.now()}-${slugify(path6.basename(markdownPath, ".md")) || "interview"}`,
|
|
5471
|
+
sessionID,
|
|
5472
|
+
idea: title || path6.basename(markdownPath, ".md"),
|
|
5473
|
+
markdownPath,
|
|
5474
|
+
createdAt: nowIso(),
|
|
5475
|
+
status: "active",
|
|
5476
|
+
baseMessageCount: messages.length
|
|
5477
|
+
};
|
|
5478
|
+
activeInterviewIds.set(sessionID, record.id);
|
|
5479
|
+
interviewsById.set(record.id, record);
|
|
5480
|
+
return record;
|
|
5481
|
+
}
|
|
5482
|
+
async function syncInterview(interview) {
|
|
5483
|
+
const allMessages = await loadMessages(interview.sessionID);
|
|
5484
|
+
const interviewMessages = allMessages.slice(interview.baseMessageCount).filter(isUserVisibleMessage);
|
|
5485
|
+
const parsed = findLatestAssistantState(interviewMessages, maxQuestions);
|
|
5486
|
+
const existingDocument = await readInterviewDocument(interview);
|
|
5487
|
+
const fallbackState = buildFallbackState(interviewMessages);
|
|
5488
|
+
const state = parsed.state ?? {
|
|
5489
|
+
...fallbackState,
|
|
5490
|
+
summary: extractSummarySection(existingDocument) || fallbackState.summary
|
|
5491
|
+
};
|
|
5492
|
+
await maybeRenameWithTitle(interview, state.title);
|
|
5493
|
+
const document = await rewriteInterviewDocument(interview, state.summary);
|
|
5494
|
+
return {
|
|
5495
|
+
interview,
|
|
5496
|
+
url: `${await ensureServer()}/interview/${interview.id}`,
|
|
5497
|
+
markdownPath: relativeInterviewPath(ctx.directory, interview.markdownPath),
|
|
5498
|
+
mode: interview.status === "abandoned" ? "abandoned" : parsed.latestAssistantError ? "error" : sessionBusy.get(interview.sessionID) === true ? "awaiting-agent" : state.questions.length > 0 ? "awaiting-user" : "awaiting-agent",
|
|
5499
|
+
lastParseError: parsed.latestAssistantError,
|
|
5500
|
+
isBusy: sessionBusy.get(interview.sessionID) === true,
|
|
5501
|
+
summary: state.summary,
|
|
5502
|
+
questions: state.questions,
|
|
5503
|
+
document
|
|
5504
|
+
};
|
|
5505
|
+
}
|
|
5506
|
+
async function notifyInterviewUrl(sessionID, interview) {
|
|
5507
|
+
const baseUrl = await ensureServer();
|
|
5508
|
+
const url = `${baseUrl}/interview/${interview.id}`;
|
|
5509
|
+
maybeOpenBrowser(interview.id, url);
|
|
5510
|
+
await ctx.client.session.prompt({
|
|
5511
|
+
path: { id: sessionID },
|
|
5512
|
+
body: {
|
|
5513
|
+
noReply: true,
|
|
5514
|
+
parts: [
|
|
5515
|
+
{
|
|
5516
|
+
type: "text",
|
|
5517
|
+
text: [
|
|
5518
|
+
"\u2394 Interview UI ready",
|
|
5519
|
+
"",
|
|
5520
|
+
`Open: ${url}`,
|
|
5521
|
+
`Document: ${relativeInterviewPath(ctx.directory, interview.markdownPath)}`,
|
|
5522
|
+
"",
|
|
5523
|
+
"[system status: continue without acknowledging this notification]"
|
|
5524
|
+
].join(`
|
|
5525
|
+
`)
|
|
5526
|
+
}
|
|
5527
|
+
]
|
|
4009
5528
|
}
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
5529
|
+
});
|
|
5530
|
+
}
|
|
5531
|
+
function registerCommand(opencodeConfig) {
|
|
5532
|
+
const configCommand = opencodeConfig.command;
|
|
5533
|
+
if (!configCommand?.[COMMAND_NAME2]) {
|
|
5534
|
+
if (!opencodeConfig.command) {
|
|
5535
|
+
opencodeConfig.command = {};
|
|
4015
5536
|
}
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
5537
|
+
opencodeConfig.command[COMMAND_NAME2] = {
|
|
5538
|
+
template: "Start an interview and write a live markdown spec",
|
|
5539
|
+
description: "Open a localhost interview UI linked to the current OpenCode session"
|
|
5540
|
+
};
|
|
5541
|
+
}
|
|
5542
|
+
}
|
|
5543
|
+
async function getInterviewState(interviewId) {
|
|
5544
|
+
const interview = getInterviewById(interviewId);
|
|
5545
|
+
if (!interview) {
|
|
5546
|
+
throw new Error("Interview not found");
|
|
5547
|
+
}
|
|
5548
|
+
return syncInterview(interview);
|
|
5549
|
+
}
|
|
5550
|
+
async function submitAnswers(interviewId, answers) {
|
|
5551
|
+
const interview = getInterviewById(interviewId);
|
|
5552
|
+
if (!interview) {
|
|
5553
|
+
throw new Error("Interview not found");
|
|
5554
|
+
}
|
|
5555
|
+
if (interview.status === "abandoned") {
|
|
5556
|
+
throw new Error("Interview session is no longer active.");
|
|
5557
|
+
}
|
|
5558
|
+
if (sessionBusy.get(interview.sessionID) === true) {
|
|
5559
|
+
throw new Error("Interview session is busy. Wait for the current response.");
|
|
5560
|
+
}
|
|
5561
|
+
sessionBusy.set(interview.sessionID, true);
|
|
5562
|
+
let promptSent = false;
|
|
5563
|
+
try {
|
|
5564
|
+
const state = await getInterviewState(interviewId);
|
|
5565
|
+
if (state.mode === "error") {
|
|
5566
|
+
throw new Error("Interview is waiting for a valid agent update.");
|
|
4023
5567
|
}
|
|
4024
|
-
const
|
|
4025
|
-
if (
|
|
4026
|
-
|
|
4027
|
-
sessionID,
|
|
4028
|
-
suppressUntil: state.suppressUntil
|
|
4029
|
-
});
|
|
4030
|
-
return;
|
|
5568
|
+
const activeQuestionIds = new Set(state.questions.map((question) => question.id));
|
|
5569
|
+
if (activeQuestionIds.size === 0) {
|
|
5570
|
+
throw new Error("There are no active interview questions to answer.");
|
|
4031
5571
|
}
|
|
4032
|
-
if (
|
|
4033
|
-
|
|
4034
|
-
sessionID
|
|
4035
|
-
});
|
|
4036
|
-
return;
|
|
5572
|
+
if (answers.length !== activeQuestionIds.size) {
|
|
5573
|
+
throw new Error("Answer every active interview question before submitting.");
|
|
4037
5574
|
}
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
}
|
|
4042
|
-
|
|
4043
|
-
|
|
5575
|
+
const invalidAnswer = answers.find((answer) => !activeQuestionIds.has(answer.questionId) || !answer.answer.trim());
|
|
5576
|
+
if (invalidAnswer) {
|
|
5577
|
+
throw new Error("Answers do not match the current interview questions.");
|
|
5578
|
+
}
|
|
5579
|
+
await appendInterviewAnswers(interview, state.questions, answers);
|
|
5580
|
+
const prompt = buildAnswerPrompt(answers, state.questions, maxQuestions);
|
|
5581
|
+
await ctx.client.session.prompt({
|
|
5582
|
+
path: { id: interview.sessionID },
|
|
4044
5583
|
body: {
|
|
4045
|
-
|
|
4046
|
-
parts: [
|
|
4047
|
-
{
|
|
4048
|
-
type: "text",
|
|
4049
|
-
text: [
|
|
4050
|
-
`\u2394 Auto-continue: ${incompleteCount} incomplete todos remaining \u2014 resuming in ${cooldownMs / 1000}s \u2014 Esc\xD72 to cancel`,
|
|
4051
|
-
"",
|
|
4052
|
-
"[system status: continue without acknowledging this notification]"
|
|
4053
|
-
].join(`
|
|
4054
|
-
`)
|
|
4055
|
-
}
|
|
4056
|
-
]
|
|
4057
|
-
}
|
|
4058
|
-
}).catch(() => {});
|
|
4059
|
-
state.pendingTimer = setTimeout(async () => {
|
|
4060
|
-
state.pendingTimer = null;
|
|
4061
|
-
if (!state.enabled) {
|
|
4062
|
-
log(`[${HOOK_NAME}] Cancelled: disabled during cooldown`, {
|
|
4063
|
-
sessionID
|
|
4064
|
-
});
|
|
4065
|
-
return;
|
|
4066
|
-
}
|
|
4067
|
-
state.isAutoInjecting = true;
|
|
4068
|
-
try {
|
|
4069
|
-
await ctx.client.session.prompt({
|
|
4070
|
-
path: { id: sessionID },
|
|
4071
|
-
body: {
|
|
4072
|
-
parts: [createInternalAgentTextPart(CONTINUATION_PROMPT)]
|
|
4073
|
-
}
|
|
4074
|
-
});
|
|
4075
|
-
state.consecutiveContinuations++;
|
|
4076
|
-
log(`[${HOOK_NAME}] Continuation injected`, {
|
|
4077
|
-
sessionID,
|
|
4078
|
-
consecutive: state.consecutiveContinuations
|
|
4079
|
-
});
|
|
4080
|
-
} catch (error) {
|
|
4081
|
-
log(`[${HOOK_NAME}] Error: failed to inject continuation`, {
|
|
4082
|
-
sessionID,
|
|
4083
|
-
error: error instanceof Error ? error.message : String(error)
|
|
4084
|
-
});
|
|
4085
|
-
} finally {
|
|
4086
|
-
state.isAutoInjecting = false;
|
|
4087
|
-
}
|
|
4088
|
-
}, cooldownMs);
|
|
4089
|
-
} else if (event.type === "session.status") {
|
|
4090
|
-
const status = properties.status;
|
|
4091
|
-
const sessionID = properties.sessionID;
|
|
4092
|
-
if (status?.type === "busy") {
|
|
4093
|
-
const isOrchestrator = sessionID === state.orchestratorSessionId;
|
|
4094
|
-
if (isOrchestrator) {
|
|
4095
|
-
cancelPendingTimer(state);
|
|
4096
|
-
}
|
|
4097
|
-
if (!state.isAutoInjecting && isOrchestrator && state.consecutiveContinuations > 0) {
|
|
4098
|
-
state.consecutiveContinuations = 0;
|
|
4099
|
-
log(`[${HOOK_NAME}] Reset consecutive count on user activity`, {
|
|
4100
|
-
sessionID
|
|
4101
|
-
});
|
|
5584
|
+
parts: [createInternalAgentTextPart(prompt)]
|
|
4102
5585
|
}
|
|
4103
|
-
}
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
const isOrchestrator = sessionID === state.orchestratorSessionId;
|
|
4109
|
-
if (isOrchestrator && (errorName === "MessageAbortedError" || errorName === "AbortError")) {
|
|
4110
|
-
state.suppressUntil = Date.now() + SUPPRESS_AFTER_ABORT_MS;
|
|
4111
|
-
log(`[${HOOK_NAME}] Suppressed continuation after abort`, {
|
|
4112
|
-
sessionID,
|
|
4113
|
-
errorName
|
|
4114
|
-
});
|
|
4115
|
-
}
|
|
4116
|
-
if (isOrchestrator) {
|
|
4117
|
-
cancelPendingTimer(state);
|
|
4118
|
-
log(`[${HOOK_NAME}] Cancelled pending timer on error`, {
|
|
4119
|
-
sessionID
|
|
4120
|
-
});
|
|
4121
|
-
}
|
|
4122
|
-
} else if (event.type === "session.deleted") {
|
|
4123
|
-
const deletedSessionId = properties.info?.id ?? properties.sessionID;
|
|
4124
|
-
if (state.orchestratorSessionId === deletedSessionId) {
|
|
4125
|
-
cancelPendingTimer(state);
|
|
4126
|
-
log(`[${HOOK_NAME}] Cancelled pending timer on orchestrator delete`, {
|
|
4127
|
-
sessionID: deletedSessionId
|
|
4128
|
-
});
|
|
4129
|
-
resetState(state);
|
|
4130
|
-
state.orchestratorSessionId = null;
|
|
4131
|
-
log(`[${HOOK_NAME}] Reset orchestrator session on delete`, {
|
|
4132
|
-
sessionID: deletedSessionId
|
|
4133
|
-
});
|
|
5586
|
+
});
|
|
5587
|
+
promptSent = true;
|
|
5588
|
+
} finally {
|
|
5589
|
+
if (!promptSent) {
|
|
5590
|
+
sessionBusy.set(interview.sessionID, false);
|
|
4134
5591
|
}
|
|
4135
5592
|
}
|
|
4136
5593
|
}
|
|
4137
5594
|
async function handleCommandExecuteBefore(input, output) {
|
|
4138
|
-
if (input.command !==
|
|
5595
|
+
if (input.command !== COMMAND_NAME2) {
|
|
4139
5596
|
return;
|
|
4140
5597
|
}
|
|
4141
|
-
|
|
4142
|
-
state.orchestratorSessionId = input.sessionID;
|
|
4143
|
-
}
|
|
5598
|
+
const idea = input.arguments.trim();
|
|
4144
5599
|
output.parts.length = 0;
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
5600
|
+
if (!idea) {
|
|
5601
|
+
const activeId = activeInterviewIds.get(input.sessionID);
|
|
5602
|
+
const interview2 = activeId ? interviewsById.get(activeId) : null;
|
|
5603
|
+
if (!interview2 || interview2.status !== "active") {
|
|
5604
|
+
output.parts.push(createInternalAgentTextPart("The user ran /interview without an idea. Ask them for the product idea in one sentence."));
|
|
5605
|
+
return;
|
|
5606
|
+
}
|
|
5607
|
+
await notifyInterviewUrl(input.sessionID, interview2);
|
|
5608
|
+
output.parts.push(createInternalAgentTextPart(`The interview UI was reopened for the current session. If your latest interview turn already contains unanswered questions, do not repeat them. Otherwise continue the interview with up to ${maxQuestions} clarifying questions and include the structured <interview_state> block.`));
|
|
5609
|
+
return;
|
|
4153
5610
|
}
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
5611
|
+
const resumePath = resolveExistingInterviewPath(ctx.directory, outputFolder, idea);
|
|
5612
|
+
if (resumePath) {
|
|
5613
|
+
const interview2 = await resumeInterview(input.sessionID, resumePath);
|
|
5614
|
+
const document = await fs5.readFile(interview2.markdownPath, "utf8");
|
|
5615
|
+
await notifyInterviewUrl(input.sessionID, interview2);
|
|
5616
|
+
output.parts.push(createInternalAgentTextPart(buildResumePrompt(document, maxQuestions)));
|
|
4160
5617
|
return;
|
|
4161
5618
|
}
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
const
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
error: error instanceof Error ? error.message : String(error)
|
|
4177
|
-
});
|
|
5619
|
+
const interview = await createInterview(input.sessionID, idea);
|
|
5620
|
+
await notifyInterviewUrl(input.sessionID, interview);
|
|
5621
|
+
output.parts.push(createInternalAgentTextPart(buildKickoffPrompt(idea, maxQuestions)));
|
|
5622
|
+
}
|
|
5623
|
+
async function handleEvent(input) {
|
|
5624
|
+
const { event } = input;
|
|
5625
|
+
const properties = event.properties ?? {};
|
|
5626
|
+
if (event.type === "session.status") {
|
|
5627
|
+
const sessionID = properties.sessionID;
|
|
5628
|
+
const status = properties.status;
|
|
5629
|
+
if (sessionID) {
|
|
5630
|
+
sessionBusy.set(sessionID, status?.type === "busy");
|
|
5631
|
+
}
|
|
5632
|
+
return;
|
|
4178
5633
|
}
|
|
4179
|
-
if (
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
5634
|
+
if (event.type === "session.deleted") {
|
|
5635
|
+
const deletedSessionId = (properties.info?.id ?? properties.sessionID) || null;
|
|
5636
|
+
if (!deletedSessionId) {
|
|
5637
|
+
return;
|
|
5638
|
+
}
|
|
5639
|
+
sessionBusy.delete(deletedSessionId);
|
|
5640
|
+
const interviewId = activeInterviewIds.get(deletedSessionId);
|
|
5641
|
+
if (!interviewId) {
|
|
5642
|
+
return;
|
|
5643
|
+
}
|
|
5644
|
+
const interview = interviewsById.get(interviewId);
|
|
5645
|
+
if (!interview) {
|
|
5646
|
+
return;
|
|
5647
|
+
}
|
|
5648
|
+
interview.status = "abandoned";
|
|
5649
|
+
activeInterviewIds.delete(deletedSessionId);
|
|
5650
|
+
log("[interview] session deleted, interview marked abandoned", {
|
|
5651
|
+
sessionID: deletedSessionId,
|
|
5652
|
+
interviewId
|
|
5653
|
+
});
|
|
4183
5654
|
}
|
|
4184
5655
|
}
|
|
4185
5656
|
return {
|
|
4186
|
-
|
|
5657
|
+
setBaseUrlResolver,
|
|
5658
|
+
registerCommand,
|
|
5659
|
+
handleCommandExecuteBefore,
|
|
4187
5660
|
handleEvent,
|
|
4188
|
-
|
|
5661
|
+
getInterviewState,
|
|
5662
|
+
submitAnswers
|
|
5663
|
+
};
|
|
5664
|
+
}
|
|
5665
|
+
|
|
5666
|
+
// src/interview/manager.ts
|
|
5667
|
+
function createInterviewManager(ctx, config) {
|
|
5668
|
+
const service = createInterviewService(ctx, config.interview);
|
|
5669
|
+
const server = createInterviewServer({
|
|
5670
|
+
getState: async (interviewId) => service.getInterviewState(interviewId),
|
|
5671
|
+
submitAnswers: async (interviewId, answers) => service.submitAnswers(interviewId, answers)
|
|
5672
|
+
});
|
|
5673
|
+
service.setBaseUrlResolver(() => server.ensureStarted());
|
|
5674
|
+
return {
|
|
5675
|
+
registerCommand: (config2) => service.registerCommand(config2),
|
|
5676
|
+
handleCommandExecuteBefore: async (input, output) => service.handleCommandExecuteBefore(input, output),
|
|
5677
|
+
handleEvent: async (input) => service.handleEvent(input)
|
|
4189
5678
|
};
|
|
4190
5679
|
}
|
|
4191
5680
|
// src/mcp/context7.ts
|
|
@@ -4248,19 +5737,19 @@ function createBuiltinMcps(disabledMcps = [], websearchConfig) {
|
|
|
4248
5737
|
import { tool as tool2 } from "@opencode-ai/plugin/tool";
|
|
4249
5738
|
|
|
4250
5739
|
// src/tools/ast-grep/cli.ts
|
|
4251
|
-
import { existsSync as
|
|
4252
|
-
var {spawn:
|
|
5740
|
+
import { existsSync as existsSync7 } from "fs";
|
|
5741
|
+
var {spawn: spawn5 } = globalThis.Bun;
|
|
4253
5742
|
|
|
4254
5743
|
// src/tools/ast-grep/constants.ts
|
|
4255
|
-
import { existsSync as
|
|
5744
|
+
import { existsSync as existsSync6, statSync as statSync2 } from "fs";
|
|
4256
5745
|
import { createRequire as createRequire2 } from "module";
|
|
4257
|
-
import { dirname as
|
|
5746
|
+
import { dirname as dirname5, join as join9 } from "path";
|
|
4258
5747
|
|
|
4259
5748
|
// src/tools/ast-grep/downloader.ts
|
|
4260
|
-
import { chmodSync, existsSync as
|
|
5749
|
+
import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync2, unlinkSync } from "fs";
|
|
4261
5750
|
import { createRequire } from "module";
|
|
4262
5751
|
import { homedir as homedir3 } from "os";
|
|
4263
|
-
import { join as
|
|
5752
|
+
import { join as join8 } from "path";
|
|
4264
5753
|
var REPO = "ast-grep/ast-grep";
|
|
4265
5754
|
var DEFAULT_VERSION = "0.40.0";
|
|
4266
5755
|
function getAstGrepVersion() {
|
|
@@ -4284,19 +5773,19 @@ var PLATFORM_MAP = {
|
|
|
4284
5773
|
function getCacheDir2() {
|
|
4285
5774
|
if (process.platform === "win32") {
|
|
4286
5775
|
const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
|
|
4287
|
-
const base2 = localAppData ||
|
|
4288
|
-
return
|
|
5776
|
+
const base2 = localAppData || join8(homedir3(), "AppData", "Local");
|
|
5777
|
+
return join8(base2, "oh-my-opencode-slim", "bin");
|
|
4289
5778
|
}
|
|
4290
5779
|
const xdgCache = process.env.XDG_CACHE_HOME;
|
|
4291
|
-
const base = xdgCache ||
|
|
4292
|
-
return
|
|
5780
|
+
const base = xdgCache || join8(homedir3(), ".cache");
|
|
5781
|
+
return join8(base, "oh-my-opencode-slim", "bin");
|
|
4293
5782
|
}
|
|
4294
5783
|
function getBinaryName() {
|
|
4295
5784
|
return process.platform === "win32" ? "sg.exe" : "sg";
|
|
4296
5785
|
}
|
|
4297
5786
|
function getCachedBinaryPath() {
|
|
4298
|
-
const binaryPath =
|
|
4299
|
-
return
|
|
5787
|
+
const binaryPath = join8(getCacheDir2(), getBinaryName());
|
|
5788
|
+
return existsSync5(binaryPath) ? binaryPath : null;
|
|
4300
5789
|
}
|
|
4301
5790
|
async function downloadAstGrep(version = DEFAULT_VERSION) {
|
|
4302
5791
|
const platformKey = `${process.platform}-${process.arch}`;
|
|
@@ -4307,8 +5796,8 @@ async function downloadAstGrep(version = DEFAULT_VERSION) {
|
|
|
4307
5796
|
}
|
|
4308
5797
|
const cacheDir = getCacheDir2();
|
|
4309
5798
|
const binaryName = getBinaryName();
|
|
4310
|
-
const binaryPath =
|
|
4311
|
-
if (
|
|
5799
|
+
const binaryPath = join8(cacheDir, binaryName);
|
|
5800
|
+
if (existsSync5(binaryPath)) {
|
|
4312
5801
|
return binaryPath;
|
|
4313
5802
|
}
|
|
4314
5803
|
const { arch, os: os3 } = platformInfo;
|
|
@@ -4316,21 +5805,21 @@ async function downloadAstGrep(version = DEFAULT_VERSION) {
|
|
|
4316
5805
|
const downloadUrl = `https://github.com/${REPO}/releases/download/${version}/${assetName}`;
|
|
4317
5806
|
console.log(`[oh-my-opencode-slim] Downloading ast-grep binary...`);
|
|
4318
5807
|
try {
|
|
4319
|
-
if (!
|
|
5808
|
+
if (!existsSync5(cacheDir)) {
|
|
4320
5809
|
mkdirSync2(cacheDir, { recursive: true });
|
|
4321
5810
|
}
|
|
4322
5811
|
const response = await fetch(downloadUrl, { redirect: "follow" });
|
|
4323
5812
|
if (!response.ok) {
|
|
4324
5813
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
4325
5814
|
}
|
|
4326
|
-
const archivePath =
|
|
5815
|
+
const archivePath = join8(cacheDir, assetName);
|
|
4327
5816
|
const arrayBuffer = await response.arrayBuffer();
|
|
4328
5817
|
await Bun.write(archivePath, arrayBuffer);
|
|
4329
5818
|
await extractZip(archivePath, cacheDir);
|
|
4330
|
-
if (
|
|
5819
|
+
if (existsSync5(archivePath)) {
|
|
4331
5820
|
unlinkSync(archivePath);
|
|
4332
5821
|
}
|
|
4333
|
-
if (process.platform !== "win32" &&
|
|
5822
|
+
if (process.platform !== "win32" && existsSync5(binaryPath)) {
|
|
4334
5823
|
chmodSync(binaryPath, 493);
|
|
4335
5824
|
}
|
|
4336
5825
|
console.log(`[oh-my-opencode-slim] ast-grep binary ready.`);
|
|
@@ -4411,9 +5900,9 @@ function findSgCliPathSync() {
|
|
|
4411
5900
|
try {
|
|
4412
5901
|
const require2 = createRequire2(import.meta.url);
|
|
4413
5902
|
const cliPkgPath = require2.resolve("@ast-grep/cli/package.json");
|
|
4414
|
-
const cliDir =
|
|
4415
|
-
const sgPath =
|
|
4416
|
-
if (
|
|
5903
|
+
const cliDir = dirname5(cliPkgPath);
|
|
5904
|
+
const sgPath = join9(cliDir, binaryName);
|
|
5905
|
+
if (existsSync6(sgPath) && isValidBinary(sgPath)) {
|
|
4417
5906
|
return sgPath;
|
|
4418
5907
|
}
|
|
4419
5908
|
} catch {}
|
|
@@ -4422,19 +5911,19 @@ function findSgCliPathSync() {
|
|
|
4422
5911
|
try {
|
|
4423
5912
|
const require2 = createRequire2(import.meta.url);
|
|
4424
5913
|
const pkgPath = require2.resolve(`${platformPkg}/package.json`);
|
|
4425
|
-
const pkgDir =
|
|
5914
|
+
const pkgDir = dirname5(pkgPath);
|
|
4426
5915
|
const astGrepName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
|
|
4427
|
-
const binaryPath =
|
|
4428
|
-
if (
|
|
5916
|
+
const binaryPath = join9(pkgDir, astGrepName);
|
|
5917
|
+
if (existsSync6(binaryPath) && isValidBinary(binaryPath)) {
|
|
4429
5918
|
return binaryPath;
|
|
4430
5919
|
}
|
|
4431
5920
|
} catch {}
|
|
4432
5921
|
}
|
|
4433
5922
|
if (process.platform === "darwin") {
|
|
4434
5923
|
const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"];
|
|
4435
|
-
for (const
|
|
4436
|
-
if (
|
|
4437
|
-
return
|
|
5924
|
+
for (const path7 of homebrewPaths) {
|
|
5925
|
+
if (existsSync6(path7) && isValidBinary(path7)) {
|
|
5926
|
+
return path7;
|
|
4438
5927
|
}
|
|
4439
5928
|
}
|
|
4440
5929
|
}
|
|
@@ -4451,8 +5940,8 @@ function getSgCliPath() {
|
|
|
4451
5940
|
}
|
|
4452
5941
|
return "sg";
|
|
4453
5942
|
}
|
|
4454
|
-
function setSgCliPath(
|
|
4455
|
-
resolvedCliPath =
|
|
5943
|
+
function setSgCliPath(path7) {
|
|
5944
|
+
resolvedCliPath = path7;
|
|
4456
5945
|
}
|
|
4457
5946
|
var DEFAULT_TIMEOUT_MS2 = 300000;
|
|
4458
5947
|
var DEFAULT_MAX_OUTPUT_BYTES = 1 * 1024 * 1024;
|
|
@@ -4462,7 +5951,7 @@ var DEFAULT_MAX_MATCHES = 500;
|
|
|
4462
5951
|
var initPromise = null;
|
|
4463
5952
|
async function getAstGrepPath() {
|
|
4464
5953
|
const currentPath = getSgCliPath();
|
|
4465
|
-
if (currentPath !== "sg" &&
|
|
5954
|
+
if (currentPath !== "sg" && existsSync7(currentPath)) {
|
|
4466
5955
|
return currentPath;
|
|
4467
5956
|
}
|
|
4468
5957
|
if (initPromise) {
|
|
@@ -4470,7 +5959,7 @@ async function getAstGrepPath() {
|
|
|
4470
5959
|
}
|
|
4471
5960
|
initPromise = (async () => {
|
|
4472
5961
|
const syncPath = findSgCliPathSync();
|
|
4473
|
-
if (syncPath &&
|
|
5962
|
+
if (syncPath && existsSync7(syncPath)) {
|
|
4474
5963
|
setSgCliPath(syncPath);
|
|
4475
5964
|
return syncPath;
|
|
4476
5965
|
}
|
|
@@ -4509,14 +5998,14 @@ async function runSg(options) {
|
|
|
4509
5998
|
const paths2 = options.paths && options.paths.length > 0 ? options.paths : ["."];
|
|
4510
5999
|
args.push(...paths2);
|
|
4511
6000
|
let cliPath = getSgCliPath();
|
|
4512
|
-
if (!
|
|
6001
|
+
if (!existsSync7(cliPath) && cliPath !== "sg") {
|
|
4513
6002
|
const downloadedPath = await getAstGrepPath();
|
|
4514
6003
|
if (downloadedPath) {
|
|
4515
6004
|
cliPath = downloadedPath;
|
|
4516
6005
|
}
|
|
4517
6006
|
}
|
|
4518
6007
|
const timeout = DEFAULT_TIMEOUT_MS2;
|
|
4519
|
-
const proc =
|
|
6008
|
+
const proc = spawn5([cliPath, ...args], {
|
|
4520
6009
|
stdout: "pipe",
|
|
4521
6010
|
stderr: "pipe"
|
|
4522
6011
|
});
|
|
@@ -4971,10 +6460,10 @@ Returns the synthesized result with councillor summary.`,
|
|
|
4971
6460
|
}
|
|
4972
6461
|
// src/tools/lsp/client.ts
|
|
4973
6462
|
import { readFileSync as readFileSync4 } from "fs";
|
|
4974
|
-
import { extname, resolve as
|
|
6463
|
+
import { extname as extname2, resolve as resolve4 } from "path";
|
|
4975
6464
|
import { Readable, Writable } from "stream";
|
|
4976
6465
|
import { pathToFileURL } from "url";
|
|
4977
|
-
var {spawn:
|
|
6466
|
+
var {spawn: spawn6 } = globalThis.Bun;
|
|
4978
6467
|
import {
|
|
4979
6468
|
createMessageConnection,
|
|
4980
6469
|
StreamMessageReader,
|
|
@@ -4982,9 +6471,9 @@ import {
|
|
|
4982
6471
|
} from "vscode-jsonrpc/node";
|
|
4983
6472
|
|
|
4984
6473
|
// src/tools/lsp/config.ts
|
|
4985
|
-
import { existsSync as
|
|
6474
|
+
import { existsSync as existsSync9 } from "fs";
|
|
4986
6475
|
import { homedir as homedir4 } from "os";
|
|
4987
|
-
import { dirname as
|
|
6476
|
+
import { dirname as dirname7, join as join10, resolve as resolve3 } from "path";
|
|
4988
6477
|
import whichSync from "which";
|
|
4989
6478
|
|
|
4990
6479
|
// src/tools/lsp/config-store.ts
|
|
@@ -5015,8 +6504,8 @@ function hasUserLspConfig() {
|
|
|
5015
6504
|
}
|
|
5016
6505
|
|
|
5017
6506
|
// src/tools/lsp/constants.ts
|
|
5018
|
-
import { existsSync as
|
|
5019
|
-
import { dirname as
|
|
6507
|
+
import { existsSync as existsSync8, readdirSync, statSync as statSync3 } from "fs";
|
|
6508
|
+
import { dirname as dirname6, resolve as resolve2 } from "path";
|
|
5020
6509
|
var SEVERITY_MAP = {
|
|
5021
6510
|
1: "error",
|
|
5022
6511
|
2: "warning",
|
|
@@ -5033,13 +6522,13 @@ var LOCK_FILE_PATTERNS = [
|
|
|
5033
6522
|
"yarn.lock"
|
|
5034
6523
|
];
|
|
5035
6524
|
function* walkUpDirectories(start, stop) {
|
|
5036
|
-
let dir =
|
|
6525
|
+
let dir = resolve2(start);
|
|
5037
6526
|
try {
|
|
5038
6527
|
if (!statSync3(dir).isDirectory()) {
|
|
5039
|
-
dir =
|
|
6528
|
+
dir = dirname6(dir);
|
|
5040
6529
|
}
|
|
5041
6530
|
} catch {
|
|
5042
|
-
dir =
|
|
6531
|
+
dir = dirname6(dir);
|
|
5043
6532
|
}
|
|
5044
6533
|
let prevDir = "";
|
|
5045
6534
|
while (dir !== prevDir && dir !== "/") {
|
|
@@ -5047,7 +6536,7 @@ function* walkUpDirectories(start, stop) {
|
|
|
5047
6536
|
prevDir = dir;
|
|
5048
6537
|
if (dir === stop)
|
|
5049
6538
|
break;
|
|
5050
|
-
dir =
|
|
6539
|
+
dir = dirname6(dir);
|
|
5051
6540
|
}
|
|
5052
6541
|
}
|
|
5053
6542
|
function NearestRoot(includePatterns, excludePatterns) {
|
|
@@ -5056,7 +6545,7 @@ function NearestRoot(includePatterns, excludePatterns) {
|
|
|
5056
6545
|
if (excludePatterns) {
|
|
5057
6546
|
for (const dir of walkUpDirectories(file, cwd)) {
|
|
5058
6547
|
for (const pattern of excludePatterns) {
|
|
5059
|
-
if (
|
|
6548
|
+
if (existsSync8(`${dir}/${pattern}`)) {
|
|
5060
6549
|
return;
|
|
5061
6550
|
}
|
|
5062
6551
|
}
|
|
@@ -5072,7 +6561,7 @@ function NearestRoot(includePatterns, excludePatterns) {
|
|
|
5072
6561
|
return dir;
|
|
5073
6562
|
}
|
|
5074
6563
|
} catch {}
|
|
5075
|
-
} else if (
|
|
6564
|
+
} else if (existsSync8(`${dir}/${pattern}`)) {
|
|
5076
6565
|
return dir;
|
|
5077
6566
|
}
|
|
5078
6567
|
}
|
|
@@ -5593,7 +7082,7 @@ function getServerWorkspace(config, filePath) {
|
|
|
5593
7082
|
return;
|
|
5594
7083
|
}
|
|
5595
7084
|
if (!config.root) {
|
|
5596
|
-
return
|
|
7085
|
+
return dirname7(resolve3(filePath));
|
|
5597
7086
|
}
|
|
5598
7087
|
return config.root(filePath);
|
|
5599
7088
|
}
|
|
@@ -5617,7 +7106,7 @@ function findInstalledServer(configs, filePath) {
|
|
|
5617
7106
|
let firstNotInstalled = null;
|
|
5618
7107
|
for (const config of configs) {
|
|
5619
7108
|
const workspace = getServerWorkspace(config, filePath);
|
|
5620
|
-
const resolvedCommand = resolveServerCommand(config.command, workspace ?? (filePath ?
|
|
7109
|
+
const resolvedCommand = resolveServerCommand(config.command, workspace ?? (filePath ? dirname7(resolve3(filePath)) : undefined));
|
|
5621
7110
|
const server = toResolvedServer(config, resolvedCommand ?? undefined);
|
|
5622
7111
|
log(`[LSP] Considering server for ${config.extensions.join(", ")}: ${config.id} with command ${config.command.join(" ")}`);
|
|
5623
7112
|
if (resolvedCommand) {
|
|
@@ -5660,11 +7149,11 @@ function resolveServerCommand(command, cwd) {
|
|
|
5660
7149
|
return null;
|
|
5661
7150
|
const [cmd, ...args] = command;
|
|
5662
7151
|
if (cmd.includes("/") || cmd.includes("\\")) {
|
|
5663
|
-
return
|
|
7152
|
+
return existsSync9(cmd) ? command : null;
|
|
5664
7153
|
}
|
|
5665
7154
|
const isWindows = process.platform === "win32";
|
|
5666
7155
|
const ext = isWindows ? ".exe" : "";
|
|
5667
|
-
const opencodeBin =
|
|
7156
|
+
const opencodeBin = join10(homedir4(), ".config", "opencode", "bin");
|
|
5668
7157
|
const searchPath = (process.env.PATH ?? "") + (isWindows ? ";" : ":") + opencodeBin;
|
|
5669
7158
|
const result = whichSync.sync(cmd, {
|
|
5670
7159
|
path: searchPath,
|
|
@@ -5675,11 +7164,11 @@ function resolveServerCommand(command, cwd) {
|
|
|
5675
7164
|
return [result, ...args];
|
|
5676
7165
|
}
|
|
5677
7166
|
const localBinRoot = cwd ?? process.cwd();
|
|
5678
|
-
const localBin =
|
|
5679
|
-
if (
|
|
7167
|
+
const localBin = join10(localBinRoot, "node_modules", ".bin", cmd);
|
|
7168
|
+
if (existsSync9(localBin)) {
|
|
5680
7169
|
return [localBin, ...args];
|
|
5681
7170
|
}
|
|
5682
|
-
if (
|
|
7171
|
+
if (existsSync9(localBin + ext)) {
|
|
5683
7172
|
return [localBin + ext, ...args];
|
|
5684
7173
|
}
|
|
5685
7174
|
return null;
|
|
@@ -5724,7 +7213,7 @@ function getDiagnosticsCapabilitySummary({
|
|
|
5724
7213
|
};
|
|
5725
7214
|
}
|
|
5726
7215
|
function withTimeout(promise, ms, label, onTimeout) {
|
|
5727
|
-
return new Promise((
|
|
7216
|
+
return new Promise((resolve5, reject) => {
|
|
5728
7217
|
let settled = false;
|
|
5729
7218
|
const timer = setTimeout(() => {
|
|
5730
7219
|
if (settled) {
|
|
@@ -5740,7 +7229,7 @@ function withTimeout(promise, ms, label, onTimeout) {
|
|
|
5740
7229
|
}
|
|
5741
7230
|
settled = true;
|
|
5742
7231
|
clearTimeout(timer);
|
|
5743
|
-
|
|
7232
|
+
resolve5(value);
|
|
5744
7233
|
}, (error) => {
|
|
5745
7234
|
if (settled) {
|
|
5746
7235
|
return;
|
|
@@ -5944,7 +7433,7 @@ class LSPClient {
|
|
|
5944
7433
|
command: command.join(" "),
|
|
5945
7434
|
root: this.root
|
|
5946
7435
|
});
|
|
5947
|
-
this.proc =
|
|
7436
|
+
this.proc = spawn6(command, {
|
|
5948
7437
|
stdin: "pipe",
|
|
5949
7438
|
stdout: "pipe",
|
|
5950
7439
|
stderr: "pipe",
|
|
@@ -6030,7 +7519,7 @@ class LSPClient {
|
|
|
6030
7519
|
this.processExited = true;
|
|
6031
7520
|
});
|
|
6032
7521
|
this.connection.listen();
|
|
6033
|
-
await new Promise((
|
|
7522
|
+
await new Promise((resolve5) => setTimeout(resolve5, 100));
|
|
6034
7523
|
if (this.proc.exitCode !== null) {
|
|
6035
7524
|
const stderr = this.stderrBuffer.join(`
|
|
6036
7525
|
`);
|
|
@@ -6137,10 +7626,10 @@ stderr: ${stderr}` : ""));
|
|
|
6137
7626
|
await this.ensureDocumentSynced(filePath);
|
|
6138
7627
|
}
|
|
6139
7628
|
async ensureDocumentSynced(filePath) {
|
|
6140
|
-
const absPath =
|
|
7629
|
+
const absPath = resolve4(filePath);
|
|
6141
7630
|
const uri = pathToFileURL(absPath).href;
|
|
6142
7631
|
const text = readFileSync4(absPath, "utf-8");
|
|
6143
|
-
const ext =
|
|
7632
|
+
const ext = extname2(absPath);
|
|
6144
7633
|
const languageId = getLanguageId(ext);
|
|
6145
7634
|
const existing = this.documents.get(uri);
|
|
6146
7635
|
if (!existing) {
|
|
@@ -6179,7 +7668,7 @@ stderr: ${stderr}` : ""));
|
|
|
6179
7668
|
}
|
|
6180
7669
|
}
|
|
6181
7670
|
async definition(filePath, line, character) {
|
|
6182
|
-
const absPath =
|
|
7671
|
+
const absPath = resolve4(filePath);
|
|
6183
7672
|
await this.openFile(absPath);
|
|
6184
7673
|
return this.connection ? withTimeout(this.connection.sendRequest("textDocument/definition", {
|
|
6185
7674
|
textDocument: { uri: pathToFileURL(absPath).href },
|
|
@@ -6187,7 +7676,7 @@ stderr: ${stderr}` : ""));
|
|
|
6187
7676
|
}), LSP_TIMEOUTS.request, `LSP definition (${this.server.id})`) : undefined;
|
|
6188
7677
|
}
|
|
6189
7678
|
async references(filePath, line, character, includeDeclaration = true) {
|
|
6190
|
-
const absPath =
|
|
7679
|
+
const absPath = resolve4(filePath);
|
|
6191
7680
|
await this.openFile(absPath);
|
|
6192
7681
|
return this.connection ? withTimeout(this.connection.sendRequest("textDocument/references", {
|
|
6193
7682
|
textDocument: { uri: pathToFileURL(absPath).href },
|
|
@@ -6196,7 +7685,7 @@ stderr: ${stderr}` : ""));
|
|
|
6196
7685
|
}), LSP_TIMEOUTS.request, `LSP references (${this.server.id})`) : undefined;
|
|
6197
7686
|
}
|
|
6198
7687
|
async diagnostics(filePath) {
|
|
6199
|
-
const absPath =
|
|
7688
|
+
const absPath = resolve4(filePath);
|
|
6200
7689
|
const uri = pathToFileURL(absPath).href;
|
|
6201
7690
|
await this.openFile(absPath);
|
|
6202
7691
|
await new Promise((r) => setTimeout(r, LSP_TIMEOUTS.diagnosticSettleDelay));
|
|
@@ -6251,7 +7740,7 @@ stderr: ${stderr}` : ""));
|
|
|
6251
7740
|
throw new Error(`Unable to retrieve diagnostics from ${this.server.id}: request timed out or is unsupported.`);
|
|
6252
7741
|
}
|
|
6253
7742
|
async rename(filePath, line, character, newName) {
|
|
6254
|
-
const absPath =
|
|
7743
|
+
const absPath = resolve4(filePath);
|
|
6255
7744
|
await this.openFile(absPath);
|
|
6256
7745
|
return this.connection ? withTimeout(this.connection.sendRequest("textDocument/rename", {
|
|
6257
7746
|
textDocument: { uri: pathToFileURL(absPath).href },
|
|
@@ -6290,19 +7779,19 @@ import { tool as tool5 } from "@opencode-ai/plugin/tool";
|
|
|
6290
7779
|
|
|
6291
7780
|
// src/tools/lsp/utils.ts
|
|
6292
7781
|
import {
|
|
6293
|
-
existsSync as
|
|
7782
|
+
existsSync as existsSync10,
|
|
6294
7783
|
readFileSync as readFileSync5,
|
|
6295
7784
|
statSync as statSync4,
|
|
6296
7785
|
unlinkSync as unlinkSync2,
|
|
6297
7786
|
writeFileSync as writeFileSync3
|
|
6298
7787
|
} from "fs";
|
|
6299
|
-
import { dirname as
|
|
7788
|
+
import { dirname as dirname8, extname as extname3, join as join11, resolve as resolve5 } from "path";
|
|
6300
7789
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6301
7790
|
function findServerProjectRoot(filePath, server) {
|
|
6302
7791
|
if (server.root) {
|
|
6303
|
-
return server.root(filePath) ??
|
|
7792
|
+
return server.root(filePath) ?? dirname8(resolve5(filePath));
|
|
6304
7793
|
}
|
|
6305
|
-
return
|
|
7794
|
+
return dirname8(resolve5(filePath));
|
|
6306
7795
|
}
|
|
6307
7796
|
function uriToPath(uri) {
|
|
6308
7797
|
return fileURLToPath2(uri);
|
|
@@ -6321,8 +7810,8 @@ function formatServerLookupError(result) {
|
|
|
6321
7810
|
return `No LSP server configured for extension: ${result.extension}`;
|
|
6322
7811
|
}
|
|
6323
7812
|
async function withLspClient(filePath, fn) {
|
|
6324
|
-
const absPath =
|
|
6325
|
-
const ext =
|
|
7813
|
+
const absPath = resolve5(filePath);
|
|
7814
|
+
const ext = extname3(absPath);
|
|
6326
7815
|
const result = findServerForExtension(ext, absPath);
|
|
6327
7816
|
if (result.status !== "found") {
|
|
6328
7817
|
log("[lsp] withLspClient: server not found", {
|
|
@@ -6332,7 +7821,7 @@ async function withLspClient(filePath, fn) {
|
|
|
6332
7821
|
throw new Error(formatServerLookupError(result));
|
|
6333
7822
|
}
|
|
6334
7823
|
const server = result.server;
|
|
6335
|
-
const root = findServerProjectRoot(absPath, server) ??
|
|
7824
|
+
const root = findServerProjectRoot(absPath, server) ?? dirname8(absPath);
|
|
6336
7825
|
log("[lsp] withLspClient: selected server", {
|
|
6337
7826
|
filePath: absPath,
|
|
6338
7827
|
extension: ext,
|
|
@@ -6695,14 +8184,14 @@ var BINARY_PREFIXES = [
|
|
|
6695
8184
|
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.";
|
|
6696
8185
|
// src/tools/smartfetch/tool.ts
|
|
6697
8186
|
import os3 from "os";
|
|
6698
|
-
import
|
|
8187
|
+
import path10 from "path";
|
|
6699
8188
|
import {
|
|
6700
8189
|
tool as tool6
|
|
6701
8190
|
} from "@opencode-ai/plugin";
|
|
6702
8191
|
|
|
6703
8192
|
// src/tools/smartfetch/binary.ts
|
|
6704
|
-
import { mkdir, writeFile } from "fs/promises";
|
|
6705
|
-
import
|
|
8193
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
8194
|
+
import path7 from "path";
|
|
6706
8195
|
function extensionForMime(contentType) {
|
|
6707
8196
|
const mime = contentType.split(";")[0]?.trim().toLowerCase();
|
|
6708
8197
|
const map = {
|
|
@@ -6721,14 +8210,14 @@ function buildBinaryResultMessage(fetchResult, savedPath) {
|
|
|
6721
8210
|
return `${subject} content omitted because it exceeds the download limit.`;
|
|
6722
8211
|
}
|
|
6723
8212
|
async function saveBinary(binaryDir, data, contentType, filename) {
|
|
6724
|
-
await
|
|
8213
|
+
await mkdir2(binaryDir, { recursive: true });
|
|
6725
8214
|
const initialName = filename || `webfetch-${Date.now()}.${extensionForMime(contentType)}`;
|
|
6726
|
-
const parsed =
|
|
8215
|
+
const parsed = path7.parse(initialName);
|
|
6727
8216
|
for (let attempt = 0;attempt < 1000; attempt++) {
|
|
6728
8217
|
const candidateName = attempt === 0 ? initialName : `${parsed.name}-${attempt}${parsed.ext || `.${extensionForMime(contentType)}`}`;
|
|
6729
|
-
const file =
|
|
8218
|
+
const file = path7.join(binaryDir, candidateName);
|
|
6730
8219
|
try {
|
|
6731
|
-
await
|
|
8220
|
+
await writeFile2(file, data, { flag: "wx" });
|
|
6732
8221
|
return file;
|
|
6733
8222
|
} catch (error) {
|
|
6734
8223
|
if (typeof error === "object" && error && "code" in error && error.code === "EEXIST") {
|
|
@@ -6744,7 +8233,7 @@ async function saveBinary(binaryDir, data, contentType, filename) {
|
|
|
6744
8233
|
import { LRUCache } from "lru-cache";
|
|
6745
8234
|
|
|
6746
8235
|
// src/tools/smartfetch/network.ts
|
|
6747
|
-
import
|
|
8236
|
+
import path8 from "path";
|
|
6748
8237
|
|
|
6749
8238
|
// src/tools/smartfetch/utils.ts
|
|
6750
8239
|
import { Readability } from "@mozilla/readability";
|
|
@@ -7463,7 +8952,7 @@ function inferFilenameFromUrl(url) {
|
|
|
7463
8952
|
function truncateFilename(name, maxLength = 180) {
|
|
7464
8953
|
if (name.length <= maxLength)
|
|
7465
8954
|
return name;
|
|
7466
|
-
const parsed =
|
|
8955
|
+
const parsed = path8.parse(name);
|
|
7467
8956
|
const ext = parsed.ext || "";
|
|
7468
8957
|
const baseLimit = Math.max(1, maxLength - ext.length);
|
|
7469
8958
|
return `${parsed.name.slice(0, baseLimit)}${ext}`;
|
|
@@ -7633,9 +9122,9 @@ function isInvalidLlmsResult(fetchResult) {
|
|
|
7633
9122
|
}
|
|
7634
9123
|
|
|
7635
9124
|
// src/tools/smartfetch/secondary-model.ts
|
|
7636
|
-
import { existsSync as
|
|
7637
|
-
import { readFile } from "fs/promises";
|
|
7638
|
-
import
|
|
9125
|
+
import { existsSync as existsSync11 } from "fs";
|
|
9126
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
9127
|
+
import path9 from "path";
|
|
7639
9128
|
function parseModelRef(value) {
|
|
7640
9129
|
if (!value)
|
|
7641
9130
|
return;
|
|
@@ -7661,8 +9150,8 @@ function pickAgentModelRef(value) {
|
|
|
7661
9150
|
}
|
|
7662
9151
|
function findPreferredOpenCodeConfigPath(baseDir) {
|
|
7663
9152
|
for (const file of ["opencode.jsonc", "opencode.json"]) {
|
|
7664
|
-
const fullPath =
|
|
7665
|
-
if (
|
|
9153
|
+
const fullPath = path9.join(baseDir, file);
|
|
9154
|
+
if (existsSync11(fullPath))
|
|
7666
9155
|
return fullPath;
|
|
7667
9156
|
}
|
|
7668
9157
|
return;
|
|
@@ -7671,14 +9160,14 @@ async function readOpenCodeConfigFile(configPath) {
|
|
|
7671
9160
|
if (!configPath)
|
|
7672
9161
|
return;
|
|
7673
9162
|
try {
|
|
7674
|
-
const content = await
|
|
9163
|
+
const content = await readFile2(configPath, "utf8");
|
|
7675
9164
|
return JSON.parse(stripJsonComments(content));
|
|
7676
9165
|
} catch {
|
|
7677
9166
|
return;
|
|
7678
9167
|
}
|
|
7679
9168
|
}
|
|
7680
9169
|
async function readEffectiveOpenCodeConfig(directory) {
|
|
7681
|
-
const projectDir =
|
|
9170
|
+
const projectDir = path9.join(directory, ".opencode");
|
|
7682
9171
|
const userDirs = getConfigSearchDirs();
|
|
7683
9172
|
const projectPath = findPreferredOpenCodeConfigPath(projectDir);
|
|
7684
9173
|
const userPath = userDirs.map((configDir) => findPreferredOpenCodeConfigPath(configDir)).find(Boolean);
|
|
@@ -7839,7 +9328,7 @@ async function runSecondaryModelWithFallback(client, directory, models, prompt,
|
|
|
7839
9328
|
// src/tools/smartfetch/tool.ts
|
|
7840
9329
|
var z5 = tool6.schema;
|
|
7841
9330
|
function createWebfetchTool(pluginCtx, options = {}) {
|
|
7842
|
-
const binaryDir = options.binaryDir ||
|
|
9331
|
+
const binaryDir = options.binaryDir || path10.join(os3.tmpdir(), "opencode-smartfetch");
|
|
7843
9332
|
return tool6({
|
|
7844
9333
|
description: WEBFETCH_DESCRIPTION,
|
|
7845
9334
|
args: {
|
|
@@ -8385,12 +9874,14 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
8385
9874
|
const delegateTaskRetryHook = createDelegateTaskRetryHook(ctx);
|
|
8386
9875
|
const jsonErrorRecoveryHook = createJsonErrorRecoveryHook(ctx);
|
|
8387
9876
|
const foregroundFallback = new ForegroundFallbackManager(ctx.client, runtimeChains, config.fallback?.enabled !== false && Object.keys(runtimeChains).length > 0);
|
|
9877
|
+
const sessionAgentMap = new Map;
|
|
8388
9878
|
const todoContinuationHook = createTodoContinuationHook(ctx, {
|
|
8389
9879
|
maxContinuations: config.todoContinuation?.maxContinuations ?? 5,
|
|
8390
9880
|
cooldownMs: config.todoContinuation?.cooldownMs ?? 3000,
|
|
8391
9881
|
autoEnable: config.todoContinuation?.autoEnable ?? false,
|
|
8392
9882
|
autoEnableThreshold: config.todoContinuation?.autoEnableThreshold ?? 4
|
|
8393
9883
|
});
|
|
9884
|
+
const interviewManager = createInterviewManager(ctx, config);
|
|
8394
9885
|
return {
|
|
8395
9886
|
name: "oh-my-opencode-slim",
|
|
8396
9887
|
agent: agents,
|
|
@@ -8510,6 +10001,7 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
8510
10001
|
description: "Enable auto-continuation \u2014 orchestrator keeps working through incomplete todos"
|
|
8511
10002
|
};
|
|
8512
10003
|
}
|
|
10004
|
+
interviewManager.registerCommand(opencodeConfig);
|
|
8513
10005
|
},
|
|
8514
10006
|
event: async (input) => {
|
|
8515
10007
|
await foregroundFallback.handleEvent(input.event);
|
|
@@ -8520,11 +10012,30 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
8520
10012
|
await multiplexerSessionManager.onSessionStatus(input.event);
|
|
8521
10013
|
await backgroundManager.handleSessionDeleted(input.event);
|
|
8522
10014
|
await multiplexerSessionManager.onSessionDeleted(input.event);
|
|
10015
|
+
await interviewManager.handleEvent(input);
|
|
8523
10016
|
},
|
|
8524
10017
|
"command.execute.before": async (input, output) => {
|
|
8525
10018
|
await todoContinuationHook.handleCommandExecuteBefore(input, output);
|
|
10019
|
+
await interviewManager.handleCommandExecuteBefore(input, output);
|
|
8526
10020
|
},
|
|
8527
10021
|
"chat.headers": chatHeadersHook["chat.headers"],
|
|
10022
|
+
"chat.message": async (input) => {
|
|
10023
|
+
if (input.agent) {
|
|
10024
|
+
sessionAgentMap.set(input.sessionID, input.agent);
|
|
10025
|
+
}
|
|
10026
|
+
},
|
|
10027
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
10028
|
+
const agentName = input.sessionID ? sessionAgentMap.get(input.sessionID) : undefined;
|
|
10029
|
+
if (agentName === "orchestrator") {
|
|
10030
|
+
const alreadyInjected = output.system.some((s) => typeof s === "string" && s.includes("<Role>") && s.includes("orchestrator"));
|
|
10031
|
+
if (!alreadyInjected) {
|
|
10032
|
+
const { ORCHESTRATOR_PROMPT: ORCHESTRATOR_PROMPT2 } = await Promise.resolve().then(() => exports_orchestrator);
|
|
10033
|
+
output.system[0] = ORCHESTRATOR_PROMPT2 + (output.system[0] ? `
|
|
10034
|
+
|
|
10035
|
+
` + output.system[0] : "");
|
|
10036
|
+
}
|
|
10037
|
+
}
|
|
10038
|
+
},
|
|
8528
10039
|
"experimental.chat.messages.transform": async (input, output) => {
|
|
8529
10040
|
const typedOutput = output;
|
|
8530
10041
|
await phaseReminderHook["experimental.chat.messages.transform"](input, typedOutput);
|