openvibers/openvibe.link Commits
Link's window: the first-run question (#127)
Changes
@@@ -109,6 +109,8 @@ type State struct {GPU *GPU `json:"gpu,omitempty"`EarningsToday *Earnings `json:"earnings_today,omitempty"`Activity []Activity `json:"activity,omitempty"`++ // FirstRun is true until the person has answered "What's this computer for?" or skipped it.++ FirstRun bool `json:"first_run,omitempty"`}// Options are what the daemon gives the panel: its state, and the actions the window can ask for.@@@ -152,6 +152,7 @@ func TestStateShapeMatchesTheDesign(t *testing.T) {GPU: &GPU{Name: "RTX", VRAMGB: 24, Models: []Model{{Name: "m", TPS: 41, Loaded: true}}},EarningsToday: &Earnings{Coins: 120},Activity: []Activity{{At: now, Text: "x"}},++ FirstRun: true,}b, err := json.Marshal(full)if err != nil {@@@ -21,6 +21,7 @@ fragment: `/panel/#t=<token>`. The page sends that token as `X-Link-Panel` on ev| `POST /panel/api/shares` | `{ "port": 3000 }` | Shares a local port through Connect, behind the person's sign-in. || `DELETE /panel/api/shares/:port` | — | Ends that share. || `POST /panel/api/files` | multipart `file`… | Sends the files to the person's other devices. |++| `POST /panel/api/first-run` | `{ "uses": ["codes", "browser"], "skip": false }` | The first-run answer. Uses are `codes`, `browser`, `models`, `earn` and `vibe`. The daemon turns on what each says (`earn` stays off until it is confirmed on Exchange) and stops asking; `skip: true` only stops asking. |## `GET /panel/api/state`@@@ -41,6 +42,7 @@ Every key is optional. A section with nothing in it is left out; the window neve- `running[].kind` is `agent`, `desk`, `share` or `model`.- `machine.needs_you` counts what waits for the person: it puts a dot on the ring.++- `first_run: true` shows the "What's this computer for?" question at the top, until it is answered or skipped.- The ring shows:- green when online;- blue and filling while things run;@@@ -18,6 +18,20 @@<button class="lk-pause" type="button" data-lk-pause aria-pressed="false"><span class="lk-pause-ic" aria-hidden="true"></span><span data-lk-pause-label>Pause everything</span></button></header>++ <!-- The first run (domains plan §13.1): one question, several answers, each turning on only what it says. -->++ <section class="lk-first" data-lk-first hidden aria-labelledby="lk-first-h">++ <h2 id="lk-first-h">What’s this computer for?</h2>++ <p class="lk-first-lede">Pick any. Each turns on only what it says, everything stays private to you, and you can change it below at any time.</p>++ <form class="lk-uses" data-lk-first-form>++ <label class="lk-use"><input type="checkbox" name="use" value="codes"><span><b>Code with agents</b><small>Codes runs your Claude Code, Codex and the rest here, with your own logins.</small></span></label>++ <label class="lk-use"><input type="checkbox" name="use" value="browser"><span><b>Let my AI use this computer</b><small>A browser your agents can drive, after you approve each task.</small></span></label>++ <label class="lk-use"><input type="checkbox" name="use" value="models"><span><b>Run AI models for me</b><small>Models on this computer that you reach from your other devices.</small></span></label>++ <label class="lk-use"><input type="checkbox" name="use" value="earn"><span><b>Earn with my spare power</b><small>OpenVibe Exchange uses it only while it is idle. Off until you confirm it there.</small></span></label>++ <label class="lk-use"><input type="checkbox" name="use" value="vibe"><span><b>Just Vibe</b><small>Chat with your agents and answer their questions from here.</small></span></label>++ <p class="lk-first-acts"><button type="submit" class="lk-btn is-primary">Done</button><button type="button" class="lk-btn" data-lk-first-skip>Not now</button></p>++ </form>++ </section>++<p class="lk-down" data-lk-down hidden>Link is not running on this computer. Start it from your apps, or run <code>openvibe-node</code>.</p><div class="lk-cols">@@@ -117,3 +117,20 @@ button, input { font: inherit; color: inherit; }.lk-cols { grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr); align-items: start; }}@media (prefers-reduced-motion: reduce) { .lk-ring-fill, .lk-switch i, .lk-switch i::after { transition: none; } }++++/* The first run: one question across the window, answers as cards you tick. */++.lk-first { padding: 18px; border-radius: 20px; border: 1px solid color-mix(in srgb, var(--accent) 45%, var(--line)); background: radial-gradient(120% 140% at 0% 0%, color-mix(in srgb, var(--accent) 16%, transparent), transparent 60%), var(--surface); }++.lk-first h2 { margin: 0; font-size: 20px; font-weight: 850; letter-spacing: -.01em; text-transform: none; color: var(--text); }++.lk-first-lede { margin: 4px 0 14px; color: var(--text-2); }++.lk-uses { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 8px; margin: 0; }++@media (max-width: 900px) { .lk-uses { grid-template-columns: minmax(0, 1fr); } }++.lk-use { position: relative; display: block; cursor: pointer; }++.lk-use input { position: absolute; opacity: 0; width: 1px; height: 1px; }++.lk-use span { display: grid; align-content: start; gap: 2px; height: 100%; padding: 12px 14px 12px 40px; border-radius: 14px; border: 1px solid var(--line); background: var(--surface-2); transition: border-color .15s, background .15s; }++.lk-use span::before { content: ""; position: absolute; left: 14px; top: 14px; width: 16px; height: 16px; border-radius: 5px; border: 2px solid var(--muted); box-sizing: border-box; }++.lk-use b { font-size: 14.5px; }++.lk-use small { color: var(--muted); font-size: 12.5px; line-height: 1.4; }++.lk-use:has(input:checked) span { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, var(--surface-2)); }++.lk-use:has(input:checked) span::before { border-color: var(--accent); background: var(--accent); box-shadow: inset 0 0 0 2px var(--surface-2); }++.lk-use:has(input:focus-visible) span { outline: 2px solid var(--accent); outline-offset: 2px; }++.lk-first-acts { grid-column: 1 / -1; display: flex; gap: 8px; margin: 6px 0 0; }@@@ -29,6 +29,7 @@root.setAttribute('data-needs', m.needs_you ? '1' : '0');root.style.setProperty('--fill', String(Math.max(10, 100 - Math.min(running.length, 4) * 22)));$('[data-lk-down]').hidden = true;++ $('[data-lk-first]').hidden = !s.first_run;$('[data-lk-name]').textContent = m.name || 'This computer';$('[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')@@@ -105,6 +106,13 @@return fetch('/panel/api/state', { credentials: 'same-origin', headers: { 'X-Link-Panel': token } }).then(function (r) { if (!r.ok) throw new Error(String(r.status)); return r.json(); }).then(draw).catch(down);}++ // The first run: the uses ticked, or Not now; the daemon then turns on what each says and stops asking.++ $('[data-lk-first-form]').addEventListener('submit', function (e) {++ e.preventDefault();++ var uses = Array.prototype.map.call(this.querySelectorAll('input[name=use]:checked'), function (i) { return i.value; });++ send('first-run', 'POST', { uses: uses });++ });++ $('[data-lk-first-skip]').addEventListener('click', function () { send('first-run', 'POST', { uses: [], skip: true }); });$('[data-lk-pause]').addEventListener('click', function () { var on = this.getAttribute('aria-pressed') !== 'true'; send('pause', 'POST', { paused: on }); });$('[data-lk-share-form]').addEventListener('submit', function (e) {e.preventDefault(); var port = Number(this.port.value);@@@ -1,4 +1,5 @@{++ "first_run": true,"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" },
@@ -109,6 +109,8 @@ type State struct {
@@ -109,6 +109,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"`
+ // FirstRun is true until the person has answered "What's this computer for?" or skipped it.
+ FirstRun bool `json:"first_run,omitempty"`
}
}
// Options are what the daemon gives the panel: its state, and the actions the window can ask for.
// Options are what the daemon gives the panel: its state, and the actions the window can ask for.
@@ -152,6 +152,7 @@ func TestStateShapeMatchesTheDesign(t *testing.T) {
@@ -152,6 +152,7 @@ func TestStateShapeMatchesTheDesign(t *testing.T) {
GPU: &GPU{Name: "RTX", VRAMGB: 24, Models: []Model{{Name: "m", TPS: 41, Loaded: true}}},
GPU: &GPU{Name: "RTX", VRAMGB: 24, Models: []Model{{Name: "m", TPS: 41, Loaded: true}}},
EarningsToday: &Earnings{Coins: 120},
EarningsToday: &Earnings{Coins: 120},
Activity: []Activity{{At: now, Text: "x"}},
Activity: []Activity{{At: now, Text: "x"}},
+ FirstRun: true,
}
}
b, err := json.Marshal(full)
b, err := json.Marshal(full)
if err != nil {
if err != nil {
@@ -21,6 +21,7 @@ fragment: `/panel/#t=<token>`. The page sends that token as `X-Link-Panel` on ev
@@ -21,6 +21,7 @@ fragment: `/panel/#t=<token>`. The page sends that token as `X-Link-Panel` on ev
| `POST /panel/api/shares` | `{ "port": 3000 }` | Shares a local port through Connect, behind the person's sign-in. |
| `POST /panel/api/shares` | `{ "port": 3000 }` | Shares a local port through Connect, behind the person's sign-in. |
| `DELETE /panel/api/shares/:port` | — | Ends that share. |
| `DELETE /panel/api/shares/:port` | — | Ends that share. |
| `POST /panel/api/files` | multipart `file`… | Sends the files to the person's other devices. |
| `POST /panel/api/files` | multipart `file`… | Sends the files to the person's other devices. |
+| `POST /panel/api/first-run` | `{ "uses": ["codes", "browser"], "skip": false }` | The first-run answer. Uses are `codes`, `browser`, `models`, `earn` and `vibe`. The daemon turns on what each says (`earn` stays off until it is confirmed on Exchange) and stops asking; `skip: true` only stops asking. |
## `GET /panel/api/state`
## `GET /panel/api/state`
@@ -41,6 +42,7 @@ Every key is optional. A section with nothing in it is left out; the window neve
@@ -41,6 +42,7 @@ Every key is optional. A section with nothing in it is left out; the window neve
- `running[].kind` is `agent`, `desk`, `share` or `model`.
- `running[].kind` is `agent`, `desk`, `share` or `model`.
- `machine.needs_you` counts what waits for the person: it puts a dot on the ring.
- `machine.needs_you` counts what waits for the person: it puts a dot on the ring.
+- `first_run: true` shows the "What's this computer for?" question at the top, until it is answered or skipped.
- The ring shows:
- The ring shows:
- green when online;
- green when online;
- blue and filling while things run;
- blue and filling while things run;
@@ -18,6 +18,20 @@
@@ -18,6 +18,20 @@
<button class="lk-pause" type="button" data-lk-pause aria-pressed="false"><span class="lk-pause-ic" aria-hidden="true"></span><span data-lk-pause-label>Pause everything</span></button>
<button class="lk-pause" type="button" data-lk-pause aria-pressed="false"><span class="lk-pause-ic" aria-hidden="true"></span><span data-lk-pause-label>Pause everything</span></button>
</header>
</header>
+ <!-- The first run (domains plan §13.1): one question, several answers, each turning on only what it says. -->
+ <section class="lk-first" data-lk-first hidden aria-labelledby="lk-first-h">
+ <h2 id="lk-first-h">What’s this computer for?</h2>
+ <p class="lk-first-lede">Pick any. Each turns on only what it says, everything stays private to you, and you can change it below at any time.</p>
+ <form class="lk-uses" data-lk-first-form>
+ <label class="lk-use"><input type="checkbox" name="use" value="codes"><span><b>Code with agents</b><small>Codes runs your Claude Code, Codex and the rest here, with your own logins.</small></span></label>
+ <label class="lk-use"><input type="checkbox" name="use" value="browser"><span><b>Let my AI use this computer</b><small>A browser your agents can drive, after you approve each task.</small></span></label>
+ <label class="lk-use"><input type="checkbox" name="use" value="models"><span><b>Run AI models for me</b><small>Models on this computer that you reach from your other devices.</small></span></label>
+ <label class="lk-use"><input type="checkbox" name="use" value="earn"><span><b>Earn with my spare power</b><small>OpenVibe Exchange uses it only while it is idle. Off until you confirm it there.</small></span></label>
+ <label class="lk-use"><input type="checkbox" name="use" value="vibe"><span><b>Just Vibe</b><small>Chat with your agents and answer their questions from here.</small></span></label>
+ <p class="lk-first-acts"><button type="submit" class="lk-btn is-primary">Done</button><button type="button" class="lk-btn" data-lk-first-skip>Not now</button></p>
+ </form>
+ </section>
+
<p class="lk-down" data-lk-down hidden>Link is not running on this computer. Start it from your apps, or run <code>openvibe-node</code>.</p>
<p class="lk-down" data-lk-down hidden>Link is not running on this computer. Start it from your apps, or run <code>openvibe-node</code>.</p>
<div class="lk-cols">
<div class="lk-cols">
@@ -117,3 +117,20 @@ button, input { font: inherit; color: inherit; }
@@ -117,3 +117,20 @@ button, input { font: inherit; color: inherit; }
.lk-cols { grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr); align-items: start; }
.lk-cols { grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr); align-items: start; }
}
}
@media (prefers-reduced-motion: reduce) { .lk-ring-fill, .lk-switch i, .lk-switch i::after { transition: none; } }
@media (prefers-reduced-motion: reduce) { .lk-ring-fill, .lk-switch i, .lk-switch i::after { transition: none; } }
+
+/* The first run: one question across the window, answers as cards you tick. */
+.lk-first { padding: 18px; border-radius: 20px; border: 1px solid color-mix(in srgb, var(--accent) 45%, var(--line)); background: radial-gradient(120% 140% at 0% 0%, color-mix(in srgb, var(--accent) 16%, transparent), transparent 60%), var(--surface); }
+.lk-first h2 { margin: 0; font-size: 20px; font-weight: 850; letter-spacing: -.01em; text-transform: none; color: var(--text); }
+.lk-first-lede { margin: 4px 0 14px; color: var(--text-2); }
+.lk-uses { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 8px; margin: 0; }
+@media (max-width: 900px) { .lk-uses { grid-template-columns: minmax(0, 1fr); } }
+.lk-use { position: relative; display: block; cursor: pointer; }
+.lk-use input { position: absolute; opacity: 0; width: 1px; height: 1px; }
+.lk-use span { display: grid; align-content: start; gap: 2px; height: 100%; padding: 12px 14px 12px 40px; border-radius: 14px; border: 1px solid var(--line); background: var(--surface-2); transition: border-color .15s, background .15s; }
+.lk-use span::before { content: ""; position: absolute; left: 14px; top: 14px; width: 16px; height: 16px; border-radius: 5px; border: 2px solid var(--muted); box-sizing: border-box; }
+.lk-use b { font-size: 14.5px; }
+.lk-use small { color: var(--muted); font-size: 12.5px; line-height: 1.4; }
+.lk-use:has(input:checked) span { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, var(--surface-2)); }
+.lk-use:has(input:checked) span::before { border-color: var(--accent); background: var(--accent); box-shadow: inset 0 0 0 2px var(--surface-2); }
+.lk-use:has(input:focus-visible) span { outline: 2px solid var(--accent); outline-offset: 2px; }
+.lk-first-acts { grid-column: 1 / -1; display: flex; gap: 8px; margin: 6px 0 0; }
@@ -29,6 +29,7 @@
@@ -29,6 +29,7 @@
root.setAttribute('data-needs', m.needs_you ? '1' : '0');
root.setAttribute('data-needs', m.needs_you ? '1' : '0');
root.style.setProperty('--fill', String(Math.max(10, 100 - Math.min(running.length, 4) * 22)));
root.style.setProperty('--fill', String(Math.max(10, 100 - Math.min(running.length, 4) * 22)));
$('[data-lk-down]').hidden = true;
$('[data-lk-down]').hidden = true;
+ $('[data-lk-first]').hidden = !s.first_run;
$('[data-lk-name]').textContent = m.name || 'This computer';
$('[data-lk-name]').textContent = m.name || 'This computer';
$('[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')
@@ -105,6 +106,13 @@
@@ -105,6 +106,13 @@
return fetch('/panel/api/state', { credentials: 'same-origin', headers: { 'X-Link-Panel': token } })
return fetch('/panel/api/state', { credentials: 'same-origin', headers: { 'X-Link-Panel': token } })
.then(function (r) { if (!r.ok) throw new Error(String(r.status)); return r.json(); }).then(draw).catch(down);
.then(function (r) { if (!r.ok) throw new Error(String(r.status)); return r.json(); }).then(draw).catch(down);
}
}
+ // The first run: the uses ticked, or Not now; the daemon then turns on what each says and stops asking.
+ $('[data-lk-first-form]').addEventListener('submit', function (e) {
+ e.preventDefault();
+ var uses = Array.prototype.map.call(this.querySelectorAll('input[name=use]:checked'), function (i) { return i.value; });
+ send('first-run', 'POST', { uses: uses });
+ });
+ $('[data-lk-first-skip]').addEventListener('click', function () { send('first-run', 'POST', { uses: [], skip: true }); });
$('[data-lk-pause]').addEventListener('click', function () { var on = this.getAttribute('aria-pressed') !== 'true'; send('pause', 'POST', { paused: on }); });
$('[data-lk-pause]').addEventListener('click', function () { var on = this.getAttribute('aria-pressed') !== 'true'; send('pause', 'POST', { paused: on }); });
$('[data-lk-share-form]').addEventListener('submit', function (e) {
$('[data-lk-share-form]').addEventListener('submit', function (e) {
e.preventDefault(); var port = Number(this.port.value);
e.preventDefault(); var port = Number(this.port.value);
@@ -1,4 +1,5 @@
@@ -1,4 +1,5 @@
{
{
+ "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_1", "kind": "agent", "title": "Vibe is comparing three hosting plans", "detail": "Browser", "started_at": "2026-11-07T10:02:00Z" },