openvibers/openvibe.link Commits
Link's window: Codes on this computer (#129)
Changes
@@@ -101,6 +101,32 @@ type Activity struct {Text string `json:"text"`}++// CodesAgent is one coding-agent CLI on this computer: ready, needs signing in (Hint is the command), or missing.++type CodesAgent struct {++ ID string `json:"id"`++ Name string `json:"name"`++ State string `json:"state"`++ Version string `json:"version,omitempty"`++ Hint string `json:"hint,omitempty"`++}++++// CodesTask is one Codes task that runs, or ran, on this computer.++type CodesTask struct {++ ID string `json:"id"`++ Title string `json:"title"`++ Folder string `json:"folder,omitempty"`++ Agent string `json:"agent,omitempty"`++ State string `json:"state"`++ StartedAt time.Time `json:"started_at,omitempty"`++ URL string `json:"url,omitempty"`++}++++// Codes is what the window's Codes section reads.++type Codes struct {++ Agents []CodesAgent `json:"agents,omitempty"`++ Tasks []CodesTask `json:"tasks,omitempty"`++}++// State is GET /panel/api/state. Every key is optional: a section with nothing in it is left out.type State struct {Machine *Machine `json:"machine,omitempty"`@@@ -111,6 +137,8 @@ type State struct {GPU *GPU `json:"gpu,omitempty"`EarningsToday *Earnings `json:"earnings_today,omitempty"`Activity []Activity `json:"activity,omitempty"`++ // Codes is the desktop app's Codes module (§13.1): the agent CLIs on this computer and the Codes tasks here.++ Codes *Codes `json:"codes,omitempty"`// FirstRun is true until the person has answered "What's this computer for?" or skipped it.FirstRun bool `json:"first_run,omitempty"`}@@@ -154,6 +154,7 @@ func TestStateShapeMatchesTheDesign(t *testing.T) {EarningsToday: &Earnings{Coins: 120},Activity: []Activity{{At: now, Text: "x"}},FirstRun: true,++ Codes: &Codes{Agents: []CodesAgent{{ID: "claude-code", Name: "Claude Code", State: "ready"}}, Tasks: []CodesTask{{ID: "t", Title: "t", State: "running", StartedAt: now}}},}b, err := json.Marshal(full)if err != nil {@@@ -67,6 +67,14 @@</div><div class="lk-col lk-col-wide">++ <section class="lk-sec lk-codes" data-lk-sec="codes" aria-labelledby="lk-codes-h" hidden>++ <h2 id="lk-codes-h">Codes on this computer <span data-lk-codes-n></span></h2>++ <ul class="lk-agents" data-lk-agents aria-label="Coding agents on this computer"></ul>++ <ul class="lk-tasks" data-lk-tasks></ul>++ <p class="lk-none" data-lk-tasks-none>No coding task here yet. Start one from Codes and pick this computer.</p>++ <p class="lk-codes-acts"><a class="lk-btn is-primary" data-lk-codes-new href="https://openvibe.codes/" target="_blank" rel="noopener">New task on this computer</a><a class="lk-btn" href="https://openvibe.codes/tasks" target="_blank" rel="noopener">All your tasks</a></p>++ </section>++<section class="lk-sec" data-lk-sec="gpu" aria-labelledby="lk-gpu-h" hidden><h2 id="lk-gpu-h">Models on this computer</h2><p class="lk-gpu-name" data-lk-gpu-name></p>@@@ -63,6 +63,30 @@ button, input { font: inherit; color: inherit; }.lk-btn { min-height: 36px; padding: 0 12px; border-radius: 999px; border: 1px solid var(--line); background: transparent; cursor: pointer; font-weight: 700; font-size: 13px; }.lk-btn:hover { background: color-mix(in srgb, var(--text) 7%, transparent); }.lk-btn.is-primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }++/* Codes on this computer (the desktop app's Codes module, §13.1): which agent CLIs are here and ready, and the tasks. */++.lk-agents { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }++.lk-agent { display: inline-flex; align-items: center; gap: 7px; min-height: 32px; padding: 4px 10px; border-radius: 999px; border: 1px solid var(--line); font-size: 13px; font-weight: 650; }++.lk-agent::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: var(--muted); flex: none; }++.lk-agent[data-state="ready"]::before { background: var(--ok); }++.lk-agent[data-state="signin"]::before { background: var(--warn); }++.lk-agent[data-state="missing"] { color: var(--muted); border-style: dashed; }++.lk-agent small { color: var(--muted); font-weight: 500; }++.lk-agent code { font: 12px/1 ui-monospace, SFMono-Regular, Menlo, monospace; padding: 3px 6px; border-radius: 6px; background: var(--surface-2); color: var(--text); }++.lk-agent button { min-height: 24px; padding: 0 8px; border-radius: 999px; border: 0; background: color-mix(in srgb, var(--text) 9%, transparent); cursor: pointer; font-size: 12px; font-weight: 700; }++.lk-tasks { display: grid; gap: 6px; }++.lk-tasks li { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 10px; padding: 8px 8px 8px 12px; border-radius: 12px; background: var(--surface-2); }++.lk-tasks b { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }++.lk-tasks small { display: flex; flex-wrap: wrap; gap: 2px 8px; color: var(--muted); font-size: 12px; min-width: 0; }++.lk-tasks code { font: 12px/1.3 ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }++.lk-task-acts { display: flex; gap: 6px; }++.lk-state { font-weight: 700; }++.lk-state[data-state="running"] { color: var(--accent-2); }++.lk-state[data-state="waiting"] { color: var(--warn); }++.lk-state[data-state="done"] { color: var(--ok); }++.lk-state[data-state="failed"], .lk-state[data-state="stopped"] { color: var(--bad); }++.lk-codes-acts { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0 0; }++a.lk-btn { display: inline-flex; align-items: center; text-decoration: none; color: inherit; }++a.lk-btn.is-primary { color: var(--on-accent); }.lk-btn.is-stop { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 45%, var(--line)); }/* Capabilities: a switch each, and who may use it in one line. */@@@ -21,6 +21,55 @@.then(function () { load(); });}++ // Codes on this computer: each agent CLI's state in words, and the tasks with Open and (while working) Stop.++ var AGENT_STATE = { ready: 'ready', signin: 'sign in', missing: 'not installed' };++ var TASK_STATE = { running: 'Working', waiting: 'Waiting for you', done: 'Done', failed: 'Didn\u2019t finish', stopped: 'Stopped' };++ function drawCodes(c, machine) {++ var sec = root.querySelector('[data-lk-sec="codes"]');++ sec.hidden = !c;++ if (!c) return;++ var agents = c.agents || [], tasks = c.tasks || [];++ $('[data-lk-agents]').replaceChildren.apply($('[data-lk-agents]'), agents.map(function (a) {++ var li = el('li', 'lk-agent'); li.setAttribute('data-state', a.state || 'missing');++ li.append(document.createTextNode(a.name || a.id), el('small', '', AGENT_STATE[a.state] || a.state || ''));++ if (a.state === 'signin' && a.hint) {++ var code = el('code', '', a.hint); var copy = el('button', '', 'Copy'); copy.type = 'button';++ copy.setAttribute('aria-label', 'Copy ' + a.hint);++ copy.addEventListener('click', function () { if (navigator.clipboard) navigator.clipboard.writeText(a.hint).then(function () { copy.textContent = 'Copied'; }); });++ li.append(code, copy);++ }++ return li;++ }));++ $('[data-lk-agents]').hidden = agents.length === 0;++ var open = tasks.filter(function (t) { return t.state === 'running' || t.state === 'waiting'; }).length;++ $('[data-lk-codes-n]').textContent = open ? '(' + open + ' working)' : '';++ $('[data-lk-tasks]').replaceChildren.apply($('[data-lk-tasks]'), tasks.map(function (t) {++ var li = el('li'); var main = el('span');++ var meta = el('small');++ var st = el('span', 'lk-state', TASK_STATE[t.state] || t.state || ''); st.setAttribute('data-state', t.state || '');++ meta.append(st);++ if (t.agent) meta.append(el('span', '', t.agent));++ if (t.started_at) meta.append(el('span', '', ago(t.started_at)));++ main.append(el('b', '', t.title || 'A coding task'));++ if (t.folder) main.append(el('code', '', t.folder));++ main.append(meta);++ var acts = el('span', 'lk-task-acts');++ if (t.url && /^https:\/\/openvibe\.codes\//.test(t.url)) {++ var a = el('a', 'lk-btn', t.state === 'waiting' ? 'Answer' : 'Open'); a.href = t.url; a.target = '_blank'; a.rel = 'noopener';++ if (t.state === 'waiting') a.className = 'lk-btn is-primary';++ acts.append(a);++ }++ if (t.state === 'running') {++ var stop = el('button', 'lk-btn is-stop', 'Stop'); stop.type = 'button'; stop.setAttribute('aria-label', 'Stop ' + (t.title || 'this task'));++ stop.addEventListener('click', function () { stop.disabled = true; send('codes/tasks/' + encodeURIComponent(t.id) + '/stop').catch(function () { stop.disabled = false; }); });++ acts.append(stop);++ }++ li.append(main, acts); return li;++ }));++ $('[data-lk-tasks-none]').hidden = tasks.length > 0;++ var nw = $('[data-lk-codes-new]');++ if (machine && machine.id) nw.href = 'https://openvibe.codes/?machine=' + encodeURIComponent(machine.id);++ }function draw(s) {var m = s.machine || {};var running = Array.isArray(s.running) ? s.running : [];@@@ -31,6 +80,7 @@$('[data-lk-down]').hidden = true;$('[data-lk-first]').hidden = !s.first_run;$('[data-lk-name]').textContent = m.name || 'This computer';++ drawCodes(s.codes, m);$('[data-lk-status]').textContent = m.paused ? 'Paused: nothing runs until you resume': m.needs_you ? (m.needs_you === 1 ? 'Online · something needs you' : 'Online · ' + m.needs_you + ' things need you'): running.length ? 'Online · ' + running.length + ' running' : 'Online · ready';@@@ -1,28 +1,158 @@{"first_run": true,-- "machine": { "name": "Workshop desktop", "os": "linux", "online": true, "paused": false, "needs_you": 1 },++ "machine": {++ "name": "Workshop desktop",++ "os": "linux",++ "online": true,++ "paused": false,++ "needs_you": 1++ },"running": [-- { "id": "r_1", "kind": "agent", "title": "Vibe is comparing three hosting plans", "detail": "Browser", "started_at": "2026-11-07T10:02:00Z" },-- { "id": "r_2", "kind": "desk", "title": "A desk for Codes: drive redesign", "detail": "Codes", "started_at": "2026-11-07T09:40:00Z" },-- { "id": "r_3", "kind": "share", "title": "Port 3000 shared", "detail": "Only you", "started_at": "2026-11-07T09:10:00Z" }++ {++ "id": "r_1",++ "kind": "agent",++ "title": "Vibe is comparing three hosting plans",++ "detail": "Browser",++ "started_at": "2026-11-07T10:02:00Z"++ },++ {++ "id": "r_2",++ "kind": "desk",++ "title": "A desk for Codes: drive redesign",++ "detail": "Codes",++ "started_at": "2026-11-07T09:40:00Z"++ },++ {++ "id": "r_3",++ "kind": "share",++ "title": "Port 3000 shared",++ "detail": "Only you",++ "started_at": "2026-11-07T09:10:00Z"++ }],"capabilities": [-- { "id": "browser", "name": "A browser for your agents", "who": "Your agents, after you approve each task", "on": true },-- { "id": "files", "name": "Files between your devices", "who": "Only you", "on": true },-- { "id": "models", "name": "Run AI models here", "who": "Only you, from your other devices", "on": false },-- { "id": "earn", "name": "Earn with spare power", "who": "OpenVibe Exchange, when this computer is idle", "on": false }++ {++ "id": "browser",++ "name": "A browser for your agents",++ "who": "Your agents, after you approve each task",++ "on": true++ },++ {++ "id": "files",++ "name": "Files between your devices",++ "who": "Only you",++ "on": true++ },++ {++ "id": "models",++ "name": "Run AI models here",++ "who": "Only you, from your other devices",++ "on": false++ },++ {++ "id": "earn",++ "name": "Earn with spare power",++ "who": "OpenVibe Exchange, when this computer is idle",++ "on": false++ }++ ],++ "shares": [++ {++ "port": 3000,++ "url": "https://dev--alex.openvibe.website",++ "expires_at": "2026-11-07T22:00:00Z"++ }],-- "shares": [ { "port": 3000, "url": "https://dev--alex.openvibe.website", "expires_at": "2026-11-07T22:00:00Z" } ],"devices": [-- { "name": "Workshop desktop", "kind": "desktop", "online": true, "here": true },-- { "name": "Laptop", "kind": "laptop", "online": true, "here": false },-- { "name": "Pixel 8", "kind": "phone", "online": false, "here": false }++ {++ "name": "Workshop desktop",++ "kind": "desktop",++ "online": true,++ "here": true++ },++ {++ "name": "Laptop",++ "kind": "laptop",++ "online": true,++ "here": false++ },++ {++ "name": "Pixel 8",++ "kind": "phone",++ "online": false,++ "here": false++ }],"gpu": null,"earnings_today": null,"activity": [-- { "at": "2026-11-07T10:02:00Z", "text": "Vibe opened a browser to compare hosting plans" },-- { "at": "2026-11-07T09:40:00Z", "text": "Codes started a desk for the drive redesign" },-- { "at": "2026-11-07T09:10:00Z", "text": "You shared port 3000" }-- ]++ {++ "at": "2026-11-07T10:02:00Z",++ "text": "Vibe opened a browser to compare hosting plans"++ },++ {++ "at": "2026-11-07T09:40:00Z",++ "text": "Codes started a desk for the drive redesign"++ },++ {++ "at": "2026-11-07T09:10:00Z",++ "text": "You shared port 3000"++ }++ ],++ "codes": {++ "agents": [++ {++ "id": "claude-code",++ "name": "Claude Code",++ "state": "ready",++ "version": "2.1.3"++ },++ {++ "id": "codex",++ "name": "Codex",++ "state": "signin",++ "hint": "codex login"++ },++ {++ "id": "opencode",++ "name": "OpenCode",++ "state": "ready",++ "version": "0.9.2"++ },++ {++ "id": "gemini",++ "name": "Gemini CLI",++ "state": "missing"++ }++ ],++ "tasks": [++ {++ "id": "tsk_01M6Q1",++ "title": "Make every page work at 320 px",++ "folder": "~/code/openvibe-site",++ "agent": "Claude Code",++ "state": "running",++ "started_at": "2026-11-07T09:52:00Z",++ "url": "https://openvibe.codes/tasks/tsk_01M6Q1"++ },++ {++ "id": "tsk_01M6P8",++ "title": "Fix the flaky upload test",++ "folder": "~/code/drive",++ "agent": "Codex",++ "state": "waiting",++ "started_at": "2026-11-07T09:31:00Z",++ "url": "https://openvibe.codes/tasks/tsk_01M6P8"++ },++ {++ "id": "tsk_01M6N2",++ "title": "Add a dark mode that follows the system",++ "folder": "~/code/notes-app",++ "agent": "OpenCode",++ "state": "done",++ "started_at": "2026-11-07T08:05:00Z",++ "url": "https://openvibe.codes/tasks/tsk_01M6N2"++ }++ ]++ }}
@@ -101,6 +101,32 @@ type Activity struct {
@@ -101,6 +101,32 @@ type Activity struct {
Text string `json:"text"`
Text string `json:"text"`
}
}
+// CodesAgent is one coding-agent CLI on this computer: ready, needs signing in (Hint is the command), or missing.
+type CodesAgent struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ State string `json:"state"`
+ Version string `json:"version,omitempty"`
+ Hint string `json:"hint,omitempty"`
+}
+
+// CodesTask is one Codes task that runs, or ran, on this computer.
+type CodesTask struct {
+ ID string `json:"id"`
+ Title string `json:"title"`
+ Folder string `json:"folder,omitempty"`
+ Agent string `json:"agent,omitempty"`
+ State string `json:"state"`
+ StartedAt time.Time `json:"started_at,omitempty"`
+ URL string `json:"url,omitempty"`
+}
+
+// Codes is what the window's Codes section reads.
+type Codes struct {
+ Agents []CodesAgent `json:"agents,omitempty"`
+ Tasks []CodesTask `json:"tasks,omitempty"`
+}
+
// State is GET /panel/api/state. Every key is optional: a section with nothing in it is left out.
// State is GET /panel/api/state. Every key is optional: a section with nothing in it is left out.
type State struct {
type State struct {
Machine *Machine `json:"machine,omitempty"`
Machine *Machine `json:"machine,omitempty"`
@@ -111,6 +137,8 @@ type State struct {
@@ -111,6 +137,8 @@ type State struct {
GPU *GPU `json:"gpu,omitempty"`
GPU *GPU `json:"gpu,omitempty"`
EarningsToday *Earnings `json:"earnings_today,omitempty"`
EarningsToday *Earnings `json:"earnings_today,omitempty"`
Activity []Activity `json:"activity,omitempty"`
Activity []Activity `json:"activity,omitempty"`
+ // Codes is the desktop app's Codes module (§13.1): the agent CLIs on this computer and the Codes tasks here.
+ Codes *Codes `json:"codes,omitempty"`
// FirstRun is true until the person has answered "What's this computer for?" or skipped it.
// FirstRun is true until the person has answered "What's this computer for?" or skipped it.
FirstRun bool `json:"first_run,omitempty"`
FirstRun bool `json:"first_run,omitempty"`
}
}
@@ -154,6 +154,7 @@ func TestStateShapeMatchesTheDesign(t *testing.T) {
@@ -154,6 +154,7 @@ func TestStateShapeMatchesTheDesign(t *testing.T) {
EarningsToday: &Earnings{Coins: 120},
EarningsToday: &Earnings{Coins: 120},
Activity: []Activity{{At: now, Text: "x"}},
Activity: []Activity{{At: now, Text: "x"}},
FirstRun: true,
FirstRun: true,
+ Codes: &Codes{Agents: []CodesAgent{{ID: "claude-code", Name: "Claude Code", State: "ready"}}, Tasks: []CodesTask{{ID: "t", Title: "t", State: "running", StartedAt: now}}},
}
}
b, err := json.Marshal(full)
b, err := json.Marshal(full)
if err != nil {
if err != nil {
@@ -67,6 +67,14 @@
@@ -67,6 +67,14 @@
</div>
</div>
<div class="lk-col lk-col-wide">
<div class="lk-col lk-col-wide">
+ <section class="lk-sec lk-codes" data-lk-sec="codes" aria-labelledby="lk-codes-h" hidden>
+ <h2 id="lk-codes-h">Codes on this computer <span data-lk-codes-n></span></h2>
+ <ul class="lk-agents" data-lk-agents aria-label="Coding agents on this computer"></ul>
+ <ul class="lk-tasks" data-lk-tasks></ul>
+ <p class="lk-none" data-lk-tasks-none>No coding task here yet. Start one from Codes and pick this computer.</p>
+ <p class="lk-codes-acts"><a class="lk-btn is-primary" data-lk-codes-new href="https://openvibe.codes/" target="_blank" rel="noopener">New task on this computer</a><a class="lk-btn" href="https://openvibe.codes/tasks" target="_blank" rel="noopener">All your tasks</a></p>
+ </section>
+
<section class="lk-sec" data-lk-sec="gpu" aria-labelledby="lk-gpu-h" hidden>
<section class="lk-sec" data-lk-sec="gpu" aria-labelledby="lk-gpu-h" hidden>
<h2 id="lk-gpu-h">Models on this computer</h2>
<h2 id="lk-gpu-h">Models on this computer</h2>
<p class="lk-gpu-name" data-lk-gpu-name></p>
<p class="lk-gpu-name" data-lk-gpu-name></p>
@@ -63,6 +63,30 @@ button, input { font: inherit; color: inherit; }
@@ -63,6 +63,30 @@ button, input { font: inherit; color: inherit; }
.lk-btn { min-height: 36px; padding: 0 12px; border-radius: 999px; border: 1px solid var(--line); background: transparent; cursor: pointer; font-weight: 700; font-size: 13px; }
.lk-btn { min-height: 36px; padding: 0 12px; border-radius: 999px; border: 1px solid var(--line); background: transparent; cursor: pointer; font-weight: 700; font-size: 13px; }
.lk-btn:hover { background: color-mix(in srgb, var(--text) 7%, transparent); }
.lk-btn:hover { background: color-mix(in srgb, var(--text) 7%, transparent); }
.lk-btn.is-primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.lk-btn.is-primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
+/* Codes on this computer (the desktop app's Codes module, §13.1): which agent CLIs are here and ready, and the tasks. */
+.lk-agents { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
+.lk-agent { display: inline-flex; align-items: center; gap: 7px; min-height: 32px; padding: 4px 10px; border-radius: 999px; border: 1px solid var(--line); font-size: 13px; font-weight: 650; }
+.lk-agent::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: var(--muted); flex: none; }
+.lk-agent[data-state="ready"]::before { background: var(--ok); }
+.lk-agent[data-state="signin"]::before { background: var(--warn); }
+.lk-agent[data-state="missing"] { color: var(--muted); border-style: dashed; }
+.lk-agent small { color: var(--muted); font-weight: 500; }
+.lk-agent code { font: 12px/1 ui-monospace, SFMono-Regular, Menlo, monospace; padding: 3px 6px; border-radius: 6px; background: var(--surface-2); color: var(--text); }
+.lk-agent button { min-height: 24px; padding: 0 8px; border-radius: 999px; border: 0; background: color-mix(in srgb, var(--text) 9%, transparent); cursor: pointer; font-size: 12px; font-weight: 700; }
+.lk-tasks { display: grid; gap: 6px; }
+.lk-tasks li { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 10px; padding: 8px 8px 8px 12px; border-radius: 12px; background: var(--surface-2); }
+.lk-tasks b { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.lk-tasks small { display: flex; flex-wrap: wrap; gap: 2px 8px; color: var(--muted); font-size: 12px; min-width: 0; }
+.lk-tasks code { font: 12px/1.3 ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
+.lk-task-acts { display: flex; gap: 6px; }
+.lk-state { font-weight: 700; }
+.lk-state[data-state="running"] { color: var(--accent-2); }
+.lk-state[data-state="waiting"] { color: var(--warn); }
+.lk-state[data-state="done"] { color: var(--ok); }
+.lk-state[data-state="failed"], .lk-state[data-state="stopped"] { color: var(--bad); }
+.lk-codes-acts { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0 0; }
+a.lk-btn { display: inline-flex; align-items: center; text-decoration: none; color: inherit; }
+a.lk-btn.is-primary { color: var(--on-accent); }
.lk-btn.is-stop { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 45%, var(--line)); }
.lk-btn.is-stop { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 45%, var(--line)); }
/* Capabilities: a switch each, and who may use it in one line. */
/* Capabilities: a switch each, and who may use it in one line. */
@@ -21,6 +21,55 @@
@@ -21,6 +21,55 @@
.then(function () { load(); });
.then(function () { load(); });
}
}
+ // Codes on this computer: each agent CLI's state in words, and the tasks with Open and (while working) Stop.
+ var AGENT_STATE = { ready: 'ready', signin: 'sign in', missing: 'not installed' };
+ var TASK_STATE = { running: 'Working', waiting: 'Waiting for you', done: 'Done', failed: 'Didn\u2019t finish', stopped: 'Stopped' };
+ function drawCodes(c, machine) {
+ var sec = root.querySelector('[data-lk-sec="codes"]');
+ sec.hidden = !c;
+ if (!c) return;
+ var agents = c.agents || [], tasks = c.tasks || [];
+ $('[data-lk-agents]').replaceChildren.apply($('[data-lk-agents]'), agents.map(function (a) {
+ var li = el('li', 'lk-agent'); li.setAttribute('data-state', a.state || 'missing');
+ li.append(document.createTextNode(a.name || a.id), el('small', '', AGENT_STATE[a.state] || a.state || ''));
+ if (a.state === 'signin' && a.hint) {
+ var code = el('code', '', a.hint); var copy = el('button', '', 'Copy'); copy.type = 'button';
+ copy.setAttribute('aria-label', 'Copy ' + a.hint);
+ copy.addEventListener('click', function () { if (navigator.clipboard) navigator.clipboard.writeText(a.hint).then(function () { copy.textContent = 'Copied'; }); });
+ li.append(code, copy);
+ }
+ return li;
+ }));
+ $('[data-lk-agents]').hidden = agents.length === 0;
+ var open = tasks.filter(function (t) { return t.state === 'running' || t.state === 'waiting'; }).length;
+ $('[data-lk-codes-n]').textContent = open ? '(' + open + ' working)' : '';
+ $('[data-lk-tasks]').replaceChildren.apply($('[data-lk-tasks]'), tasks.map(function (t) {
+ var li = el('li'); var main = el('span');
+ var meta = el('small');
+ var st = el('span', 'lk-state', TASK_STATE[t.state] || t.state || ''); st.setAttribute('data-state', t.state || '');
+ meta.append(st);
+ if (t.agent) meta.append(el('span', '', t.agent));
+ if (t.started_at) meta.append(el('span', '', ago(t.started_at)));
+ main.append(el('b', '', t.title || 'A coding task'));
+ if (t.folder) main.append(el('code', '', t.folder));
+ main.append(meta);
+ var acts = el('span', 'lk-task-acts');
+ if (t.url && /^https:\/\/openvibe\.codes\//.test(t.url)) {
+ var a = el('a', 'lk-btn', t.state === 'waiting' ? 'Answer' : 'Open'); a.href = t.url; a.target = '_blank'; a.rel = 'noopener';
+ if (t.state === 'waiting') a.className = 'lk-btn is-primary';
+ acts.append(a);
+ }
+ if (t.state === 'running') {
+ var stop = el('button', 'lk-btn is-stop', 'Stop'); stop.type = 'button'; stop.setAttribute('aria-label', 'Stop ' + (t.title || 'this task'));
+ stop.addEventListener('click', function () { stop.disabled = true; send('codes/tasks/' + encodeURIComponent(t.id) + '/stop').catch(function () { stop.disabled = false; }); });
+ acts.append(stop);
+ }
+ li.append(main, acts); return li;
+ }));
+ $('[data-lk-tasks-none]').hidden = tasks.length > 0;
+ var nw = $('[data-lk-codes-new]');
+ if (machine && machine.id) nw.href = 'https://openvibe.codes/?machine=' + encodeURIComponent(machine.id);
+ }
function draw(s) {
function draw(s) {
var m = s.machine || {};
var m = s.machine || {};
var running = Array.isArray(s.running) ? s.running : [];
var running = Array.isArray(s.running) ? s.running : [];
@@ -31,6 +80,7 @@
@@ -31,6 +80,7 @@
$('[data-lk-down]').hidden = true;
$('[data-lk-down]').hidden = true;
$('[data-lk-first]').hidden = !s.first_run;
$('[data-lk-first]').hidden = !s.first_run;
$('[data-lk-name]').textContent = m.name || 'This computer';
$('[data-lk-name]').textContent = m.name || 'This computer';
+ drawCodes(s.codes, m);
$('[data-lk-status]').textContent = m.paused ? 'Paused: nothing runs until you resume'
$('[data-lk-status]').textContent = m.paused ? 'Paused: nothing runs until you resume'
: m.needs_you ? (m.needs_you === 1 ? 'Online · something needs you' : 'Online · ' + m.needs_you + ' things need you')
: m.needs_you ? (m.needs_you === 1 ? 'Online · something needs you' : 'Online · ' + m.needs_you + ' things need you')
: running.length ? 'Online · ' + running.length + ' running' : 'Online · ready';
: running.length ? 'Online · ' + running.length + ' running' : 'Online · ready';
@@ -1,28 +1,158 @@
@@ -1,28 +1,158 @@
{
{
"first_run": true,
"first_run": true,
- "machine": { "name": "Workshop desktop", "os": "linux", "online": true, "paused": false, "needs_you": 1 },
+ "machine": {
+ "name": "Workshop desktop",
+ "os": "linux",
+ "online": true,
+ "paused": false,
+ "needs_you": 1
+ },
"running": [
"running": [
- { "id": "r_1", "kind": "agent", "title": "Vibe is comparing three hosting plans", "detail": "Browser", "started_at": "2026-11-07T10:02:00Z" },
- { "id": "r_2", "kind": "desk", "title": "A desk for Codes: drive redesign", "detail": "Codes", "started_at": "2026-11-07T09:40:00Z" },
- { "id": "r_3", "kind": "share", "title": "Port 3000 shared", "detail": "Only you", "started_at": "2026-11-07T09:10:00Z" }
+ {
+ "id": "r_1",
+ "kind": "agent",
+ "title": "Vibe is comparing three hosting plans",
+ "detail": "Browser",
+ "started_at": "2026-11-07T10:02:00Z"
+ },
+ {
+ "id": "r_2",
+ "kind": "desk",
+ "title": "A desk for Codes: drive redesign",
+ "detail": "Codes",
+ "started_at": "2026-11-07T09:40:00Z"
+ },
+ {
+ "id": "r_3",
+ "kind": "share",
+ "title": "Port 3000 shared",
+ "detail": "Only you",
+ "started_at": "2026-11-07T09:10:00Z"
+ }
],
],
"capabilities": [
"capabilities": [
- { "id": "browser", "name": "A browser for your agents", "who": "Your agents, after you approve each task", "on": true },
- { "id": "files", "name": "Files between your devices", "who": "Only you", "on": true },
- { "id": "models", "name": "Run AI models here", "who": "Only you, from your other devices", "on": false },
- { "id": "earn", "name": "Earn with spare power", "who": "OpenVibe Exchange, when this computer is idle", "on": false }
+ {
+ "id": "browser",
+ "name": "A browser for your agents",
+ "who": "Your agents, after you approve each task",
+ "on": true
+ },
+ {
+ "id": "files",
+ "name": "Files between your devices",
+ "who": "Only you",
+ "on": true
+ },
+ {
+ "id": "models",
+ "name": "Run AI models here",
+ "who": "Only you, from your other devices",
+ "on": false
+ },
+ {
+ "id": "earn",
+ "name": "Earn with spare power",
+ "who": "OpenVibe Exchange, when this computer is idle",
+ "on": false
+ }
+ ],
+ "shares": [
+ {
+ "port": 3000,
+ "url": "https://dev--alex.openvibe.website",
+ "expires_at": "2026-11-07T22:00:00Z"
+ }
],
],
- "shares": [ { "port": 3000, "url": "https://dev--alex.openvibe.website", "expires_at": "2026-11-07T22:00:00Z" } ],
"devices": [
"devices": [
- { "name": "Workshop desktop", "kind": "desktop", "online": true, "here": true },
- { "name": "Laptop", "kind": "laptop", "online": true, "here": false },
- { "name": "Pixel 8", "kind": "phone", "online": false, "here": false }
+ {
+ "name": "Workshop desktop",
+ "kind": "desktop",
+ "online": true,
+ "here": true
+ },
+ {
+ "name": "Laptop",
+ "kind": "laptop",
+ "online": true,
+ "here": false
+ },
+ {
+ "name": "Pixel 8",
+ "kind": "phone",
+ "online": false,
+ "here": false
+ }
],
],
"gpu": null,
"gpu": null,
"earnings_today": null,
"earnings_today": null,
"activity": [
"activity": [
- { "at": "2026-11-07T10:02:00Z", "text": "Vibe opened a browser to compare hosting plans" },
- { "at": "2026-11-07T09:40:00Z", "text": "Codes started a desk for the drive redesign" },
- { "at": "2026-11-07T09:10:00Z", "text": "You shared port 3000" }
- ]
+ {
+ "at": "2026-11-07T10:02:00Z",
+ "text": "Vibe opened a browser to compare hosting plans"
+ },
+ {
+ "at": "2026-11-07T09:40:00Z",
+ "text": "Codes started a desk for the drive redesign"
+ },
+ {
+ "at": "2026-11-07T09:10:00Z",
+ "text": "You shared port 3000"
+ }
+ ],
+ "codes": {
+ "agents": [
+ {
+ "id": "claude-code",
+ "name": "Claude Code",
+ "state": "ready",
+ "version": "2.1.3"
+ },
+ {
+ "id": "codex",
+ "name": "Codex",
+ "state": "signin",
+ "hint": "codex login"
+ },
+ {
+ "id": "opencode",
+ "name": "OpenCode",
+ "state": "ready",
+ "version": "0.9.2"
+ },
+ {
+ "id": "gemini",
+ "name": "Gemini CLI",
+ "state": "missing"
+ }
+ ],
+ "tasks": [
+ {
+ "id": "tsk_01M6Q1",
+ "title": "Make every page work at 320 px",
+ "folder": "~/code/openvibe-site",
+ "agent": "Claude Code",
+ "state": "running",
+ "started_at": "2026-11-07T09:52:00Z",
+ "url": "https://openvibe.codes/tasks/tsk_01M6Q1"
+ },
+ {
+ "id": "tsk_01M6P8",
+ "title": "Fix the flaky upload test",
+ "folder": "~/code/drive",
+ "agent": "Codex",
+ "state": "waiting",
+ "started_at": "2026-11-07T09:31:00Z",
+ "url": "https://openvibe.codes/tasks/tsk_01M6P8"
+ },
+ {
+ "id": "tsk_01M6N2",
+ "title": "Add a dark mode that follows the system",
+ "folder": "~/code/notes-app",
+ "agent": "OpenCode",
+ "state": "done",
+ "started_at": "2026-11-07T08:05:00Z",
+ "url": "https://openvibe.codes/tasks/tsk_01M6N2"
+ }
+ ]
+ }
}
}