kaizen-agent 1.7.3 → 1.7.4
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 +658 -657
- package/cli/settings.ts +416 -415
- package/package.json +1 -1
- package/skills/kaizen/config.default.yml +11 -0
- package/web/board.html +47 -23
package/package.json
CHANGED
|
@@ -165,3 +165,14 @@ agent:
|
|
|
165
165
|
# opencode: { model: anthropic/claude-sonnet-4-6 }
|
|
166
166
|
# gemini: { model: gemini-2.5-pro }
|
|
167
167
|
|
|
168
|
+
|
|
169
|
+
dictation:
|
|
170
|
+
# Whisper model the web board dictates with. Runs in the browser; downloaded once
|
|
171
|
+
# from Hugging Face and cached. Bigger is more accurate and better with languages
|
|
172
|
+
# other than English, at the cost of the first-use download and GPU memory:
|
|
173
|
+
# tiny ~ 60 MB fast, rough
|
|
174
|
+
# base ~165 MB quick, English-leaning
|
|
175
|
+
# small ~410 MB accurate, good across languages (default)
|
|
176
|
+
# large-v3-turbo ~1.5 GB best, needs a capable GPU
|
|
177
|
+
# Changing it downloads the new model on the next press of the mic.
|
|
178
|
+
model: small
|
package/web/board.html
CHANGED
|
@@ -28,8 +28,10 @@ pre.mark span:nth-child(1){color:var(--ink-hi)}pre.mark span:nth-child(2){color:
|
|
|
28
28
|
.ib.sm{width:26px;height:26px}.ib.sm svg{width:15px;height:15px}
|
|
29
29
|
/* dictation mic: accent while listening (pulsing) or still transcribing; digits while the model downloads */
|
|
30
30
|
/* the field being dictated into: accent border and a glow that swells with the voice level (--lvl, 0..1) */
|
|
31
|
-
.dictating{border-color:var(--accent)!important;box-shadow:0 0 0 calc(
|
|
32
|
-
|
|
31
|
+
.dictating{outline:none;border-color:var(--accent)!important;box-shadow:0 0 calc(6px + 16px*var(--lvl,0)) calc(1px + 3px*var(--lvl,0)) color-mix(in srgb,var(--accent) 45%,transparent);transition:box-shadow .08s}
|
|
32
|
+
/* the mic button turns into a level meter while listening: five bars, each weighted so the set reads as a wave */
|
|
33
|
+
.vu{display:inline-flex;gap:2px;align-items:center;height:100%}.vu i{width:2px;border-radius:1px;background:currentColor;height:calc(3px + 12px*min(1,var(--lvl,0)*var(--w)));transition:height .08s}
|
|
34
|
+
.ib.mic.on{color:var(--accent)}.ib.mic.busy{color:var(--accent)}.ib.mic i{font-style:normal;font-size:10px;font-variant-numeric:tabular-nums}.row .mic{margin-right:auto;align-self:center}
|
|
33
35
|
.btn{border:1px solid var(--line);border-radius:var(--r);padding:5px 12px;color:var(--ink);background:var(--bg-2);min-height:30px;font-size:13px}
|
|
34
36
|
.btn:hover{border-color:var(--dim)}
|
|
35
37
|
.btn.pri{background:var(--accent);border-color:var(--accent);color:#fff}
|
|
@@ -147,18 +149,22 @@ aside .foot:empty{display:none}
|
|
|
147
149
|
.find input{margin-top:4px}
|
|
148
150
|
.find .sev{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--warn)}.find .sev.hi{color:var(--danger)}
|
|
149
151
|
.find .loc{color:var(--dim);font-size:12px;margin-left:8px}
|
|
150
|
-
|
|
151
|
-
.
|
|
152
|
-
.
|
|
153
|
-
.
|
|
154
|
-
.
|
|
152
|
+
/* settings: the TUI screen, one row per setting — label, then the value in its own grammar */
|
|
153
|
+
.sg{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--dim);margin:18px 0 6px;font-weight:600}.sg:first-child{margin-top:0}
|
|
154
|
+
.srow{display:grid;grid-template-columns:200px 1fr;gap:0 16px;align-items:center;font-size:13px;padding:4px 8px;margin:0 -8px;border-radius:var(--r)}
|
|
155
|
+
.srow:hover,.srow:focus-within{background:color-mix(in srgb,var(--dim) 8%,transparent)}
|
|
156
|
+
.srow label{color:var(--dim)}
|
|
157
|
+
.srow .hint{display:none;grid-column:1/-1;margin:6px 0 2px;font-size:12px;color:var(--dim);line-height:1.45;max-width:60ch}.srow:hover .hint,.srow:focus-within .hint{display:block}.srow .hint code{background:none;padding:0;color:var(--dim)}
|
|
158
|
+
.srow .sel{padding:2px 6px;font-size:13px;justify-self:start;border-color:transparent}.srow:hover .sel,.srow .sel:focus{border-color:var(--line)}
|
|
159
|
+
.chk{justify-self:start;color:var(--ink);padding:2px 0;font-variant-numeric:tabular-nums}.chk:not(.on){color:var(--dim)}.chk i{font-style:normal;color:var(--ink)}
|
|
160
|
+
.bar{display:inline-flex;align-items:center;gap:2px;justify-self:start}.bar button{width:10px;height:14px;border:1px solid var(--dim);border-radius:2px;background:none}.bar button.on{background:var(--ink);border-color:var(--ink)}.bar button:hover{border-color:var(--accent)}.bar b{font-weight:400;margin-left:6px;font-variant-numeric:tabular-nums}
|
|
155
161
|
.toast{position:fixed;z-index:12;left:24px;bottom:52px;background:var(--ink);color:var(--bg);border-radius:var(--r);padding:8px 14px;font-size:13px;opacity:0;transition:opacity .2s;pointer-events:none}
|
|
156
162
|
.toast.on{opacity:1}
|
|
157
163
|
.help{position:fixed;inset:0;z-index:12;display:none;align-items:center;justify-content:center;background:color-mix(in srgb,var(--bg) 70%,transparent)}
|
|
158
164
|
.help.on{display:flex}.help div{background:var(--bg-2);border:1px solid var(--line);border-radius:var(--r);padding:20px 24px;line-height:2}
|
|
159
165
|
.help kbd{display:inline-block;width:7ch;color:var(--accent)}
|
|
160
166
|
@media(max-width:900px){main{grid-template-columns:1fr;padding-inline:16px;gap:8px}.hdr,.strip,.ftr{padding-inline:16px}section h2{position:sticky;top:0;background:var(--bg);z-index:1}.more{display:grid}.card:hover .age{display:inline}}
|
|
161
|
-
@media(prefers-reduced-motion:reduce){aside,.toast,.mascot,.d-run .pulse,.ib.mic.on,.dictating{transition:none;animation:none}}
|
|
167
|
+
@media(prefers-reduced-motion:reduce){aside,.toast,.mascot,.d-run .pulse,.ib.mic.on,.dictating,.vu i{transition:none;animation:none}}
|
|
162
168
|
</style>
|
|
163
169
|
</head>
|
|
164
170
|
<body>
|
|
@@ -227,15 +233,17 @@ const ico=n=>`<svg><use href="#i-${n}"/></svg>`;
|
|
|
227
233
|
|
|
228
234
|
// ---- dictation: Whisper in the page. Audio never leaves the browser; the model is
|
|
229
235
|
// fetched once from huggingface.co and cached. Library and model are pinned here.
|
|
230
|
-
const TFJS="https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.3.0",
|
|
236
|
+
const TFJS="https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.3.0",ASR_MODELS={tiny:"onnx-community/whisper-tiny",base:"onnx-community/whisper-base",small:"onnx-community/whisper-small","large-v3-turbo":"onnx-community/whisper-large-v3-turbo"};
|
|
237
|
+
// The model is a setting (dictation.model); the worker is rebuilt when it changes.
|
|
238
|
+
let asrModel=null;
|
|
231
239
|
// Inference lives in a module Worker so a slow chunk never freezes the board.
|
|
232
240
|
const WORKER_SRC=`import{pipeline}from"${TFJS}";let asr=null;
|
|
233
241
|
onmessage=async e=>{const m=e.data;try{
|
|
234
242
|
if(m.type==="load"){const prog=p=>{if(p.status==="progress")postMessage({type:"progress",file:p.file,pct:p.progress||0})};
|
|
235
243
|
// WebGPU first, with the encoder in fp16 only where the adapter has shader-f16; any GPU failure falls back to the CPU.
|
|
236
244
|
const gpu=m.webgpu?{device:"webgpu",dtype:{encoder_model:m.f16?"fp16":"fp32",decoder_model_merged:"q4"}}:null,cpu={device:"wasm",dtype:"q8"};
|
|
237
|
-
try{if(!gpu)throw 0;asr=await pipeline("automatic-speech-recognition",
|
|
238
|
-
catch(e){if(gpu)postMessage({type:"fallback",why:String(e&&e.message||e)});asr=await pipeline("automatic-speech-recognition",
|
|
245
|
+
try{if(!gpu)throw 0;asr=await pipeline("automatic-speech-recognition",m.model,{...gpu,progress_callback:prog})}
|
|
246
|
+
catch(e){if(gpu)postMessage({type:"fallback",why:String(e&&e.message||e)});asr=await pipeline("automatic-speech-recognition",m.model,{...cpu,progress_callback:prog})}
|
|
239
247
|
postMessage({type:"ready"})}
|
|
240
248
|
if(m.type==="run"){const r=await asr(m.audio,{chunk_length_s:30,return_timestamps:false});postMessage({type:"text",sid:m.sid,seq:m.seq,text:(r.text||"").trim()})}
|
|
241
249
|
}catch(err){postMessage({type:"error",sid:m.sid,seq:m.seq,error:String(err&&err.message||err)})}}`;
|
|
@@ -245,13 +253,18 @@ const HAS_MIC=!!(navigator.mediaDevices&&navigator.mediaDevices.getUserMedia&&wi
|
|
|
245
253
|
// `active` is the one session listening; `sessions` also holds stopped ones still waiting on a chunk.
|
|
246
254
|
let asrWorker=null,asrReady=null,asrProgress=()=>{},active=null,starting=false;const sessions=new Set();
|
|
247
255
|
// One worker for the page; loading is awaited by whoever pressed first, later presses join.
|
|
248
|
-
|
|
256
|
+
// Loads the model named in settings; a model changed since the last load replaces the worker.
|
|
257
|
+
async function loadAsr(){
|
|
258
|
+
let want="small";try{const cfg=await fetch("/settings").then(r=>r.json());want=(cfg.rows.find(r=>r.key==="dictation.model")||{}).value||want}catch(e){}
|
|
259
|
+
if(!ASR_MODELS[want])want="small";
|
|
260
|
+
if(asrReady&&asrModel!==want){try{asrWorker.terminate()}catch(e){}asrWorker=null;asrReady=null;sessions.clear();active=null}
|
|
261
|
+
if(asrReady)return asrReady;asrModel=want;asrReady=new Promise(async(res,rej)=>{
|
|
249
262
|
asrWorker=new Worker(URL.createObjectURL(new Blob([WORKER_SRC],{type:"text/javascript"})),{type:"module"});
|
|
250
263
|
const files={};asrWorker.onmessage=e=>{const m=e.data;if(m.type==="progress"){files[m.file]=m.pct;const v=Object.values(files);asrProgress(Math.round(v.reduce((a,b)=>a+b,0)/v.length))}else if(m.type==="ready")res();else if(m.type==="fallback")toast("GPU could not run the model, using the CPU: "+m.why);else if(m.type==="error"&&m.seq==null)rej(new Error(m.error));for(const s of sessions)s.onmsg(m)};
|
|
251
264
|
asrWorker.onerror=e=>rej(new Error(e.message||"worker failed"));
|
|
252
265
|
// Ask the adapter what it can do: no adapter means the CPU path; no shader-f16 means an fp32 encoder.
|
|
253
266
|
let adapter=null;try{adapter=navigator.gpu&&await navigator.gpu.requestAdapter()}catch(e){}
|
|
254
|
-
if(!adapter)toast("No WebGPU: dictation will run on the CPU and be slower");asrWorker.postMessage({type:"load",webgpu:!!adapter,f16:!!(adapter&&adapter.features.has("shader-f16"))})});
|
|
267
|
+
if(!adapter)toast("No WebGPU: dictation will run on the CPU and be slower");asrWorker.postMessage({type:"load",model:ASR_MODELS[asrModel],webgpu:!!adapter,f16:!!(adapter&&adapter.features.has("shader-f16"))})});
|
|
255
268
|
asrReady.catch(()=>{asrReady=null;try{asrWorker.terminate()}catch(e){}asrWorker=null});return asrReady}
|
|
256
269
|
// After a re-render only a listener whose field is gone is stopped; closing the panel stops any.
|
|
257
270
|
function stopDictation(all){if(active&&(all||!active.field.isConnected))active.stop()}
|
|
@@ -275,20 +288,21 @@ function mic(fields){
|
|
|
275
288
|
// Text lands where the caret is when each chunk arrives (or after the last insertion when the
|
|
276
289
|
// field is not focused), so typing while the mic is on is kept.
|
|
277
290
|
const f=target;let at=f.selectionStart??f.value.length;
|
|
291
|
+
const vu=document.createElement("span");vu.className="vu";vu.setAttribute("aria-hidden","true");vu.innerHTML=[.5,.9,1.3,.8,.45].map(w=>`<i style="--w:${w}"></i>`).join("");
|
|
278
292
|
let buf=[],len=0,voiced=0,quiet=0,seq=0,done=0,pending=new Map(),stopping=false;
|
|
279
293
|
const SR=16000,MAX=6*SR,SIL=0.8*SR,MIN=1*SR;
|
|
280
294
|
const insert=t=>{if(!t||!f.isConnected)return;const v=f.value;if(document.activeElement===f)at=f.selectionStart;at=Math.min(at,v.length);const head=v.slice(0,at),sep=head&&!/\s$/.test(head)?" ":"";f.value=head+sep+t+v.slice(at);at=head.length+sep.length+t.length;f.selectionStart=f.selectionEnd=at;f.dispatchEvent(new Event("input",{bubbles:true}))};
|
|
281
295
|
const flush=()=>{if(!len||voiced<MIN){buf=[];len=0;voiced=0;quiet=0;return}const a=new Float32Array(len);let o=0;for(const c of buf){a.set(c,o);o+=c.length}buf=[];len=0;voiced=0;quiet=0;pending.set(seq,null);asrWorker.postMessage({type:"run",sid,seq,audio:a},[a.buffer]);seq++;b.classList.add("busy")};
|
|
282
296
|
const drain=()=>{while(pending.has(done)&&pending.get(done)!==null){insert(pending.get(done));pending.delete(done);done++}if(!pending.size){b.classList.remove("busy");if(stopping){idle();sessions.delete(sess)}}};
|
|
283
297
|
tap.port.onmessage=e=>{const c=e.data;buf.push(c);len+=c.length;let s=0;for(let i=0;i<c.length;i++)s+=c[i]*c[i];const rms=Math.sqrt(s/c.length);
|
|
284
|
-
if(rms>0.01){voiced+=c.length;quiet=0}else quiet+=c.length;
|
|
298
|
+
if(rms>0.01){voiced+=c.length;quiet=0}else quiet+=c.length;{const l=Math.min(1,rms*8).toFixed(2);f.style.setProperty("--lvl",l);vu.style.setProperty("--lvl",l)}
|
|
285
299
|
if(len>=MAX||(voiced>=MIN&&quiet>=SIL))flush()};
|
|
286
300
|
// Chunk seqs are per session, so a result is matched by session id too.
|
|
287
301
|
const sid=Math.random().toString(36).slice(2);
|
|
288
302
|
const sess={btn:b,field:f,onmsg:m=>{if(m.sid!==sid)return;if(m.type==="text"){pending.set(m.seq,m.text);drain()}else if(m.type==="error"&&m.seq!=null){pending.set(m.seq,"");toast("Dictation failed: "+m.error);drain()}},
|
|
289
|
-
stop:()=>{if(stopping)return;stopping=true;b.classList.remove("on");f.classList.remove("dictating");f.style.removeProperty("--lvl");try{tap.port.onmessage=null;src.disconnect();tap.disconnect();stream.getTracks().forEach(t=>t.stop());ctx.close()}catch(e){}active=null;flush();drain()}};
|
|
303
|
+
stop:()=>{if(stopping)return;stopping=true;b.classList.remove("on");f.classList.remove("dictating");f.style.removeProperty("--lvl");b.innerHTML=ico("mic");try{tap.port.onmessage=null;src.disconnect();tap.disconnect();stream.getTracks().forEach(t=>t.stop());ctx.close()}catch(e){}active=null;flush();drain()}};
|
|
290
304
|
sessions.add(sess);active=sess;starting=false;
|
|
291
|
-
b.classList.remove("busy");b.classList.add("on");f.classList.add("dictating");f.focus()};
|
|
305
|
+
b.classList.remove("busy");b.classList.add("on");f.classList.add("dictating");b.replaceChildren(vu);f.focus()};
|
|
292
306
|
return b}
|
|
293
307
|
const m=60e3,h=3600e3,d=86400e3;
|
|
294
308
|
const age=(t,now)=>{const x=now-t;return x<h?Math.round(x/m)+"m":x<d?Math.round(x/h)+"h":Math.round(x/d)+"d"};
|
|
@@ -480,19 +494,29 @@ function md(src){
|
|
|
480
494
|
endList();return out.join("")}
|
|
481
495
|
|
|
482
496
|
// ---- settings, toast, notifications, keys
|
|
497
|
+
// Rows in config order, grouped as the TUI groups them.
|
|
498
|
+
const groups=rows=>{const m=new Map();for(const r of rows){if(!m.has(r.group))m.set(r.group,[]);m.get(r.group).push(r)}return [...m]};
|
|
483
499
|
async function openSettings(){
|
|
484
500
|
stopDictation(true);
|
|
485
501
|
openKey=null;runDoc=null;$("panel").classList.add("open");$("scrim").classList.add("on");$("ptitle").textContent="Settings";$("ptabs").innerHTML="";$("pfoot").innerHTML="";
|
|
486
502
|
const cur=document.documentElement.dataset.theme||"system";
|
|
487
|
-
const opts=(id,vals,on)=>`<div class="opts" id="${id}">${vals.map(v=>`<button class="${v===on?"on":""}" data-v="${v}" ${v===""?'title="empty"':""}>${v===""?"—":v}</button>`).join("")}</div>`;
|
|
488
503
|
const cfg=await fetch("/settings").then(r=>r.json()).catch(()=>null);
|
|
489
504
|
$("psub").textContent=cfg?`theme is this browser's; the rest is ${cfg.file}`:"theme is this browser's";
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
505
|
+
// One control per row, in the TUI's grammar: [✓] on / [ ] off for a switch, ▮▮▯ bars for a
|
|
506
|
+
// number, the word itself (a select) for a choice. Help shows for the row under the pointer.
|
|
507
|
+
const ctl=(id,vals,on)=>{
|
|
508
|
+
if(vals.length===2&&vals.includes("true")&&vals.includes("false")){const yes=on==="true";return `<button class="chk${yes?" on":""}" id="${id}" data-v="${yes?"false":"true"}" role="switch" aria-checked="${yes}"><i>[${yes?"✓":" "}]</i> ${yes?"on":"off"}</button>`}
|
|
509
|
+
if(vals.every(v=>/^\d+$/.test(v))){const at=vals.indexOf(on);return `<span class="bar" id="${id}">${vals.map((v,i)=>`<button data-v="${v}" class="${i<=at?"on":""}" title="${v}"></button>`).join("")}<b>${esc(on)}</b></span>`}
|
|
510
|
+
return `<select class="sel" id="${id}">${vals.map(v=>`<option value="${esc(v)}"${v===on?" selected":""}>${v===""?"—":esc(v)}</option>`).join("")}</select>`};
|
|
511
|
+
const row=(label,control,hint)=>`<div class="srow"><label>${esc(label)}</label>${control}<p class="hint">${hint}</p></div>`;
|
|
512
|
+
$("pbody").innerHTML=`<h3 class="sg">Browser</h3>${row("Theme",ctl("th",["system","light","dark"],cur),"Light or dark for this browser only; system follows the OS.")}
|
|
513
|
+
${row("Notifications",`<button class="chk" id="nt"><i>[${typeof Notification!=="undefined"&&Notification.permission==="granted"?"✓":" "}]</i> ${typeof Notification==="undefined"?"unsupported":Notification.permission==="granted"?"on":"ask"}</button>`,"A desktop notification when a run starts waiting on you.")}
|
|
514
|
+
${cfg?groups(cfg.rows).map(([g,rows])=>`<h3 class="sg">${esc(g)}</h3>${rows.map(r=>row(r.label,ctl("s-"+r.key.replace(/\./g,"-"),r.values,r.value),`<code>${esc(r.key)}</code> options: ${r.values.map(v=>v||'""').map(esc).join(" / ")}<br>${esc(r.help)}`)).join("")}`).join(""):`<p style="color:var(--dim);margin-top:20px">Settings could not be read. Run <code>kaizen settings</code>.</p>`}`;
|
|
515
|
+
const setTheme=t=>{if(t==="system")delete document.documentElement.dataset.theme;else document.documentElement.dataset.theme=t;try{t==="system"?localStorage.removeItem("kz-theme"):localStorage.setItem("kz-theme",t)}catch(e){}};
|
|
516
|
+
$("th").onchange=e=>setTheme(e.target.value);
|
|
517
|
+
$("nt").onclick=()=>{askNotify();setTimeout(openSettings,300)};
|
|
518
|
+
if(cfg)cfg.rows.forEach(r=>{const el=$("s-"+r.key.replace(/\./g,"-"));const save=async v=>{const j=await post("/settings",{key:r.key,value:v});if(j.ok)toast(`saved ${r.key} = ${v||'""'}`);openSettings()};
|
|
519
|
+
if(el.tagName==="SELECT")el.onchange=()=>save(el.value);else el.querySelectorAll("[data-v]").forEach(b=>b.onclick=()=>save(b.dataset.v));if(el.matches(".chk"))el.onclick=()=>save(el.dataset.v)})}
|
|
496
520
|
let tt;function toast(s){const t=$("toast");t.textContent=s;t.classList.add("on");clearTimeout(tt);tt=setTimeout(()=>t.classList.remove("on"),4000)}
|
|
497
521
|
function askNotify(){try{if(typeof Notification!=="undefined"&&Notification.permission==="default")Notification.requestPermission()}catch(e){}}
|
|
498
522
|
function notifyUser(title,body){try{if(typeof Notification!=="undefined"&&Notification.permission==="granted")new Notification(title,{body})}catch(e){}}
|