@yunhozz/arc 0.2.4 → 0.2.5
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/MANIFEST.sha256 +31 -27
- package/claude/agents/arc-planner.md +3 -3
- package/claude/skill/SKILL.md +2 -2
- package/claude/skill/models.toml +11 -11
- package/claude/skill/references/brainstorming.md +111 -42
- package/claude/skill/references/coordination.md +3 -3
- package/claude/skill/references/planning.md +6 -6
- package/claude/skill/references/superpowers-provenance.json +3 -3
- package/claude/skill/runtime/role-policy.json +3 -3
- package/claude/skill/scripts/model_config.py +17 -5
- package/codex/agents/arc-planner.toml +3 -3
- package/codex/skill/SKILL.md +2 -2
- package/codex/skill/models.toml +13 -13
- package/codex/skill/references/brainstorming.md +111 -42
- package/codex/skill/references/coordination.md +3 -3
- package/codex/skill/references/planning.md +6 -6
- package/codex/skill/references/superpowers-provenance.json +3 -3
- package/codex/skill/runtime/role-policy.json +3 -3
- package/codex/skill/scripts/model_config.py +25 -6
- package/lib/installer/global-config.mjs +1 -1
- package/lib/installer/plan.mjs +6 -5
- package/lib/installer/preset.mjs +7 -4
- package/lib/management/operations.mjs +1 -1
- package/lib/models/catalog-anthropic.mjs +46 -0
- package/lib/models/catalog.mjs +62 -0
- package/lib/models/codex-catalog.mjs +103 -0
- package/lib/models/config.mjs +19 -11
- package/lib/models/index.mjs +1 -1
- package/lib/policy/role-policy.json +3 -3
- package/lib/tui/app.mjs +15 -9
- package/lib/tui/custom-preset.mjs +122 -0
- package/lib/tui/render.mjs +3 -2
- package/lib/tui/setup.mjs +62 -6
- package/package.json +1 -1
package/lib/tui/app.mjs
CHANGED
|
@@ -2,23 +2,27 @@ import {createTerminalSession} from './terminal.mjs';
|
|
|
2
2
|
import {escapeTerminalText,navigationHint,wrapDetailRows} from './render.mjs';
|
|
3
3
|
import {createManagementContext,prepareOperation,executeOperation} from '../management/operations.mjs';
|
|
4
4
|
import {collectStatus} from '../management/status.mjs';
|
|
5
|
-
import {runSetup} from './setup.mjs';
|
|
5
|
+
import {runSetup,loadPresetPreviewConfig} from './setup.mjs';
|
|
6
|
+
import {loadCustomCatalog} from './custom-preset.mjs';
|
|
6
7
|
const home=['Install / Update','Change Preset','Check Version','Uninstall','Exit'];
|
|
7
8
|
function goHome(){throw Object.assign(Error('Return home'),{code:'ARC_TUI_HOME'});}
|
|
8
9
|
function interrupted(){return Object.assign(Error('Session interrupted'),{code:'ARC_CANCELLED'});}
|
|
9
10
|
export function createTuiInterface(session,{input,controller}={}){
|
|
10
11
|
async function event(){const e=await session.nextEvent();if(e.name==='interrupt')throw interrupted();if(e.name==='eof')throw Object.assign(Error('Session closed'),{code:'ARC_TUI_EOF'});if(e.name==='error')throw e.error;return e;}
|
|
11
|
-
async function menu(title,rows,{selected=0,info=[],separatorBefore,backRow=false,backLabel='back',escapeBack=false,wordmark=false}={}){
|
|
12
|
+
async function menu(title,rows,{selected=0,info=[],separatorBefore,backRow=false,backLabel='back',escapeBack=false,wordmark=false,renderBody,refreshMs}={}){
|
|
12
13
|
const actions=backRow?[...rows,'Back']:rows;
|
|
13
14
|
selected=actions.length?Math.max(0,Math.min(selected,actions.length-1)):0;
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
let pending,timer,blink=true;
|
|
16
|
+
try{while(true){
|
|
17
|
+
const viewport=session.viewport(),display=actions.flatMap((row,index)=>(index===separatorBefore||backRow&&index===rows.length)?['',row]:[row]),displayIndex=selected+(Number.isInteger(separatorBefore)&&selected>=separatorBefore?1:0)+(backRow&&selected===rows.length?1:0),information=wrapDetailRows(typeof info==='function'?info({selected,...viewport}):info,viewport.columns);
|
|
16
18
|
if(wordmark&&information.length)information.push('');
|
|
17
19
|
const capacity=Math.max(1,viewport.rows-(wordmark?6:1)-information.length-4),offset=Math.max(0,Math.min(displayIndex-capacity+1,display.length-capacity)),visible=display.slice(offset,offset+capacity);
|
|
18
20
|
const screen={title,wordmark,rows:[...information,...visible],selected:actions.length?information.length+displayIndex-offset:undefined,hint:navigationHint('menu',{backLabel}),...(display.length>capacity?{position:(offset+1)+'-'+Math.min(offset+capacity,display.length)+'/'+display.length}:{})};
|
|
19
|
-
const
|
|
21
|
+
const body=renderBody?.({selected,blink,...viewport});if(body){screen.rows=body.rows;screen.selected=body.selected;screen.showSelection=body.showSelection;}
|
|
22
|
+
const rendered=session.render(screen);pending??=event();
|
|
23
|
+
let e;if(refreshMs>0){e=await Promise.race([pending,new Promise(resolve=>{timer=setTimeout(()=>resolve(null),refreshMs);})]);clearTimeout(timer);timer=undefined;if(e===null){blink=!blink;continue;}}else e=await pending;pending=undefined;
|
|
20
24
|
if(e.name==='up'&&actions.length)selected=(selected+actions.length-1)%actions.length;else if(e.name==='down'&&actions.length)selected=(selected+1)%actions.length;else if(e.name==='escape'){if(backLabel==='exit'||escapeBack)return null;goHome();}else if(e.name==='enter'&&actions.length&&rendered.selectionEnabled!==false)return backRow&&selected===rows.length?null:selected;
|
|
21
|
-
}
|
|
25
|
+
}}finally{if(timer!==undefined)clearTimeout(timer);}
|
|
22
26
|
}
|
|
23
27
|
async function text(title,initial=''){let value=initial,selected=2;while(true){const rendered=session.render({title,mode:'text',rows:[value,'','Save','Back',''],selected,hint:navigationHint('text')});const e=await event();if(e.name==='text'||e.name==='paste')value+=e.text;else if(e.name==='backspace')value=[...value].slice(0,-1).join('');else if(e.name==='up'||e.name==='down')selected=selected===2?3:2;else if(e.name==='escape')goHome();else if(e.name==='enter'&&rendered.selectionEnabled!==false)return selected===2?value:null;}}
|
|
24
28
|
async function page(title,rows){
|
|
@@ -42,8 +46,10 @@ export async function runTui(options={}){
|
|
|
42
46
|
try{session??=createTerminalSession(options);const ui=createTuiInterface(session,{input:options.input??process.stdin,controller}),api={collectStatus,prepareOperation,executeOperation,...options.services},context=options.context?{...options.context,signal:controller.signal}:await createManagementContext({...options.contextOptions,signal:controller.signal});const draft={};let selected=0;
|
|
43
47
|
async function execute(prepared){
|
|
44
48
|
if(!prepared)return;const result=await ui.busy(()=>api.executeOperation(context,prepared));code=result.code;
|
|
45
|
-
if(code===
|
|
46
|
-
|
|
49
|
+
if(code===0&&['install','preset'].includes(prepared.kind))for(const plan of prepared.plans??[]){const platform=plan.target?.platform;if(draft[platform])delete draft[platform].customEdited;}
|
|
50
|
+
const rows=prepared.kind==='update'&&code===0?[`ARC updated to ${prepared.latestVersion}.`,'Start a new agent session to use the updated settings.']:resultRows(result);
|
|
51
|
+
if(code===130||result.exitSession){finalReport=rows;if(result.exitSession)session.render({title:'Result',rows:finalReport,hint:'Session ended'});return true;}
|
|
52
|
+
await ui.page('Result',rows);return false;
|
|
47
53
|
}
|
|
48
54
|
async function uninstall(){
|
|
49
55
|
let selected=0,target=0;
|
|
@@ -58,7 +64,7 @@ export async function runTui(options={}){
|
|
|
58
64
|
const info=ordered.length?[ordered.map(status=>label(status)+': '+state(status)).join(' / '),...ordered.flatMap(status=>(status.diagnostics??[]).map(error=>label(status)+': '+error.message))]:[];const choice=await ui.menu('ARC',home,{selected,info,backLabel:'exit',wordmark:true});if(choice===null||choice===4)return code;selected=choice;
|
|
59
65
|
try{
|
|
60
66
|
let prepared;
|
|
61
|
-
if(choice===0||choice===1)prepared=await runSetup({ui,statuses,draft,kind:
|
|
67
|
+
if(choice===0||choice===1){const kind=choice===0?'install':'preset';prepared=await runSetup({ui,statuses,draft,kind,loadPreviewConfig:target=>loadPresetPreviewConfig({context,statuses,kind,target}),loadCatalog:target=>api.loadCatalog?api.loadCatalog(target):loadCustomCatalog({platform:target,env:context.env??process.env,invocationCwd:context.cwd??process.cwd(),userHome:context.userHome,signal:controller.signal}),prepare:settings=>ui.busy(()=>api.prepareOperation(context,settings))});}
|
|
62
68
|
else if(choice===2){prepared=await ui.busy(()=>api.prepareOperation(context,{kind:'update'}));const action=await ui.menu('Check Version',prepared.canUpgrade?['Version Upgrade']:[],{backRow:true,info:['Current version: '+prepared.currentVersion,'Latest version: '+(prepared.latestVersion??'Unavailable'),...(prepared.latestError?[prepared.latestError.message]:[]),...(prepared.canUpgrade?['Continue?']:[])]});if(action!==0)prepared=null;}
|
|
63
69
|
else prepared=await uninstall();
|
|
64
70
|
if(await execute(prepared))return code;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { ROLES, effortField, validateRoles } from '../models/config.mjs';
|
|
5
|
+
import { assertCatalogRoles, discoverModelCatalog, listCodexModelCapabilities } from '../models/catalog.mjs';
|
|
6
|
+
import { escapeTerminalText, wrapDetailRows } from './render.mjs';
|
|
7
|
+
import { resolveTarget } from '../installer/targets.mjs';
|
|
8
|
+
import { presetPreviewRows } from './setup.mjs';
|
|
9
|
+
|
|
10
|
+
const names = ['Scouter', 'Planner', 'Implementer', 'QA', 'Reviewer'];
|
|
11
|
+
const unavailable = () => ({ status: 'unavailable', code: 'unavailable', message: 'Catalog is unavailable.' });
|
|
12
|
+
|
|
13
|
+
async function assertProjectless(directory) {
|
|
14
|
+
for (let current = directory;; current = path.dirname(current)) {
|
|
15
|
+
for (const name of ['.git', '.codex']) {
|
|
16
|
+
try { await fs.lstat(path.join(current, name)); throw Error('Project configuration is present'); }
|
|
17
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
18
|
+
}
|
|
19
|
+
if (path.dirname(current) === current) break;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function loadCustomCatalog({ platform, env = process.env, signal, fetchImpl, listCodexModels = listCodexModelCapabilities, tempDir = os.tmpdir(), invocationCwd = process.cwd(), userHome = os.homedir() } = {}) {
|
|
24
|
+
if (platform !== 'codex') return discoverModelCatalog({ platform, env, signal, fetchImpl });
|
|
25
|
+
let directory, result;
|
|
26
|
+
try {
|
|
27
|
+
const childEnv = env?.CODEX_HOME === undefined ? env : { ...env, CODEX_HOME: resolveTarget('codex', { env, cwd: invocationCwd, userHome }).home };
|
|
28
|
+
const base = await fs.realpath(tempDir);
|
|
29
|
+
await assertProjectless(base);
|
|
30
|
+
directory = await fs.mkdtemp(path.join(base, 'arc-model-catalog-'));
|
|
31
|
+
const cwd = await fs.realpath(directory);
|
|
32
|
+
await assertProjectless(cwd);
|
|
33
|
+
result = await discoverModelCatalog({ platform, env: childEnv, signal, listCodexModels: options => listCodexModels({ ...options, cwd }) });
|
|
34
|
+
} catch { result = unavailable(); }
|
|
35
|
+
finally { if (directory) try { await fs.rm(directory, { recursive: true, force: true }); } catch { result = unavailable(); } }
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function pairKey(model, effort) { return JSON.stringify([model, effort]); }
|
|
40
|
+
function short(value, limit = 12) {
|
|
41
|
+
const characters = [...escapeTerminalText(value)];
|
|
42
|
+
return characters.length > limit ? characters.slice(0, limit - 1).join('') + '…' : characters.join('');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function details(model, effort, { columns = 80, rows = 24 } = {}) {
|
|
46
|
+
return [...wrapDetailRows([`Model: ${model}`], columns), ...wrapDetailRows([`Effort: ${effort}`], columns), ''];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function detailGuard(info) {
|
|
50
|
+
return state => {
|
|
51
|
+
const requiredRows = wrapDetailRows(info(state), state.columns).length + 6;
|
|
52
|
+
return state.rows < requiredRows ? { rows: [`Menu requires ${requiredRows} rows at ${state.columns} columns.`], selected: undefined } : undefined;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function editCustomPreset({ ui, config, initialRoles, catalog }) {
|
|
57
|
+
const platform = config.platform, field = effortField(platform), roles = validateRoles(initialRoles, platform);
|
|
58
|
+
if (catalog?.platform !== platform || !Array.isArray(catalog.pairs) || !catalog.pairs.length) return null;
|
|
59
|
+
const available = new Set(catalog.pairs.map(pair => pairKey(pair.model, pair.effort)));
|
|
60
|
+
let selectedRole = 0;
|
|
61
|
+
while (true) {
|
|
62
|
+
const labels = ROLES.map((role, index) => {
|
|
63
|
+
const value = roles[role], missing = !available.has(pairKey(value.model, value[field]));
|
|
64
|
+
return `${names[index]}: ${short(value.model)} / ${short(value[field], 8)}${missing ? ' !' : ''}`;
|
|
65
|
+
});
|
|
66
|
+
const selected = await ui.menu('Custom preset', [...labels, 'Done'], { selected: selectedRole, backRow: true, separatorBefore: labels.length, refreshMs:600, renderBody:({selected,blink,columns,rows})=>{
|
|
67
|
+
const values=ROLES.map((role,index)=>({role:names[index]+(!available.has(pairKey(roles[role].model,roles[role][field]))?' !':''),model:roles[role].model,effort:roles[role][field]}));
|
|
68
|
+
const diagram=presetPreviewRows(values,{columns,rows,selectedRole:selected<ROLES.length?selected:undefined,blink});
|
|
69
|
+
const body=[...diagram,'','Done','Back'];
|
|
70
|
+
return {rows:body,selected:diagram.length===1?undefined:selected<3?1:selected<5?9:selected===5?diagram.length+1:diagram.length+2,showSelection:selected>=ROLES.length};
|
|
71
|
+
}});
|
|
72
|
+
if (selected === null) return null;
|
|
73
|
+
if (selected === ROLES.length) {
|
|
74
|
+
try { assertCatalogRoles({ platform, roles, catalog }); return roles; }
|
|
75
|
+
catch { await ui.page('Custom preset unavailable', ['Select an available model and effort for each role.']); continue; }
|
|
76
|
+
}
|
|
77
|
+
if (!ROLES[selected]) continue;
|
|
78
|
+
selectedRole = selected;
|
|
79
|
+
const role = ROLES[selected], current = roles[role], initial = catalog.pairs.findIndex(pair => pairKey(pair.model, pair.effort) === pairKey(current.model, current[field]));
|
|
80
|
+
const models = [...new Set(catalog.pairs.map(pair => pair.model))];
|
|
81
|
+
const excluded = Array.isArray(catalog.unavailable) ? catalog.unavailable.filter(item => typeof item?.model === 'string' && typeof item.reason === 'string') : [];
|
|
82
|
+
const rows = [...models, ...(excluded.length ? ['Unavailable models'] : [])];
|
|
83
|
+
while (true) {
|
|
84
|
+
const info = state => {
|
|
85
|
+
const model = models[state.selected];
|
|
86
|
+
return [...(initial < 0 ? ['Current model and effort are unavailable.'] : []), ...details(model ?? '-', model ? catalog.pairs.filter(pair => pair.model === model).map(pair => pair.effort).join(', ') : '-', state)];
|
|
87
|
+
};
|
|
88
|
+
const renderModelMenu = state => {
|
|
89
|
+
const guarded = detailGuard(info)(state);
|
|
90
|
+
if (guarded) return guarded;
|
|
91
|
+
const information = wrapDetailRows(info(state), state.columns), items = [...rows, 'Back'], display = [], starts = [], lengths = [];
|
|
92
|
+
for (let index = 0; index < items.length; index++) {
|
|
93
|
+
if (index === rows.length) display.push('');
|
|
94
|
+
starts[index] = display.length;
|
|
95
|
+
const block = wrapDetailRows([items[index]], state.columns);
|
|
96
|
+
lengths[index] = block.length;
|
|
97
|
+
display.push(...block);
|
|
98
|
+
}
|
|
99
|
+
const selected = Math.max(0, Math.min(state.selected, items.length - 1)), commonCapacity = Math.max(1, state.rows - information.length - 5), positionNeeded = rows.length + 2 > commonCapacity, reservedRows = positionNeeded ? 5 : 4, requiredRows = information.length + lengths[selected] + reservedRows;
|
|
100
|
+
if (state.rows < requiredRows) return { rows: [`Menu requires ${requiredRows} rows at ${state.columns} columns.`], selected: undefined };
|
|
101
|
+
const capacity = Math.max(1, state.rows - information.length - reservedRows), maxOffset = Math.max(0, display.length - capacity);
|
|
102
|
+
const offset = Math.max(0, Math.min(maxOffset, starts[selected] + lengths[selected] - capacity));
|
|
103
|
+
return { rows: [...information, ...display.slice(offset, offset + capacity)], selected: information.length + starts[selected] - offset };
|
|
104
|
+
};
|
|
105
|
+
const choice = await ui.menu(`Select model — ${names[selected]}`, rows, {
|
|
106
|
+
selected: Math.max(0, models.indexOf(current.model)), backRow: true, info,
|
|
107
|
+
renderBody: renderModelMenu,
|
|
108
|
+
});
|
|
109
|
+
if (choice === null) break;
|
|
110
|
+
if (choice === models.length && excluded.length) {
|
|
111
|
+
await ui.page('Unavailable models', excluded.map(item => `${escapeTerminalText(item.model)}: ${escapeTerminalText(item.reason)}`));
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (models[choice]) {
|
|
115
|
+
const model=models[choice], efforts=[...new Set(catalog.pairs.filter(pair=>pair.model===model).map(pair=>pair.effort))];
|
|
116
|
+
const effortInfo=state=>details(model,efforts[state.selected]??'-',state);
|
|
117
|
+
const effort=await ui.menu(`Select effort — ${names[selected]}`,efforts,{selected:Math.max(0,efforts.indexOf(current[field])),backRow:true,info:effortInfo,renderBody:detailGuard(effortInfo)});
|
|
118
|
+
if(effort!==null&&efforts[effort]){roles[role]={model,[field]:efforts[effort]};break;}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
package/lib/tui/render.mjs
CHANGED
|
@@ -28,13 +28,14 @@ export function renderScreen(screen={}, {columns=80,rows=24}={}){
|
|
|
28
28
|
const entries=screen.rows??[],hint=screen.hint??navigationHint('menu');
|
|
29
29
|
const heading=screen.wordmark?[...wordmark,'─'.repeat(Math.min(columns,34))]:[escapeTerminalText(screen.title)],position=screen.position?[escapeTerminalText(screen.position)]:[];
|
|
30
30
|
const footer=screen.mode==='busy'||screen.hint==='Session ended'?[escapeTerminalText(hint)]:[...(screen.mode==='text'||screen.mode==='page'?[]:['']),'─'.repeat(columns),escapeTerminalText(hint)];
|
|
31
|
-
const
|
|
31
|
+
const showSelection=screen.showSelection!==false;
|
|
32
|
+
const lines=[...heading,...entries.map((row,index)=>(index===screen.selected&&showSelection?'> ':' ')+escapeTerminalText(row)),...position,...footer];
|
|
32
33
|
const fitted=lines.map(line=>fit(line,columns));
|
|
33
34
|
const bodySize=Math.max(0,rows-footer.length),visible=[...fitted.slice(0,Math.min(lines.length-footer.length,bodySize)),...fitted.slice(-Math.min(rows,footer.length))].map(line=>line.text);
|
|
34
35
|
const styles=[];
|
|
35
36
|
if(screen.wordmark){for(let index=0;index<Math.min(wordmark.length,visible.length);index++){if(visible[index]===fitted[index].text)styles.push({line:index,color:'logo',cells:[...visible[index]].flatMap((character,column)=>character==='█'?[{column,rgb:logoColor(column)}]:[])});}if(visible.length>wordmark.length&&visible[wordmark.length]===fitted[wordmark.length].text)styles.push({line:wordmark.length,color:'green'});}
|
|
36
37
|
const selectedLine=heading.length+screen.selected;
|
|
37
|
-
if(Number.isInteger(screen.selected)&&screen.selected>=0&&screen.selected<entries.length&&selectedLine<Math.min(bodySize,lines.length-footer.length)){const used=[...visible[selectedLine]].reduce((sum,character)=>sum+width(character),0);visible[selectedLine]+=' '.repeat(Math.max(0,Math.min(columns,34)-used));styles.push({line:selectedLine,color:'selected'});}
|
|
38
|
+
if(showSelection&&Number.isInteger(screen.selected)&&screen.selected>=0&&screen.selected<entries.length&&selectedLine<Math.min(bodySize,lines.length-footer.length)){const used=[...visible[selectedLine]].reduce((sum,character)=>sum+width(character),0);visible[selectedLine]+=' '.repeat(Math.max(0,Math.min(columns,34)-used));styles.push({line:selectedLine,color:'selected'});}
|
|
38
39
|
|
|
39
40
|
const selectionEnabled=Number.isInteger(screen.selected)&&screen.selected>=0&&screen.selected<entries.length&&selectedLine<rows-footer.length-position.length&&fitted[selectedLine].complete;
|
|
40
41
|
return{text:visible.join('\n'),selectionEnabled,styles};
|
package/lib/tui/setup.mjs
CHANGED
|
@@ -1,19 +1,75 @@
|
|
|
1
|
-
const presetLabel=value=>({economy:'Economy',balanced:'Balanced',smart:'Smart'}[value]??value);
|
|
1
|
+
const presetLabel=value=>({economy:'Economy',balanced:'Balanced',smart:'Smart',custom:'Custom'}[value]??value);
|
|
2
2
|
const languageLabel=value=>({en:'EN',ko:'KR'}[value]??value);
|
|
3
3
|
import {parsePositiveLimit} from '../limits.mjs';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import {readFileSync} from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {loadModelConfig,applyPreset,applyCustomRoles,resolveModelSettings,ModelValidationError} from '../models/index.mjs';
|
|
7
|
+
import {escapeTerminalText,wrapDetailRows} from './render.mjs';
|
|
8
|
+
import {editCustomPreset} from './custom-preset.mjs';
|
|
9
|
+
import {assertCatalogRoles} from '../models/catalog.mjs';
|
|
10
|
+
import {ROLES,effortField,validateRoles} from '../models/config.mjs';
|
|
11
|
+
const presets=['economy','balanced','smart','custom'];
|
|
12
|
+
const roleNames=['Scouter','Planner','Implementer','QA','Reviewer'];
|
|
13
|
+
export function loadPresetPreviewConfig({context,statuses,kind,target,readText=filename=>readFileSync(filename,'utf8')}){
|
|
14
|
+
if(!['codex','claude'].includes(target)||!['install','preset'].includes(kind))throw Object.assign(Error('Unsupported preset preview target or operation'),{code:'ARC_CLI_INPUT'});
|
|
15
|
+
if(kind==='install')return loadModelConfig(readText(path.join(context.packageRoot,target,'skill/models.toml')),target);
|
|
16
|
+
const config=statuses.find(status=>status.platform===target)?.settings?.modelConfig;
|
|
17
|
+
if(!config)throw new ModelValidationError('Installed model configuration unavailable for '+target);
|
|
18
|
+
const validated=applyPreset(config,config.preset);
|
|
19
|
+
if(validated.platform!==target)throw new ModelValidationError('Model configuration platform does not match target');
|
|
20
|
+
return validated;
|
|
21
|
+
}
|
|
22
|
+
export function presetRoleRows(config,preset){const resolved=resolveModelSettings(applyPreset(config,preset));return Object.values(resolved.roles).map((settings,index)=>({role:roleNames[index],model:settings.model,effort:settings[resolved.platform==='codex'?'model_reasoning_effort':'effort']}));}
|
|
23
|
+
export function presetPreviewRows(roleRows,{columns=80,rows=24,selectedRole,blink=true,compact=false}={}){
|
|
24
|
+
const values=roleRows.map(({role,model,effort})=>({role:escapeTerminalText(role),model:escapeTerminalText(model),effort:escapeTerminalText(effort)}));
|
|
25
|
+
const cells=value=>[...value].reduce((total,character)=>total+(/[\p{Mark}]/u.test(character)?0:/[\u2190-\u2193\u2500-\u257f\u2588]/u.test(character)?1:character.codePointAt(0)>=0x1100?2:1),0);
|
|
26
|
+
const width=Math.max(20,...values.flatMap(value=>[cells(value.role)+2,cells(value.model),cells('effort: '+value.effort)])),boxWidth=width+4,total=boxWidth*3+4,requiredRows=24;
|
|
27
|
+
if(columns<total+3||rows<requiredRows){
|
|
28
|
+
if(compact){
|
|
29
|
+
const available=Math.max(2,Math.floor(Number(columns)||80)-3),shorten=(value,limit)=>{if(cells(value)<=limit)return value;let result='',used=0;for(const character of value){const size=cells(character);if(used+size>Math.max(0,limit-1))break;result+=character;used+=size;}return result+'…';};
|
|
30
|
+
const roleRow=value=>{const limit=Math.max(1,available-cells(value.role)-cells(': / '+value.effort));return value.role+': '+shorten(value.model,limit)+' / '+value.effort;};
|
|
31
|
+
return [...values.map(roleRow),''];
|
|
32
|
+
}
|
|
33
|
+
return [`Diagram requires ${total+3} columns and ${requiredRows} rows.`];
|
|
34
|
+
}
|
|
35
|
+
const border=(left,right,junction,hidden)=>{const line=[left,...'─'.repeat(width+2),right];if(junction)line[Math.floor(boxWidth/2)]=junction;return hidden?' '.repeat(boxWidth):line.join('');};
|
|
36
|
+
const box=(value,index,up)=>{const hidden=index===selectedRole&&!blink,edge=hidden?' ':'│',label=(index===selectedRole?'> ':'')+value.role;return [border('┌','┐',up?'┴':null,hidden),...[label,value.model,'effort: '+value.effort].map(text=>edge+' '+text+' '.repeat(width-cells(text))+' '+edge),border('└','┘',up?null:'┬',hidden)];};
|
|
37
|
+
const group=(indices,indent,up)=>{const boxes=indices.map(index=>box(values[index],index,up));return Array.from({length:5},(_,row)=>' '.repeat(indent)+boxes.map(lines=>lines[row]).join(' '));};
|
|
38
|
+
const center=Math.floor(boxWidth/2),middle=boxWidth+2+center,right=2*(boxWidth+2)+center,indent=Math.floor((total-(boxWidth*2+2))/2),bottomLeft=indent+center,bottomRight=bottomLeft+boxWidth+2;
|
|
39
|
+
const bus=(left,right,branches,up=false)=>{const line=Array(total).fill(' ');for(let index=left;index<=right;index++)line[index]='─';for(const index of branches)line[index]=index===middle?'┼':up?'┴':'┬';return line.join('').trimEnd();};
|
|
40
|
+
return [...group([0,1,2],0,false),bus(center,right,[center,middle,right],true),' '.repeat(Math.max(0,middle-6))+'Orchestrator',bus(bottomLeft,bottomRight,[bottomLeft,middle,bottomRight]),...group([3,4],indent,true)];
|
|
41
|
+
}
|
|
42
|
+
export function initialSettings(statuses){return Object.fromEntries(['codex','claude'].map(platform=>{const settings=statuses.find(s=>s.platform===platform)?.settings,custom=settings?.modelConfig?.custom;return[platform,{preset:settings?.modelConfig?.preset??'balanced',language:settings?.language??'en',maxTotalSubagents:settings?.limits?.max_total_subagents??100,maxConcurrentSubagents:settings?.limits?.max_concurrent_subagents??5,skipGlobal:false,...(custom?{customRoles:structuredClone(custom)}:{})}];}));}
|
|
43
|
+
export function setupOptions(draft,target,verifiedCatalog,kind){if(!['codex','claude'].includes(target))throw Object.assign(Error('Unsupported setup target'),{code:'ARC_CLI_INPUT'});const {customEdited,customRoles,...settings}=draft[target];return{...settings,target,...(settings.preset==='custom'||customEdited?{customRoles,verifiedCatalog}:kind==='install'&&customRoles?{preservedCustomRoles:customRoles}:{})};}
|
|
44
|
+
export async function runSetup({ui,statuses,draft,kind,prepare,loadPreviewConfig,loadCatalog}){
|
|
7
45
|
for(const[key,value]of Object.entries(initialSettings(statuses)))draft[key]??=value;
|
|
46
|
+
const catalogs=new Map();
|
|
47
|
+
async function catalogFor(target){
|
|
48
|
+
if(catalogs.has(target))return catalogs.get(target);
|
|
49
|
+
let result;try{result=typeof loadCatalog==='function'?await(ui.busy?ui.busy(()=>loadCatalog(target)):loadCatalog(target)):null;}catch(error){if(['ARC_CANCELLED','ARC_TUI_EOF','ARC_TUI_HOME'].includes(error?.code))throw error;}
|
|
50
|
+
if(result?.status==='ready'&&result.catalog?.platform===target&&Array.isArray(result.catalog.pairs)&&result.catalog.pairs.length){try{const pair=result.catalog.pairs[0],field=effortField(target),sample=Object.fromEntries(ROLES.map(role=>[role,{model:pair.model,[field]:pair.effort}]));assertCatalogRoles({platform:target,roles:sample,catalog:result.catalog});for(const candidate of result.catalog.pairs)validateRoles(Object.fromEntries(ROLES.map(role=>[role,{model:candidate.model,[field]:candidate.effort}])),target);catalogs.set(target,result.catalog);return result.catalog;}catch{}}
|
|
51
|
+
await ui.page('Catalog unavailable',['Model catalog could not be loaded.']);return null;
|
|
52
|
+
}
|
|
8
53
|
let selectedTarget=0;
|
|
9
54
|
while(true){const targetIndex=await ui.menu(kind==='preset'?'Change Preset':'Install / Update',['Codex','Claude'],{selected:selectedTarget,backRow:true});if(targetIndex===null)return null;selectedTarget=targetIndex;const target=['codex','claude'][targetIndex];
|
|
10
55
|
const settings=draft[target];let editing=true,selectedField=0;
|
|
11
56
|
while(editing){const labels=kind==='preset'?['Preset: '+presetLabel(settings.preset),'Apply','Back']:['Preset: '+presetLabel(settings.preset),'Harness language: '+languageLabel(settings.language),'Total subagents: '+settings.maxTotalSubagents,'Concurrent subagents: '+settings.maxConcurrentSubagents,'Apply global settings: '+!settings.skipGlobal,'Apply','Back'];const field=await ui.menu('Settings — '+target,labels,{selected:selectedField,separatorBefore:labels.length-2});if(field===null||field===labels.length-1)break;selectedField=field;
|
|
12
|
-
if(field===0){
|
|
57
|
+
if(field===0){
|
|
58
|
+
const config=loadPreviewConfig(target);let preview=presets.indexOf(settings.preset);if(preview<0)preview=1;
|
|
59
|
+
const value=await ui.menu('Performance preset',['Economy','Balanced','Smart','Custom'],{backRow:true,selected:preview,info:({selected,columns,rows})=>{
|
|
60
|
+
const custom=settings.customRoles??config.custom;
|
|
61
|
+
const choice=selected>=presets.length||selected===3&&!custom?settings.preset:presets[selected];
|
|
62
|
+
const previewConfig=choice==='custom'&&custom?applyCustomRoles(config,custom):config;
|
|
63
|
+
return presetPreviewRows(presetRoleRows(previewConfig,choice),{columns,rows,compact:true});
|
|
64
|
+
}});
|
|
65
|
+
if(value!==null&&value<3)settings.preset=presets[value];
|
|
66
|
+
else if(value===3){const catalog=await catalogFor(target);if(catalog){const initial=settings.customRoles??config.custom??resolveModelSettings(applyPreset(config,settings.preset==='custom'?'balanced':settings.preset)).roles;const roles=await editCustomPreset({ui,config,initialRoles:initial,catalog});if(roles){settings.customRoles=roles;settings.customEdited=true;settings.preset='custom';}}}
|
|
67
|
+
if(kind==='preset')selectedField=1;
|
|
68
|
+
}
|
|
13
69
|
else if(kind==='install'&&field===1){const value=await ui.menu('Harness output language',['EN','KR'],{backRow:true,selected:settings.language==='ko'?1:0});if(value!==null)settings.language=['en','ko'][value];}
|
|
14
70
|
else if(kind==='install'&&(field===2||field===3)){const key=field===2?'maxTotalSubagents':'maxConcurrentSubagents';let raw=String(settings[key]);while(true){raw=await ui.text(field===2?'Maximum total subagents':'Maximum concurrent subagents',raw);if(raw===null)break;let value;try{value=parsePositiveLimit(raw);}catch{await ui.page('Invalid limit',['Enter a positive safe ASCII decimal integer.']);continue;}settings[key]=value;break;}}
|
|
15
71
|
else if(kind==='install'&&field===4)settings.skipGlobal=!settings.skipGlobal;
|
|
16
|
-
else{return prepare({kind,...setupOptions(draft,target)});}
|
|
72
|
+
else{let catalog;if(settings.preset==='custom'||settings.customEdited){catalog=await catalogFor(target);if(!catalog)continue;try{assertCatalogRoles({platform:target,roles:settings.customRoles,catalog});}catch{await ui.page('Custom preset unavailable',['Select an available model and effort for each role.']);continue;}}return prepare({kind,...setupOptions(draft,target,catalog,kind)});}
|
|
17
73
|
}
|
|
18
74
|
}
|
|
19
75
|
}
|