openvibers/openvibe.link Commits

Link's window: the first-run question (#127)

OP OpenVibers committed 6 days ago · 0f8d81d803 · 7 files, +45 −0

* Link's first run in its own window: What's this computer for? — five answers, each turning on only what it says

* Link's panel state carries first_run, so the shape test matches the design fixture

Changes

ddiff --git a/internal/panel/panel.go b/internal/panel/panel.go
iindex b09dee8..e4679b2 100644
---- a/internal/panel/panel.go
++++ b/internal/panel/panel.go
@@@ -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.
ddiff --git a/internal/panel/panel_test.go b/internal/panel/panel_test.go
iindex 0ed1c88..ebd59c6 100644
---- a/internal/panel/panel_test.go
++++ b/internal/panel/panel_test.go
@@@ -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 {
ddiff --git a/internal/panel/ui/README.md b/internal/panel/ui/README.md
iindex be13c3d..708177d 100644
---- a/internal/panel/ui/README.md
++++ b/internal/panel/ui/README.md
@@@ -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;
ddiff --git a/internal/panel/ui/index.html b/internal/panel/ui/index.html
iindex 0f24da8..c2cb8ec 100644
---- a/internal/panel/ui/index.html
++++ b/internal/panel/ui/index.html
@@@ -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">
ddiff --git a/internal/panel/ui/panel.css b/internal/panel/ui/panel.css
iindex f5c7a82..b456215 100644
---- a/internal/panel/ui/panel.css
++++ b/internal/panel/ui/panel.css
@@@ -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; }
ddiff --git a/internal/panel/ui/panel.js b/internal/panel/ui/panel.js
iindex a00ba1c..94e494a 100644
---- a/internal/panel/ui/panel.js
++++ b/internal/panel/ui/panel.js
@@@ -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);
ddiff --git a/internal/panel/ui/testdata/state.json b/internal/panel/ui/testdata/state.json
iindex 356959b..c08ddac 100644
---- a/internal/panel/ui/testdata/state.json
++++ b/internal/panel/ui/testdata/state.json
@@@ -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" },