@rockysoft/opencode-hybrid-workflow 1.0.1 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/THIRD_PARTY_NOTICES.md +5 -0
- package/assets/integrations/impeccable/LICENSE +191 -0
- package/assets/integrations/impeccable/NOTICE.md +11 -0
- package/assets/integrations/impeccable/colorize.md +86 -0
- package/assets/integrations/impeccable/craft-floor.md +44 -0
- package/assets/integrations/impeccable/layout.md +84 -0
- package/assets/integrations/impeccable/polish.md +105 -0
- package/assets/integrations/impeccable/provenance.json +8 -0
- package/assets/integrations/impeccable/typeset.md +90 -0
- package/core/controller.mjs +1 -1
- package/core/design.mjs +5 -1
- package/core/plugin.mjs +6 -0
- package/docs/CHANGELOG.md +13 -0
- package/docs/images/22-design-guidance.png +0 -0
- package/docs/images/23-impeccable-saved.png +0 -0
- package/docs/impeccable.md +22 -0
- package/lib/config.mjs +1 -0
- package/lib/continuation-notice.mjs +5 -0
- package/lib/continuation.mjs +85 -0
- package/lib/impeccable.mjs +36 -0
- package/lib/native-result.mjs +31 -0
- package/lib/opencode-reviewer.mjs +3 -7
- package/lib/settings-rpc.mjs +1 -0
- package/lib/settings-service.mjs +8 -1
- package/package.json +1 -1
- package/sidebar/reader.mjs +6 -1
- package/sidebar/settings.mjs +5 -0
- package/tui.js +86 -70
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Typography carries information, hierarchy, and voice. Improve it inside the established visual world; do not replace the identity unless the user asked to.
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## Visitor mode
|
|
6
|
+
|
|
7
|
+
- **Persuade + Experience:** display type may carry the voice. Use decisive contrast and responsive scale when the composition benefits.
|
|
8
|
+
- **Operate + Read:** stability, scanability, and measure come first. A single well-tuned family and fixed role scale are often right.
|
|
9
|
+
- **Native:** follow [ios.md](ios.md) or [android.md](android.md), including platform scaling and accessibility behavior.
|
|
10
|
+
|
|
11
|
+
If typography replacement would create a new identity, route through [new-work.md](new-work.md) and update DESIGN.md. Otherwise preserve confirmed families and improve their use.
|
|
12
|
+
|
|
13
|
+
## Two isolated assessments
|
|
14
|
+
|
|
15
|
+
When a sub-agent tool is available and permitted, run these independently; otherwise run them yourself in this order. Do not let detector findings anchor the design assessment.
|
|
16
|
+
|
|
17
|
+
1. **Typographic assessment:** inspect representative pages and styles. Answer every question below with a file, selector, or computed value:
|
|
18
|
+
- **Authority and fit:** Which faces, weights, and roles are established? Do they fit the product and selected world, or are they unexamined defaults? Is every family necessary?
|
|
19
|
+
- **Hierarchy:** Can heading, body, label, metadata, and data roles be distinguished at a glance? Are adjacent sizes or weights too close to carry different jobs?
|
|
20
|
+
- **Scale and consistency:** Is there a deliberate role scale, or a collection of arbitrary values? Do repeated roles stay identical across screens and states?
|
|
21
|
+
- **Reading:** Does body copy stay within a comfortable 45–75 character measure? Are line height, paragraph rhythm, contrast, and tracking tuned to the actual face, width, language, and surface?
|
|
22
|
+
- **Stress:** What happens with long headings, localization expansion, zoom, narrow containers, missing weights, and font fallback?
|
|
23
|
+
- **Delivery:** Are only used assets loaded? Do fallback metrics, loading strategy, and variable-font settings avoid invisible text and disruptive reflow?
|
|
24
|
+
2. **Mechanical scan:** run:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
.opencode/skills/impeccable/scripts/impeccable detect --json --scope type [target files or dirs]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Also inspect dynamic or arbitrary font values the detector cannot interpret. Synthesize both assessments before editing, noting what each caught alone. A clean scan is a floor, not proof of good typography.
|
|
31
|
+
|
|
32
|
+
## Set the system
|
|
33
|
+
|
|
34
|
+
Before editing, state:
|
|
35
|
+
|
|
36
|
+
- the roles the interface needs;
|
|
37
|
+
- the intended contrast between those roles;
|
|
38
|
+
- the reading measure and density;
|
|
39
|
+
- which existing faces and weights are authoritative;
|
|
40
|
+
- any performance, localization, or accessibility constraints.
|
|
41
|
+
|
|
42
|
+
Use the fewest roles and families that make the hierarchy unmistakable. Combine size, weight, space, and tone deliberately instead of asking size alone to do all the work. Role names and tokens should describe purpose rather than values.
|
|
43
|
+
|
|
44
|
+
## Apply
|
|
45
|
+
|
|
46
|
+
- Keep body copy comfortably readable and zoomable. Use 1rem / 16px as the ordinary web body floor unless a dense role, platform convention, or user setting justifies otherwise.
|
|
47
|
+
- Keep prose in the 45–75ch range. Tune line height inversely with measure: wider lines generally need more leading.
|
|
48
|
+
- Compensate light text on dark surfaces on all three perceptual axes: slightly more line height, a touch more tracking, and one step more weight when the face needs it.
|
|
49
|
+
- Tune line height to the face, width, language, and contrast, not a universal ratio.
|
|
50
|
+
- Keep repeated roles consistent across screens and states.
|
|
51
|
+
- Use numeric, tabular, code, and label features when their content benefits.
|
|
52
|
+
- Load only used font assets and weights. Provide metric-compatible fallbacks and avoid blocking text.
|
|
53
|
+
- Let marketing display type respond to available space when useful; keep dense product and reading surfaces spatially predictable.
|
|
54
|
+
- Preserve browser zoom, user font settings, Dynamic Type, and platform text scaling.
|
|
55
|
+
- Use paragraph spacing or first-line indentation as the primary paragraph rhythm; combining both usually double-marks the boundary.
|
|
56
|
+
|
|
57
|
+
Do not make type decorative at the expense of comprehension, or introduce a second family without a clear role it alone can perform.
|
|
58
|
+
|
|
59
|
+
## Line breaks
|
|
60
|
+
|
|
61
|
+
After Matthew Butterick's *Practical Typography*, paraphrased; where his numbers differ from these, these stand.
|
|
62
|
+
|
|
63
|
+
- Set `text-wrap: balance` on headings and short centered blocks, and `text-wrap: pretty` on body copy. Both are progressive: a browser that ignores them keeps its default wrap.
|
|
64
|
+
- `pretty` pulls a word down beside a lone last word. It does not fill the last line, and a two-word tail is fine.
|
|
65
|
+
- In a column too narrow for either to help, edit the copy or widen the column.
|
|
66
|
+
- Break a heading by hand only between phrases, and check the break at every width the heading renders at. A hard break that lands mid-line on a phone is worse than the default wrap.
|
|
67
|
+
- An uneven break can carry meaning: a headline that lands on one word for weight, a poster line, a pull quote. Make it deliberate with a hard break or a wrap set on that element, and keep it rare. The detector's `straggler` and `unbalanced-heading` findings are advisory for this reason; say which breaks are intended.
|
|
68
|
+
|
|
69
|
+
## Verify
|
|
70
|
+
|
|
71
|
+
- Primary, secondary, body, and metadata roles are recognizable without reading the copy.
|
|
72
|
+
- Long text remains comfortable across relevant widths and languages.
|
|
73
|
+
- The typography belongs to the product and its established world.
|
|
74
|
+
- Loading does not create disruptive reflow or invisible text.
|
|
75
|
+
- Zoom, text scaling, focus, contrast, and reduced viewport paths remain usable.
|
|
76
|
+
- The final mechanical scan has no unexplained findings.
|
|
77
|
+
|
|
78
|
+
Answer each item with rendered or source evidence, then rerun the scan. Do not substitute a bare “yes” for verification.
|
|
79
|
+
|
|
80
|
+
When the hierarchy holds, hand off to `/impeccable polish`.
|
|
81
|
+
|
|
82
|
+
## Live-mode signature params
|
|
83
|
+
|
|
84
|
+
Every variant declares a coarse `scale` parameter and authors its type ramp against `var(--p-scale, 1)`.
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{"id":"scale","kind":"range","min":0.85,"max":1.3,"step":0.05,"default":1,"label":"Scale"}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Add at most one pairing or weight parameter when it represents a real system choice. Follow [live.md](live.md)'s parameter contract.
|
package/core/controller.mjs
CHANGED
|
@@ -40,7 +40,7 @@ For UI implementation reviews compare the supplied approved Claude design or est
|
|
|
40
40
|
For planning report a concrete plan: requirements, responsibility boundaries, contracts, alternatives/tradeoffs, security, data handling, investigation points, specialist-owned slices, acceptance checks and relevant design skills.
|
|
41
41
|
For author/proposal/synthesis stages, pass means your proposed document is ready for independent validation, not implementation approval. Do not use revise merely because you rewrote a plan: revise requires a concrete open finding and blocked requires a material open finding.
|
|
42
42
|
Architecture planning precedes design for new applications and substantive behavior/architecture changes; existing-app visual redesigns use mode=ui and go directly to design. Claude/Opus owns new UI/UX design, using the supplied design skills and research; Astra reviews the exact design. Never assign new design decisions to the implementation model or its frontend agent. Existing/supplied design patterns may be implemented faithfully without a new design phase. Architecture approval alone is not visual-design approval. Do not block architecture planning because later design artifacts do not exist yet. The reviewer CLI receives captured skill evidence rather than OpenCode's native skill catalog.
|
|
43
|
-
New websites/redesigns must use frontend-design and ui-ux-pro-max before implementation, and web-design-guidelines for review. Use pnpm where appropriate; honor installed versions and project rules.
|
|
43
|
+
New websites/redesigns must use the supplied design guidance before implementation. When the packet selects Impeccable, its bundled references replace frontend-design/ui-ux-pro-max research for this checkpoint; follow its integration policy, not upstream executable workflow or delegation commands. Otherwise use frontend-design and ui-ux-pro-max before implementation, and web-design-guidelines for review. Use pnpm where appropriate; honor installed versions and project rules.
|
|
44
44
|
You may read files under source/ and the packet. Never search the web with private source or internal URLs. No commands that change anything. Keep reports concise and proportional to risk. Missing evidence must be explicit.
|
|
45
45
|
${WORKFLOW_POLICY}`;
|
|
46
46
|
|
package/core/design.mjs
CHANGED
|
@@ -6,10 +6,12 @@ import { runProcess } from './workers.mjs';
|
|
|
6
6
|
import { captureImages } from './images.mjs';
|
|
7
7
|
import {findingChecklist,findingDecision} from './findings.mjs';
|
|
8
8
|
import {browserEvidence} from '../lib/browser-evidence/receipt.mjs';
|
|
9
|
+
import {impeccableIntent,impeccableEvidence} from '../lib/impeccable.mjs';
|
|
9
10
|
|
|
10
11
|
export const DESIGN_POLICY = `UI/UX ownership: the implementation model must not invent visual direction or interaction design. Claude/Opus authors new design decisions and Astra reviews them. This applies to new projects, substantial features without a design, and later redesign/improve-UI requests. Small/medium additions can reuse an established design system and supplied Figma/Claude/local designs can be implemented faithfully, with concrete reference evidence. Escalate material gaps instead of filling them with your own design. Before edits call hybrid_checkpoint design when design is pending; do not bypass through subagents. Read the returned approved design document and give it to frontend-engineer. Existing/supplied exceptions require designMode and designEvidence at begin. Architecture planning does not authorize new visual decisions.`;
|
|
11
12
|
|
|
12
13
|
export function designIntent(text = '') {
|
|
14
|
+
if(impeccableIntent(text))return 'new';
|
|
13
15
|
// This is an admission safety net, not a substitute for the semantic routing rule.
|
|
14
16
|
const value = text.toLowerCase().replace(/\b(?:do not|don't|without|no)\s+(?:re-?design|redesign|redisign)\b/g, 'preserve');
|
|
15
17
|
const supplied = /\b(figma|supplied design|provided design|claude design|design file|reference design)\b/.test(value) && /\b(implement|match|follow|reproduce|replicate)\b/.test(value);
|
|
@@ -85,6 +87,8 @@ export async function design(state, input, signal) {
|
|
|
85
87
|
const brief = pending.length ? pending.map(r=>r.text).join('\n\n') : state.brief;
|
|
86
88
|
const evidence = {brief, userDirections:pending, plan:state.plan, referenceEvidence:state.designEvidence ?? '', supplied:input.checks ?? ''};
|
|
87
89
|
const tree = snapshot(this.directory);
|
|
90
|
+
const integration=impeccableEvidence(this.config,brief,tree);
|
|
91
|
+
if(integration)evidence.impeccable={name:integration.name,mode:integration.mode,provenance:integration.provenance,digest:integration.digest};
|
|
88
92
|
const captured=browserEvidence(this,state,tree.fingerprint);
|
|
89
93
|
const images=[...captureImages(this.directory,input.images,tree.fingerprint),...captured.images].map((image,index)=>({...image,manifest:{...image.manifest,path:`images/${index}.png`}}));
|
|
90
94
|
evidence.browserEvidence=captured.summary;
|
|
@@ -95,7 +99,7 @@ export async function design(state, input, signal) {
|
|
|
95
99
|
state.designResumePhase ??= ['fix','review'].includes(state.phase) ? 'fix' : 'implement';
|
|
96
100
|
if(state.mode==='cosmetic') state.mode='small';
|
|
97
101
|
state.phase='design'; this.save(state);
|
|
98
|
-
const skills = await (this.designEvidence ?? skillEvidence)(this,brief,state.mode === 'small');
|
|
102
|
+
const skills = integration ? {impeccable:integration,guidance:integration.policy} : await (this.designEvidence ?? skillEvidence)(this,brief,state.mode === 'small');
|
|
99
103
|
if (state.designEvidenceHash !== evidenceHash) { state.designNeedsRevision=true; state.designEvidenceHash=evidenceHash; delete state.designPause; }
|
|
100
104
|
while (true) {
|
|
101
105
|
if(signal?.aborted) throw new Error('Design cancelled; approval remains closed.');
|
package/core/plugin.mjs
CHANGED
|
@@ -8,6 +8,8 @@ import { Controller } from './controller.mjs';
|
|
|
8
8
|
import { readDashboard } from './monitor-data.mjs';
|
|
9
9
|
import { DESIGN_POLICY, recordDesignRequest } from './design.mjs';
|
|
10
10
|
import { researchTool } from './research-tools.mjs';
|
|
11
|
+
import {registerImpeccableCommand,IMPECCABLE_POLICY} from '../lib/impeccable.mjs';
|
|
12
|
+
import {registerContinuation,CONTINUATION_POLICY} from '../lib/continuation.mjs';
|
|
11
13
|
|
|
12
14
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
13
15
|
// V2's Plugin.define is an identity function. This plain plugin needs no installed dependencies.
|
|
@@ -20,6 +22,7 @@ export function createPlugin(config, storage = config.runtimeDirectory ?? path.j
|
|
|
20
22
|
const compatible = config.compatibleOpenCode ?? [config.targetOpenCode];
|
|
21
23
|
if (!compatible.includes(ctx.app.version)) throw new Error(`Hybrid plugin compatibility check required: running OpenCode ${ctx.app.version}; verified versions: ${compatible.join(', ')}. Run the isolated compatibility probe before adding this version. Existing task history is preserved.`);
|
|
22
24
|
const controllers = new Map();
|
|
25
|
+
await registerImpeccableCommand(ctx);
|
|
23
26
|
const forSession = async sessionID => {
|
|
24
27
|
const result = await ctx.session.get({ sessionID });
|
|
25
28
|
const session = result.data ?? result;
|
|
@@ -28,6 +31,7 @@ export function createPlugin(config, storage = config.runtimeDirectory ?? path.j
|
|
|
28
31
|
if (!controllers.has(directory)) controllers.set(directory, new Controller(directory, config.storageFor?.(directory) ?? storage, config));
|
|
29
32
|
return controllers.get(directory);
|
|
30
33
|
};
|
|
34
|
+
await registerContinuation(ctx,forSession);
|
|
31
35
|
await ctx.session.hook('prompt', async event => {
|
|
32
36
|
if(await isReviewerSession(ctx,event.sessionID))return;
|
|
33
37
|
const response = await ctx.session.get({sessionID:event.sessionID});
|
|
@@ -40,6 +44,8 @@ export function createPlugin(config, storage = config.runtimeDirectory ?? path.j
|
|
|
40
44
|
if(await isReviewerSession(ctx,event.sessionID))return;
|
|
41
45
|
event.system.push({type:'text',text:roleText(DESIGN_POLICY)});
|
|
42
46
|
event.system.push({type:'text',text:roleText(WORKFLOW_POLICY)});
|
|
47
|
+
event.system.push({type:'text',text:IMPECCABLE_POLICY});
|
|
48
|
+
event.system.push({type:'text',text:CONTINUATION_POLICY});
|
|
43
49
|
event.system.push({type:'text',text:`${FRONTEND_COMMON}\n${FRONTEND_BUILD}`});
|
|
44
50
|
event.system.push({type:'text',text:'Hybrid recovery: completed negative/blocked code reviews allow corrections and evidence collection in fix; continue automatically and re-review, never claim acceptance. action=fix recovers a legacy completed review. New evidence in checks/images resumes paused plan/design; action=replan with concrete changed requirements opens architecture revision while preserving findings and history. action=inspect checks installed tools and CLI logins without model calls and is available during planning. Never edit bridge state or reset history. Playwright research outputs use plain filenames in .playwright-mcp/hybrid-evidence; this generated evidence is excluded from source snapshots. Navigation/resizing and screenshot capture are available while planning/designing. Supply actual PNGs to plan/design/review as images [{path,caption,artifactHash}], using status.currentArtifactHash for the code/reference being inspected. Recapture after code changes and include interaction evidence in checks. Both reviewers receive pixels; never substitute hashes or ask the user to attest images merely because they live outside Git.'});
|
|
45
51
|
});
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Release notes
|
|
2
2
|
|
|
3
|
+
## 1.1.1
|
|
4
|
+
|
|
5
|
+
- Recover reasoning-only silent stops in participating Hybrid sessions on OpenCode 2.2.5+. The same agent continues within the current execution, preserving user instructions and checkpoint approvals.
|
|
6
|
+
- Require clear plan-review, completion and blocker handoffs in agent guidance. Visible answers and user interruptions are left alone.
|
|
7
|
+
- Show a session-specific sidebar warning if recovery stops silently again without new evidence or checkpoint progress. This warning does not call a model or change approvals.
|
|
8
|
+
|
|
9
|
+
## 1.1.0
|
|
10
|
+
|
|
11
|
+
- Add optional Impeccable design guidance in Hybrid settings and `/hybrid-impeccable redesign` or `polish`. The configured design lead authors the direction, reviewers validate it, and the implementation model follows the approved document.
|
|
12
|
+
- Bundle selected, version-pinned Impeccable references with their license. No separate engine, hooks or runtime downloads are required.
|
|
13
|
+
- Recover interrupted native-provider reports as complete responses instead of accepting a continuation fragment. Incomplete recovery still blocks approval.
|
|
14
|
+
- Accept an exact single JSON code-block wrapper from native reviewers while retaining strict response validation.
|
|
15
|
+
|
|
3
16
|
## 1.0.1
|
|
4
17
|
|
|
5
18
|
- Refresh usage sidebar settings immediately after saving Hybrid setup, including reviewer models, effort, reviewer count and telemetry location. No restart is needed for these sidebar changes.
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Impeccable design guidance
|
|
2
|
+
|
|
3
|
+
In **/hybrid → Design guidance**, select **Impeccable**. Your selected design lead and reviewers stay in charge. Nothing else needs installing.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
For a single task, use:
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
/hybrid-impeccable redesign the portfolio
|
|
11
|
+
/hybrid-impeccable polish the checkout
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Or ask normally: “Use Impeccable to redesign this page.” Say “without Impeccable” to override the global setting for a request.
|
|
15
|
+
|
|
16
|
+
The lead receives selected Impeccable references and existing project design documents, authors concrete implementation instructions, and submits them to the configured reviewers. Your implementation model follows the approved document. Polish preserves the current identity; redesign can replace it. The existing rule still applies: all configured reports, at least one pass, and no unresolved medium/high/critical findings.
|
|
17
|
+
|
|
18
|
+
This integrates **Impeccable 4.8.0 design guidance**, not its separate executable engine. It does not install or run upstream hooks, detectors, live mode, concept boards, image generation or audit commands. Those tools' results are never implied by using the guidance. `/hybrid-impeccable` is separate from an independently installed `/impeccable` command; use the Hybrid command for this workflow.
|
|
19
|
+
|
|
20
|
+
The setting affects future design checkpoints without invalidating existing approvals or changing reviewers on active tasks. The command is available on hosts with OpenCode's command transform API; normal chat routing remains available on other supported OpenCode 2 hosts.
|
|
21
|
+
|
|
22
|
+
Bundled references carry their original license and exact source commit in `assets/integrations/impeccable/`. No third-party scripts or runtime downloads are required. Relevant project documents come from the same sanitized snapshot as the source evidence.
|
package/lib/config.mjs
CHANGED
|
@@ -34,6 +34,7 @@ export function readSettings(options={},env=process.env) {
|
|
|
34
34
|
const config={...defaults,...supplied,reviewers:Object.fromEntries(['astra','opus'].map(who=>[who,supplied.reviewers?.[who]===null?null:supplied.reviewers?.[who]?.adapter&&supplied.reviewers[who].adapter!==defaults.reviewers[who].adapter?supplied.reviewers[who]:{...defaults.reviewers[who],...supplied.reviewers?.[who]}])),
|
|
35
35
|
design:{...defaults.design,...supplied.design},verification:{...defaults.verification,...supplied.verification}};
|
|
36
36
|
if(options.enabled===false)config.enabled=false;
|
|
37
|
+
if(config.design.impeccable!==undefined&&typeof config.design.impeccable!=='boolean')throw Error('design.impeccable must be true or false.');
|
|
37
38
|
validateBrowser(config.reviewBrowser);
|
|
38
39
|
for(const who of ['astra','opus']) {
|
|
39
40
|
if(supplied.reviewers?.[who]===null)config.reviewers[who]=null;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import {createHash} from 'node:crypto';
|
|
2
|
+
export const continuationFile=sessionID=>`continuation-${createHash('sha256').update(JSON.stringify(sessionID)).digest('hex')}.json`;
|
|
3
|
+
export function continuationNotice(notice,session) {
|
|
4
|
+
return notice?.busy===session?.time?.busy ? notice?.text??'' : '';
|
|
5
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {createHash} from 'node:crypto';
|
|
2
|
+
import {isReviewerSession} from './opencode-reviewer.mjs';
|
|
3
|
+
import {writeJSONAtomic,runtimePath} from '../core/state-store.mjs';
|
|
4
|
+
import {operationBusy} from '../core/operation-lock.mjs';
|
|
5
|
+
import {continuationFile} from './continuation-notice.mjs';
|
|
6
|
+
|
|
7
|
+
const SOURCE='hybrid-silent-stop';
|
|
8
|
+
const unwrap=value=>value.data??value;
|
|
9
|
+
const digest=value=>createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
10
|
+
const textOf=message=>(message?.content??[]).filter(p=>p.type==='text').map(p=>p.text??'').join('').trim();
|
|
11
|
+
const meaningful=part=>part.type==='tool' && part.state?.status==='completed' &&
|
|
12
|
+
!['hybrid_activity','todowrite','todoread'].includes(part.name) &&
|
|
13
|
+
!(part.name==='hybrid_checkpoint' && ['status','inspect'].includes(part.state.input?.action));
|
|
14
|
+
|
|
15
|
+
export const CONTINUATION_POLICY=`Finish the turn visibly. Reasoning and tool output are not a handoff to the user. While authorized work remains, continue gathering evidence, resolving findings and calling the next checkpoint; do not merely promise the next tool call and stop. When the user asked to review the plan before coding, finish planning and present the plan with its actual approval status and an explicit request for their review; do not start implementation. Respect all user pauses, cancellations, scope and approval requirements. If truly blocked by missing user input, authentication or unavailable external evidence, explain what is unfinished, the exact blocker and the specific next action. A model stop or successful turn is not task completion; claim completion only when the milestone and checkpoint are complete.`;
|
|
16
|
+
|
|
17
|
+
// No provider call or semantic guess about consent: visible final answers are
|
|
18
|
+
// left alone. This guard repairs the observed reasoning-only terminal response.
|
|
19
|
+
// Receipts live in the host transcript, not the approval/state files.
|
|
20
|
+
export function silentStopDecision(messages,state,sessionID,background=[]) {
|
|
21
|
+
if(!state || background.some(work=>work.sessionID===sessionID))return null;
|
|
22
|
+
const userIndex=messages.findLastIndex(m=>m.type==='user');
|
|
23
|
+
if(userIndex<0)return null;
|
|
24
|
+
const turn=messages.slice(userIndex+1),last=turn.findLast(m=>m.type==='assistant');
|
|
25
|
+
if(!last || last.error || last.finish!=='stop' || textOf(last))return null;
|
|
26
|
+
if(turn.some(m=>m.type==='assistant' && (m.error || ['error','length','content-filter'].includes(m.finish))))return null;
|
|
27
|
+
// Only sessions actually participating in the workflow, never an unrelated
|
|
28
|
+
// chat in the same directory or a model-generated reviewer session.
|
|
29
|
+
if(!messages.some(m=>m.type==='assistant' && (m.content??[]).some(p=>
|
|
30
|
+
p.name==='hybrid_checkpoint' && p.state?.status==='completed' &&
|
|
31
|
+
['begin','plan','replan','design','review','verify','browser','fix','finish'].includes(p.state.input?.action))))return null;
|
|
32
|
+
const progress=[...new Set(turn.flatMap(m=>m.type==='assistant'?(m.content??[]):[])
|
|
33
|
+
.filter(meaningful).map(p=>digest([p.name,p.state.input,p.state.content])))].sort();
|
|
34
|
+
const key=digest([state.id,state.phase,state.candidate?.hash,state.approval?.artifactHash,
|
|
35
|
+
state.designApproval?.artifactHash,state.checkpoint,progress]);
|
|
36
|
+
const receipts=turn.filter(m=>m.type==='synthetic' && m.metadata?.source===SOURCE && m.metadata?.taskID===state.id);
|
|
37
|
+
if(receipts.some(m=>m.metadata?.key===key))return {
|
|
38
|
+
notice:`The agent stopped without a user-facing handoff after recovery. Checkpoint: ${state.phase}. Send a message to resume. Existing approvals are unchanged.`,
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
resume:false,metadata:{source:SOURCE,taskID:state.id,key},
|
|
42
|
+
description:'Hybrid Workflow: recovering a silent stop; the agent has not handed the task back to you.',
|
|
43
|
+
text:`Hybrid Workflow detected a terminal response with no user-facing answer. Checkpoint phase: ${state.phase}. ${CONTINUATION_POLICY} Continue the next authorized step now, or provide the clear final handoff if waiting for the user. This recovery is not user consent, a new task, or permission to bypass a checkpoint. Do not repeat an unchanged failing call.`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function registerContinuation(ctx,forSession) {
|
|
48
|
+
// experimental.idle was verified against the 2.2.5 API/host. Older hosts keep
|
|
49
|
+
// the instructions but cannot safely emulate this using after-idle timers.
|
|
50
|
+
const [major,minor,patch]=String(ctx.app?.version??'').split('.').map(Number);
|
|
51
|
+
if(major!==2 || minor<2 || (minor===2 && patch<5) || typeof ctx.session.synthetic!=='function')return;
|
|
52
|
+
const pending=new Set();
|
|
53
|
+
const generation=new Map();
|
|
54
|
+
await ctx.session.hook('prompt',event=>{generation.set(event.sessionID,(generation.get(event.sessionID)??0)+1);});
|
|
55
|
+
await ctx.session.hook('experimental.idle',async event=>{
|
|
56
|
+
if(pending.has(event.sessionID))return;
|
|
57
|
+
pending.add(event.sessionID);
|
|
58
|
+
try {
|
|
59
|
+
if(await isReviewerSession(ctx,event.sessionID))return;
|
|
60
|
+
const session=unwrap(await ctx.session.get({sessionID:event.sessionID}));
|
|
61
|
+
if(session.parentID || session.time?.archived)return;
|
|
62
|
+
const epoch=generation.get(event.sessionID)??0;
|
|
63
|
+
const controller=await forSession(event.sessionID);
|
|
64
|
+
if(operationBusy(controller.lockFile))return;
|
|
65
|
+
const messages=unwrap(await ctx.session.context({sessionID:event.sessionID}));
|
|
66
|
+
const decision=silentStopDecision(messages,controller.read(),event.sessionID,event.background);
|
|
67
|
+
if(!decision)return;
|
|
68
|
+
const current=unwrap(await ctx.session.get({sessionID:event.sessionID}));
|
|
69
|
+
const busy=Number(new Date(current.time?.busy??0)),idle=Number(new Date(current.time?.idle??0));
|
|
70
|
+
if(!busy || idle>=busy || current.time?.busy!==session.time?.busy || current.time?.archived ||
|
|
71
|
+
(generation.get(event.sessionID)??0)!==epoch)return;
|
|
72
|
+
if(decision.notice){
|
|
73
|
+
// A synthetic notice would itself cause another model call inside this
|
|
74
|
+
// execution, even with resume:false. The sidebar reads this inert receipt.
|
|
75
|
+
writeJSONAtomic(runtimePath(controller.folder,continuationFile(event.sessionID)),{
|
|
76
|
+
sessionID:event.sessionID,taskID:controller.read()?.id,busy:current.time.busy,text:decision.notice,
|
|
77
|
+
});
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// Drain only within the existing execution. If a user interrupts while the
|
|
81
|
+
// async hook is finishing, resume:false cannot wake the stopped session.
|
|
82
|
+
await ctx.session.synthetic({sessionID:event.sessionID,...decision});
|
|
83
|
+
} finally {pending.delete(event.sessionID);}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {readFileSync} from 'node:fs';
|
|
2
|
+
import {createHash} from 'node:crypto';
|
|
3
|
+
const root=new URL('../assets/integrations/impeccable/',import.meta.url);
|
|
4
|
+
const optedOut=text=>/\b(?:without|disable|do not use|don't use|no)\s+impecc?able\b/i.test(text);
|
|
5
|
+
export const IMPECCABLE_POLICY='Impeccable integration: /hybrid-impeccable redesign or polish routes through hybrid_checkpoint design, never directly to a Build subagent. The configured design lead owns decisions; implementation follows its approved document. Bundled Impeccable references are design guidance, not executable workflow instructions. Hybrid owns approval, delegation, retries and tools. Do not run referenced Impeccable binaries, hooks, detectors, installers or nested commands, or claim their results. Use actual source, screenshots and existing verification tools. No additional rounds, quotas or approval gates are introduced. Polish preserves the current visual identity; redesign may replace it within the user brief. Read-only audits are not supported by this command; they must not authorize edits.';
|
|
6
|
+
export function impeccableIntent(text='') {
|
|
7
|
+
if(!/\bimpecc?able\b/i.test(text)||optedOut(text))return null;
|
|
8
|
+
const explicit=/\buse\s+impecc?able\s+(?:for this\s+)?(redesign|polish)\b/i.exec(text);
|
|
9
|
+
if(explicit)return explicit[1].toLowerCase();
|
|
10
|
+
if(/\b(?:redesign|redisign|restyle|rebrand)\b/i.test(text))return 'redesign';
|
|
11
|
+
if(/\bpolish\b/i.test(text))return 'polish';
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
export function impeccableEvidence(config,brief,tree) {
|
|
15
|
+
if(optedOut(brief))return null;
|
|
16
|
+
const requested=impeccableIntent(brief);
|
|
17
|
+
if(!requested&&!config.design?.impeccable)return null;
|
|
18
|
+
const mode=requested??(/\bpolish\b/i.test(brief)?'polish':'redesign');
|
|
19
|
+
const files=mode==='polish'?['craft-floor.md','polish.md']:['craft-floor.md','layout.md','typeset.md','colorize.md'];
|
|
20
|
+
const provenance=JSON.parse(readFileSync(new URL('provenance.json',root),'utf8'));
|
|
21
|
+
const references=Object.fromEntries(files.map(name=>[name,readFileSync(new URL(name,root),'utf8')]));
|
|
22
|
+
// Use the already sanitized, immutable snapshot. Never follow a project link or
|
|
23
|
+
// load a model-supplied path to collect additional context.
|
|
24
|
+
const projectContext=Object.fromEntries(['PRODUCT.md','DESIGN.md','docs/design-brief.md','docs/design-system.md'].filter(name=>tree.files[name]?.text&&!tree.files[name].hidden).map(name=>[name,tree.files[name].text]));
|
|
25
|
+
const digest=createHash('sha256').update(JSON.stringify({provenance,references,projectContext,mode})).digest('hex');
|
|
26
|
+
return {name:'Impeccable',mode,provenance,digest,policy:IMPECCABLE_POLICY,references,projectContext};
|
|
27
|
+
}
|
|
28
|
+
export async function registerImpeccableCommand(ctx) {
|
|
29
|
+
if(!ctx.command?.transform)return; // Normal chat routing works on older v2 hosts.
|
|
30
|
+
await ctx.command.transform(editor=>editor.add({name:'hybrid-impeccable',description:'Impeccable redesign or polish through Hybrid’s design lead and reviewers',execute:async({sessionID,prompt,delivery})=>{
|
|
31
|
+
const match=/^\s*(redesign|polish)\b\s*([\s\S]*)$/i.exec(prompt.text??'');
|
|
32
|
+
if(!match)throw Error('Use /hybrid-impeccable redesign <scope> or /hybrid-impeccable polish <scope>. This command does not run read-only audits or the upstream CLI.');
|
|
33
|
+
// Keep attachment mention offsets valid by appending, not prepending.
|
|
34
|
+
await ctx.session.prompt({...prompt,sessionID,delivery,text:`${prompt.text}\n\nUse Impeccable for this ${match[1].toLowerCase()}. ${IMPECCABLE_POLICY}\nBegin a ui task with designMode new if no task is active, then call hybrid_checkpoint design before editing. Existing active tasks keep their history and reviewers.`});
|
|
35
|
+
}}));
|
|
36
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {validateResult} from '../core/workers.mjs';
|
|
2
|
+
const unwrap=x=>x.data??x;
|
|
3
|
+
function interrupted(messages){
|
|
4
|
+
const lastPrompt=messages.findLastIndex(m=>m.type==='user');
|
|
5
|
+
return messages.slice(lastPrompt+1).some(m=>m.type==='assistant'&&(m.error||m.finish==='error'||m.finish==='length'));
|
|
6
|
+
}
|
|
7
|
+
export async function readNativeResult(ctx,sessionID,{isAborted=()=>false,onRecovery=()=>{}}={}){
|
|
8
|
+
let messages=unwrap(await ctx.session.context({sessionID}));
|
|
9
|
+
if(interrupted(messages)){
|
|
10
|
+
if(isAborted())throw Error('Review cancelled; no approval recorded.');
|
|
11
|
+
onRecovery();
|
|
12
|
+
// OpenCode can recover a broken stream with "continue without repeating".
|
|
13
|
+
// Its final message may then be valid JSON containing only a report fragment.
|
|
14
|
+
// Ask the same model to assemble a whole response; never stitch partial JSON
|
|
15
|
+
// or accept approval from the suffix alone.
|
|
16
|
+
await ctx.session.prompt({sessionID,text:'Transport recovery: your previous response was interrupted and the host requested a continuation. Return ONE complete, self-contained JSON result now, including the ENTIRE report, findings and verifiedResolutions. Reconstruct the whole artifact from this conversation, not just its ending. Preserve all substantive decisions and unresolved findings; concise tables are welcome. Do not omit earlier sections, refer to an unavailable earlier report, or claim any new verification. This is response assembly, not a new design or approval. Return only the complete JSON object.'});
|
|
17
|
+
await ctx.session.wait({sessionID});
|
|
18
|
+
messages=unwrap(await ctx.session.context({sessionID}));
|
|
19
|
+
if(interrupted(messages))throw Error('Reviewer response was interrupted again during complete-result recovery; no approval recorded. Retry after resolving the transport failure.');
|
|
20
|
+
}
|
|
21
|
+
const session=unwrap(await ctx.session.get({sessionID}));
|
|
22
|
+
if(isAborted()||session.outcome!=='succeeded')throw Error('OpenCode reviewer did not complete successfully; no approval recorded.');
|
|
23
|
+
const assistant=messages.slice(messages.findLastIndex(m=>m.type==='user')+1).filter(m=>m.type==='assistant').at(-1);
|
|
24
|
+
if(!assistant||assistant.error||assistant.finish!=='stop')throw Error('Reviewer response is incomplete; no approval recorded.');
|
|
25
|
+
const text=assistant.content.filter(c=>c.type==='text').map(c=>c.text).join('').trim();
|
|
26
|
+
// Accept only an exact single JSON fence wrapper, not prose or guessed JSON
|
|
27
|
+
// substrings. The complete enclosed object still passes the same strict schema.
|
|
28
|
+
const fenced=/^```(?:json)?[ \t]*\r?\n([\s\S]*)\r?\n```$/.exec(text);
|
|
29
|
+
let result;try{result=validateResult(JSON.parse(fenced?fenced[1]:text));}catch{throw Error('Reviewer returned invalid JSON; no approval recorded.');}
|
|
30
|
+
return {result,session};
|
|
31
|
+
}
|
|
@@ -2,13 +2,14 @@ import {publicDocument,documentTool,documentSchema,documentDescription} from './
|
|
|
2
2
|
import {readFileSync,realpathSync,readdirSync,statSync} from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import {reviewer} from './reviewers.mjs';
|
|
5
|
-
import {
|
|
5
|
+
import {resultSchema} from '../core/workers.mjs';
|
|
6
6
|
import {Telemetry} from '../core/telemetry.mjs';
|
|
7
7
|
import {NativeBrowser} from './native-browser.mjs';
|
|
8
8
|
import {browserInstructions} from './reviewer-browser.mjs';
|
|
9
9
|
import {tracedTool} from './run-trace.mjs';
|
|
10
10
|
import {EvidenceStore} from './evidence-store.mjs';
|
|
11
11
|
import {queryEvidence,querySchema,queryDescription} from './evidence-query.mjs';
|
|
12
|
+
import {readNativeResult} from './native-result.mjs';
|
|
12
13
|
|
|
13
14
|
const active=new Map();
|
|
14
15
|
const readTools=['hybrid_evidence_read','hybrid_evidence_query',documentTool];
|
|
@@ -123,15 +124,10 @@ export async function installNativeReviewer(ctx) {
|
|
|
123
124
|
await ctx.session.prompt({sessionID:id,text:nativePrompt});
|
|
124
125
|
await ctx.session.wait({sessionID:id});
|
|
125
126
|
if(interrupt)await interrupt;
|
|
126
|
-
const final=
|
|
127
|
+
const {result,session:final}=await readNativeResult(ctx,id,{isAborted:()=>aborted,onRecovery:()=>{telemetry.value.currentAction='Recovering complete report after interrupted provider response';telemetry.value.responseRecovery=true;telemetry.save();}});
|
|
127
128
|
telemetry.value.usage=nativeUsage(final.tokens);telemetry.save();
|
|
128
129
|
if(aborted||final.outcome!=='succeeded')throw Error('OpenCode reviewer did not complete successfully; no approval recorded.');
|
|
129
130
|
if(model.cost?.some(rate=>rate.input>0||rate.output>0))telemetry.value.apiCost=final.cost;
|
|
130
|
-
const messages=unwrap(await ctx.session.context({sessionID:id}));
|
|
131
|
-
const assistant=messages.filter(m=>m.type==='assistant').at(-1);
|
|
132
|
-
if(!assistant||assistant.error||assistant.finish!=='stop')throw Error('Reviewer response is incomplete; no approval recorded.');
|
|
133
|
-
const text=assistant.content.filter(c=>c.type==='text').map(c=>c.text).join('');
|
|
134
|
-
let result;try{result=validateResult(JSON.parse(text.trim()));}catch{throw Error('Reviewer returned invalid JSON; no approval recorded.');}
|
|
135
131
|
await closeBrowser();if(browserCloseError)throw browserCloseError;
|
|
136
132
|
telemetry.end('completed',result.verdict);return result;
|
|
137
133
|
}catch(error){
|
package/lib/settings-rpc.mjs
CHANGED
|
@@ -6,5 +6,6 @@ export const SettingsRPC={id:'hybrid.settings',methods:{
|
|
|
6
6
|
save:{input:{type:'object',additionalProperties:false,properties:{revision:{type:'string'},reviewers:object,designAuthor:{enum:['astra','opus']}},required:['revision','reviewers','designAuthor']},output:object},
|
|
7
7
|
browser:{input:{type:'object',additionalProperties:false,properties:{action:{enum:['enable','disable','install']}},required:['action']},output:object},
|
|
8
8
|
diagnostics:{input:{type:'object',additionalProperties:false},output:object},
|
|
9
|
+
designGuidance:{input:{type:'object',additionalProperties:false,properties:{revision:{type:'string'},enabled:{type:'boolean'}},required:['revision','enabled']},output:object},
|
|
9
10
|
dependencies:{input:{type:'object',additionalProperties:false,properties:{action:{enum:['pnpm','context7']}},required:['action']},output:object},
|
|
10
11
|
},events:{}};
|
package/lib/settings-service.mjs
CHANGED
|
@@ -48,7 +48,7 @@ export function createSettingsService(ctx,options={}){
|
|
|
48
48
|
async function get(){
|
|
49
49
|
const config=load(),models=await catalog();
|
|
50
50
|
return {configured:existsSync(file)&&config.setupComplete!==false,readOnly,revision:revision(file),version:JSON.parse(readFileSync(path.join(packageRoot,'package.json'),'utf8')).version,
|
|
51
|
-
reviewers:config.reviewers,designAuthor:config.designAuthor??(config.reviewers.opus?'opus':'astra'),browserEnabled:!!config.reviewBrowser?.enabled,
|
|
51
|
+
reviewers:config.reviewers,designAuthor:config.designAuthor??(config.reviewers.opus?'opus':'astra'),browserEnabled:!!config.reviewBrowser?.enabled,impeccableEnabled:config.design.impeccable===true,
|
|
52
52
|
catalog:models.map(m=>({providerID:m.providerID,id:m.id,name:m.name,variants:(m.variants??[]).map(v=>v.id),vision:m.capabilities.input?.includes('image')===true})),
|
|
53
53
|
configFile:file,implementationModel:globalRead().model??'Selected in OpenCode',host:process.platform};
|
|
54
54
|
}
|
|
@@ -76,6 +76,13 @@ export function createSettingsService(ctx,options={}){
|
|
|
76
76
|
return folder;
|
|
77
77
|
}
|
|
78
78
|
return {get,diagnostics,
|
|
79
|
+
designGuidance:input=>exclusive(async()=>{
|
|
80
|
+
if(input.revision!==revision(file))throw Error('Settings changed in another window. Reopen settings and try again.');
|
|
81
|
+
if(typeof input.enabled!=='boolean')throw Error('Choose enabled or disabled.');
|
|
82
|
+
const existing=existsSync(file)?JSON.parse(readFileSync(file,'utf8')):{};
|
|
83
|
+
persist({design:{...existing.design,impeccable:input.enabled}});
|
|
84
|
+
return {message:'Saved. Future design checkpoints '+(input.enabled?'use bundled Impeccable guidance.':'use standard guidance unless Impeccable is explicitly requested.')+' Existing approvals and reviewer assignments are preserved.'};
|
|
85
|
+
}),
|
|
79
86
|
cliModels:(input,context={})=>{
|
|
80
87
|
if(!['astra','opus'].includes(input.who)||!['codex-cli','claude-cli'].includes(input.adapter))throw Error('Unknown reviewer connection.');
|
|
81
88
|
const previous=load().reviewers?.[input.who];
|
package/package.json
CHANGED
package/sidebar/reader.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { createHash } from 'node:crypto';
|
|
|
4
4
|
import { reportedUsage, priceCall } from '../core/pricing.mjs';
|
|
5
5
|
import { summarize, countedCalls } from '../core/dashboard-state.js';
|
|
6
6
|
import { runtimeFor } from '../lib/config.mjs';
|
|
7
|
+
import {continuationFile} from '../lib/continuation-notice.mjs';
|
|
7
8
|
|
|
8
9
|
export const projectKey = directory => createHash('sha256').update(process.platform==='win32'?directory.toLowerCase():directory).digest('hex').slice(0,24);
|
|
9
10
|
export const number = value => value == null ? 'Unavailable' : value.toLocaleString('en-US');
|
|
@@ -42,7 +43,7 @@ export function createReader(settings) {
|
|
|
42
43
|
if (relative === '..' || relative.startsWith('..'+path.sep) || path.isAbsolute(relative)) throw new Error('Telemetry path outside runtime');
|
|
43
44
|
return resolved;
|
|
44
45
|
}
|
|
45
|
-
return async directory => {
|
|
46
|
+
return async (directory,sessionID) => {
|
|
46
47
|
if (typeof directory !== 'string' || !path.isAbsolute(directory)) throw new Error('Project location unavailable');
|
|
47
48
|
const project = await realpath(directory);
|
|
48
49
|
const efforts = settings.efforts;
|
|
@@ -57,6 +58,10 @@ export function createReader(settings) {
|
|
|
57
58
|
const normalize=p=>process.platform==='win32'?p.toLowerCase():p;
|
|
58
59
|
if (!state.baseline?.root || normalize(await realpath(state.baseline.root)) !== normalize(project)) throw new Error('Project telemetry does not match current project');
|
|
59
60
|
const result = { ...empty, task: state.id, key: `${empty.key}:${state.id}` };
|
|
61
|
+
if(typeof sessionID==='string')try {
|
|
62
|
+
const notice=await json(await contained(runtime,path.join(folder,continuationFile(sessionID))));
|
|
63
|
+
if(notice.sessionID===sessionID&¬ice.taskID===state.id&&typeof notice.text==='string')result.continuation=notice;
|
|
64
|
+
}catch(error){if(error.code!=='ENOENT')throw error;}
|
|
60
65
|
let taskFolder;
|
|
61
66
|
try { taskFolder = await contained(runtime, path.join(folder, state.id)); }
|
|
62
67
|
catch (error) { if (error.code === 'ENOENT') return result; throw error; }
|
package/sidebar/settings.mjs
CHANGED
|
@@ -87,6 +87,7 @@ export function createSettingsUI(context,onSaved=()=>{}){
|
|
|
87
87
|
{title:'Connect provider',value:'connect',description:'Open OpenCode’s account connection menu'},
|
|
88
88
|
{title:'Implementation model',value:'models',description:'Select the model that builds your code in OpenCode'},
|
|
89
89
|
{title:'Reviewer browser',value:'browser',description:state.browserEnabled?'Enabled · isolated sessions':'Disabled'},
|
|
90
|
+
{title:'Design guidance',value:'designGuidance',description:state.impeccableEnabled?'Impeccable · enabled':'Standard · Impeccable available per request'},
|
|
90
91
|
{title:'Tools and dependencies',value:'dependencies',description:'Browser installation is in Reviewer browser'},
|
|
91
92
|
{title:'Run diagnostics',value:'diagnostics',description:'Check setup and logins without model calls'},
|
|
92
93
|
{title:'About this setup',value:'about',description:'Global configuration and active-task behavior'},
|
|
@@ -96,6 +97,10 @@ export function createSettingsUI(context,onSaved=()=>{}){
|
|
|
96
97
|
if(action==='reviewers')await wizard(state);
|
|
97
98
|
else if(action==='connect'||action==='models'){dispatch(action);return;}
|
|
98
99
|
else if(action==='browser')await browser(state);
|
|
100
|
+
else if(action==='designGuidance'){
|
|
101
|
+
const value=await select('Design guidance',[{title:'Impeccable',value:'impeccable',description:'Guidance for your premium design lead'},{title:'Standard',value:'standard',description:'Impeccable only when requested'}],state.impeccableEnabled?'impeccable':'standard');
|
|
102
|
+
if(value){const result=await invoke('designGuidance',{revision:state.revision,enabled:value==='impeccable'});onSaved();await alert('Design guidance',result.message);}
|
|
103
|
+
}
|
|
99
104
|
else if(action==='dependencies')await dependencies();
|
|
100
105
|
else if(action==='diagnostics'){
|
|
101
106
|
context.ui.toast.show({message:'Checking Hybrid setup…',variant:'info'});
|