@neosh/sidebar 0.1.0 → 0.3.0
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/main.ts +139 -19
- package/package.json +1 -1
- package/plugin.toml +1 -1
package/main.ts
CHANGED
|
@@ -78,6 +78,7 @@ import {
|
|
|
78
78
|
type PickerItem,
|
|
79
79
|
prompt,
|
|
80
80
|
pulseBright,
|
|
81
|
+
pulseHl,
|
|
81
82
|
spinnerFrame,
|
|
82
83
|
} from "@neosh/api/ui";
|
|
83
84
|
|
|
@@ -1968,6 +1969,7 @@ async function showNodes(neosh: Neosh): Promise<void> {
|
|
|
1968
1969
|
const listening = await neosh.vars
|
|
1969
1970
|
.get<{ addr: string | null; error?: string }>({ scope: "global" }, "swarm.listen")
|
|
1970
1971
|
.catch(() => null);
|
|
1972
|
+
const ascii = (await neosh.opt.get<boolean>("ui.ascii_only").catch(() => false)) ?? false;
|
|
1971
1973
|
|
|
1972
1974
|
type Row =
|
|
1973
1975
|
| { kind: "node"; node: SwarmNode }
|
|
@@ -1975,11 +1977,17 @@ async function showNodes(neosh: Neosh): Promise<void> {
|
|
|
1975
1977
|
| { kind: "add" }
|
|
1976
1978
|
| { kind: "self" };
|
|
1977
1979
|
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1980
|
+
// The last answer from the host, so a spinner frame costs a redraw rather than two round trips.
|
|
1981
|
+
// At 80ms a tick, rebuilding these over the API would be the most talkative thing in the
|
|
1982
|
+
// workspace, to animate one glyph.
|
|
1983
|
+
let nodes: SwarmNode[] = [];
|
|
1984
|
+
let strangers: SwarmStranger[] = [];
|
|
1985
|
+
|
|
1986
|
+
/** Whether anything on screen is mid-dial, and therefore whether the tick has any work. */
|
|
1987
|
+
const spinning = () =>
|
|
1988
|
+
nodes.some((n) => n.link.state === "connecting" || n.link.state === "retrying");
|
|
1989
|
+
|
|
1990
|
+
const build = (): PickerItem<Row>[] => {
|
|
1983
1991
|
const rows: PickerItem<Row>[] = [];
|
|
1984
1992
|
|
|
1985
1993
|
for (const s of strangers) {
|
|
@@ -1989,17 +1997,30 @@ async function showNodes(neosh: Neosh): Promise<void> {
|
|
|
1989
1997
|
? `found at ${s.addr ?? "an address you gave"} · ${fingerprint(s.info.id)} · ↵ to add`
|
|
1990
1998
|
: `wants to join · ${fingerprint(s.info.id)} · ↵ to allow`,
|
|
1991
1999
|
keywords: `${s.info.id} pending join pair new`,
|
|
2000
|
+
// A machine asking to join is the one row here that wants answering, so it wears the
|
|
2001
|
+
// colour the workspace already uses for *act now* and pulses on the same 1 Hz duty cycle
|
|
2002
|
+
// as every other waiting indicator. Nothing else in this list moves unless it is working.
|
|
2003
|
+
icon: "?",
|
|
2004
|
+
hl: pulseHl("Status.Pending"),
|
|
1992
2005
|
value: { kind: "stranger", stranger: s },
|
|
1993
2006
|
});
|
|
1994
2007
|
}
|
|
1995
2008
|
|
|
1996
2009
|
for (const n of nodes) {
|
|
1997
2010
|
const running = n.agents.filter((a) => a.state === "running").length;
|
|
1998
|
-
// One sentence per link state, because they are different answers to "why is it not
|
|
1999
|
-
//
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2011
|
+
// One sentence per link state, because they are different answers to "why is it not here" —
|
|
2012
|
+
// and the fourth of them is the reason this reads the way it does. `waiting` is the far half
|
|
2013
|
+
// of pairing: reached, proven, and not yet allowed over there. It used to arrive as
|
|
2014
|
+
// `connecting` with attempt 0, which printed a bare `connecting…` and threw the only useful
|
|
2015
|
+
// sentence away, so adding a computer looked like a network fault for as long as anybody
|
|
2016
|
+
// was willing to watch it.
|
|
2017
|
+
const state = n.link.state === "up"
|
|
2018
|
+
? `${n.agents.length} ${n.agents.length === 1 ? "conversation" : "conversations"}`
|
|
2019
|
+
: n.link.state === "waiting"
|
|
2020
|
+
// Reads on from the label, which is the machine's name — naming it again here gave
|
|
2021
|
+
// `linux-box allow this computer on linux-box`. What the sentence has to carry is the
|
|
2022
|
+
// key and the fact that it is pressed somewhere else.
|
|
2023
|
+
? "has not allowed this computer yet — ^J there"
|
|
2003
2024
|
: n.link.state === "connecting"
|
|
2004
2025
|
? n.link.attempt > 0
|
|
2005
2026
|
? `connecting — try ${n.link.attempt}, ${n.reason ?? "no answer"}`
|
|
@@ -2009,6 +2030,16 @@ async function showNodes(neosh: Neosh): Promise<void> {
|
|
|
2009
2030
|
? `reconnecting — try ${n.link.attempt}`
|
|
2010
2031
|
: "reconnecting…"
|
|
2011
2032
|
: `disconnected — ${n.reason ?? "it dials in"} · ^R reconnects`;
|
|
2033
|
+
// The glyph says what the row is doing; the sentence says what to do about it. A spinner
|
|
2034
|
+
// only ever means "this machine is working on it" — never over `waiting`, where nothing on
|
|
2035
|
+
// this computer is trying and the motion would be a promise nobody is keeping.
|
|
2036
|
+
const [icon, hl] = n.link.state === "up"
|
|
2037
|
+
? [ascii ? "*" : "●", "Diagnostic.Ok"]
|
|
2038
|
+
: n.link.state === "waiting"
|
|
2039
|
+
? [ascii ? "!" : "◍", pulseHl("Status.Pending")]
|
|
2040
|
+
: n.link.state === "down"
|
|
2041
|
+
? [ascii ? "-" : "○", "Sidebar.Dim"]
|
|
2042
|
+
: [spinnerFrame(), "Status.Streaming"];
|
|
2012
2043
|
rows.push({
|
|
2013
2044
|
label: n.info.name,
|
|
2014
2045
|
detail: [
|
|
@@ -2019,14 +2050,20 @@ async function showNodes(neosh: Neosh): Promise<void> {
|
|
|
2019
2050
|
fingerprint(n.info.id),
|
|
2020
2051
|
].filter(Boolean).join(" · "),
|
|
2021
2052
|
keywords: `${n.info.os} ${n.info.id} ${n.link.state}`,
|
|
2053
|
+
icon,
|
|
2054
|
+
hl,
|
|
2022
2055
|
value: { kind: "node", node: n },
|
|
2023
2056
|
});
|
|
2024
2057
|
}
|
|
2025
2058
|
|
|
2026
2059
|
rows.push({
|
|
2027
|
-
|
|
2060
|
+
// The `+` is the icon now that this list has a gutter, not the first character of the label:
|
|
2061
|
+
// with both it read `+ + Add a computer…`.
|
|
2062
|
+
label: "Add a computer…",
|
|
2028
2063
|
detail: "its address on your network, or through Tailscale",
|
|
2029
2064
|
keywords: "pair join new machine host",
|
|
2065
|
+
icon: "+",
|
|
2066
|
+
hl: "Sidebar.Dim",
|
|
2030
2067
|
value: { kind: "add" },
|
|
2031
2068
|
});
|
|
2032
2069
|
rows.push({
|
|
@@ -2049,24 +2086,55 @@ async function showNodes(neosh: Neosh): Promise<void> {
|
|
|
2049
2086
|
return rows;
|
|
2050
2087
|
};
|
|
2051
2088
|
|
|
2052
|
-
|
|
2089
|
+
/** Ask the host again. Everything else here draws from what this last brought back. */
|
|
2090
|
+
const fetch = async () => {
|
|
2091
|
+
[nodes, strangers] = await Promise.all([
|
|
2092
|
+
neosh.swarm.nodes().catch(() => []),
|
|
2093
|
+
neosh.swarm.strangers().catch(() => []),
|
|
2094
|
+
]);
|
|
2095
|
+
};
|
|
2096
|
+
|
|
2097
|
+
const redraw = () => {
|
|
2098
|
+
items.splice(0, items.length, ...build());
|
|
2099
|
+
};
|
|
2053
2100
|
const refill = async () => {
|
|
2054
|
-
|
|
2101
|
+
await fetch();
|
|
2102
|
+
redraw();
|
|
2055
2103
|
};
|
|
2056
2104
|
|
|
2105
|
+
await fetch();
|
|
2106
|
+
const items: PickerItem<Row>[] = build();
|
|
2107
|
+
|
|
2057
2108
|
const chosen = await picker(neosh, items, {
|
|
2058
2109
|
title: "Computers",
|
|
2059
2110
|
width: 84,
|
|
2060
2111
|
height: 14,
|
|
2061
|
-
hints: "↵
|
|
2112
|
+
hints: "↵ open ^E rename ^R reconnect ^D disconnect ^X remove ^Y my id",
|
|
2062
2113
|
// The list keeps up while it is open: a machine connecting, dropping, or moving from
|
|
2063
2114
|
// "connecting" to a row of conversations changes under the cursor rather than on reopen.
|
|
2064
|
-
subscribe: (reload) =>
|
|
2065
|
-
neosh.swarm.onChange(() => {
|
|
2115
|
+
subscribe: (reload) => {
|
|
2116
|
+
const changed = neosh.swarm.onChange(() => {
|
|
2066
2117
|
void refill().then(reload);
|
|
2067
|
-
})
|
|
2118
|
+
});
|
|
2119
|
+
// And the spinner turns on the shared clock, off what the last change brought back rather
|
|
2120
|
+
// than off a fresh pair of API calls: a dial takes seconds, the clock ticks twelve times a
|
|
2121
|
+
// second, and asking the host what it knows on every frame to move one glyph would make
|
|
2122
|
+
// this panel the noisiest thing in the workspace. Nothing ticks while nothing is dialling,
|
|
2123
|
+
// so a list of connected machines costs exactly as much as it did before.
|
|
2124
|
+
const ticked = onTick(() => {
|
|
2125
|
+
if (!spinning()) return;
|
|
2126
|
+
redraw();
|
|
2127
|
+
reload();
|
|
2128
|
+
});
|
|
2129
|
+
return {
|
|
2130
|
+
dispose() {
|
|
2131
|
+
changed.dispose();
|
|
2132
|
+
ticked.dispose();
|
|
2133
|
+
},
|
|
2134
|
+
};
|
|
2135
|
+
},
|
|
2068
2136
|
// Chords, because every bare letter a picker takes is a letter its filter can never contain.
|
|
2069
|
-
ownKeys: ["<C-y>", "<C-x>", "<C-r>", "<C-d>"],
|
|
2137
|
+
ownKeys: ["<C-y>", "<C-x>", "<C-r>", "<C-d>", "<C-e>"],
|
|
2070
2138
|
async onKey(key, ctx) {
|
|
2071
2139
|
if (key.key.code.kind !== "char" || !key.key.mods.ctrl) return;
|
|
2072
2140
|
switch (key.key.code.c.toLowerCase()) {
|
|
@@ -2090,6 +2158,27 @@ async function showNodes(neosh: Neosh): Promise<void> {
|
|
|
2090
2158
|
await refill();
|
|
2091
2159
|
return "reload";
|
|
2092
2160
|
}
|
|
2161
|
+
case "e": {
|
|
2162
|
+
const row = ctx.item;
|
|
2163
|
+
if (row?.kind !== "node") return "handled";
|
|
2164
|
+
// Prefilled with what the row says, so this is an edit rather than a blank field you
|
|
2165
|
+
// have to remember the old name to fill in. Clearing it is how you go back to the
|
|
2166
|
+
// hostname, which is why an empty answer is not the same as `Esc`.
|
|
2167
|
+
const next = await prompt(neosh, `Call ${row.node.info.name}`, {
|
|
2168
|
+
initial: row.node.info.name,
|
|
2169
|
+
width: 60,
|
|
2170
|
+
});
|
|
2171
|
+
if (next === null) return "handled";
|
|
2172
|
+
try {
|
|
2173
|
+
await neosh.swarm.rename(row.node.info.id, next.trim() === "" ? null : next.trim());
|
|
2174
|
+
await refill();
|
|
2175
|
+
return "reload";
|
|
2176
|
+
} catch (e) {
|
|
2177
|
+
// Almost always "that one is in your config file", which names the field to change.
|
|
2178
|
+
neosh.notify(String(e), "warn");
|
|
2179
|
+
return "handled";
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2093
2182
|
case "x": {
|
|
2094
2183
|
const row = ctx.item;
|
|
2095
2184
|
if (row?.kind !== "node") return "handled";
|
|
@@ -2137,6 +2226,23 @@ async function showNodes(neosh: Neosh): Promise<void> {
|
|
|
2137
2226
|
return;
|
|
2138
2227
|
}
|
|
2139
2228
|
case "node": {
|
|
2229
|
+
// A row you cannot open should say why in the terms the row is in. "Nothing running" is true
|
|
2230
|
+
// of a machine that has not allowed this one yet, and is not what is wrong with it.
|
|
2231
|
+
if (chosen.node.link.state === "waiting") {
|
|
2232
|
+
neosh.notify(
|
|
2233
|
+
`${chosen.node.info.name} has not allowed this computer yet — press ^J there and add `
|
|
2234
|
+
+ `${me.name}`,
|
|
2235
|
+
"info",
|
|
2236
|
+
);
|
|
2237
|
+
return;
|
|
2238
|
+
}
|
|
2239
|
+
if (!chosen.node.up) {
|
|
2240
|
+
neosh.notify(
|
|
2241
|
+
`${chosen.node.info.name} is not connected — ${chosen.node.reason ?? "still dialling"}`,
|
|
2242
|
+
"info",
|
|
2243
|
+
);
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2140
2246
|
const newest = [...chosen.node.agents].sort((a, b) => b.updated_at - a.updated_at)[0];
|
|
2141
2247
|
if (!newest) {
|
|
2142
2248
|
neosh.notify(`${chosen.node.info.name} has nothing running`, "info");
|
|
@@ -2384,9 +2490,16 @@ async function addComputer(neosh: Neosh): Promise<void> {
|
|
|
2384
2490
|
const target = addr.includes(":") ? addr.trim() : `${addr.trim()}:7717`;
|
|
2385
2491
|
|
|
2386
2492
|
let found;
|
|
2493
|
+
// A dial across a network somebody just typed the address of is the one thing here that can take
|
|
2494
|
+
// long enough to look broken — a wrong hostname sits on a TCP timeout — so it turns while it
|
|
2495
|
+
// waits. Progress is keyed and replaced in place, which is what makes this one line rather than
|
|
2496
|
+
// a column of `asking…`.
|
|
2497
|
+
const turning = onTick(() => {
|
|
2498
|
+
neosh.progress("swarm.probe", `${spinnerFrame()} asking ${target}…`);
|
|
2499
|
+
});
|
|
2387
2500
|
try {
|
|
2388
2501
|
// A state that stops being true the moment the far end answers or does not.
|
|
2389
|
-
neosh.progress("swarm.probe",
|
|
2502
|
+
neosh.progress("swarm.probe", `${spinnerFrame()} asking ${target}…`);
|
|
2390
2503
|
found = await neosh.swarm.probe(target);
|
|
2391
2504
|
} catch (e) {
|
|
2392
2505
|
// `String(e)` here would read `NeoshError: not found: …: swarm i/o: Connection refused (os
|
|
@@ -2401,6 +2514,7 @@ async function addComputer(neosh: Neosh): Promise<void> {
|
|
|
2401
2514
|
neosh.log.warn(`swarm probe of ${target} failed: ${String(e)}`);
|
|
2402
2515
|
return;
|
|
2403
2516
|
} finally {
|
|
2517
|
+
turning.dispose();
|
|
2404
2518
|
neosh.done("swarm.probe");
|
|
2405
2519
|
}
|
|
2406
2520
|
|
|
@@ -2419,6 +2533,12 @@ async function addComputer(neosh: Neosh): Promise<void> {
|
|
|
2419
2533
|
// Both halves have to happen, and only one of them is ours. Saying so now saves the ten minutes
|
|
2420
2534
|
// otherwise spent wondering why the machine is listed and permanently unreachable.
|
|
2421
2535
|
neosh.notify(`${found.name} added — now allow this computer over there, with ^J`);
|
|
2536
|
+
// And back to the panel, which is where that second half becomes visible: the new row sits on
|
|
2537
|
+
// `allow this computer on <name>` until somebody does it and then turns over to its
|
|
2538
|
+
// conversations, live, without a key being pressed here. Dropping the user back at the composer
|
|
2539
|
+
// instead is what made pairing feel like it had silently failed — the one thing worth watching
|
|
2540
|
+
// was on a panel they had just been taken out of.
|
|
2541
|
+
await showNodes(neosh);
|
|
2422
2542
|
}
|
|
2423
2543
|
|
|
2424
2544
|
/**
|
package/package.json
CHANGED
package/plugin.toml
CHANGED