wisp-agents 0.6.0 → 0.7.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/CHANGELOG.md +15 -1
- package/README.md +14 -0
- package/package.json +2 -2
- package/server/index.js +64 -0
- package/server/openapi.js +21 -1
- package/server/preferences.js +296 -0
- package/server/runner.js +2 -1
- package/server/update.js +136 -0
- package/web/css/app.css +83 -1
- package/web/index.html +5 -0
- package/web/js/app.js +350 -18
package/server/update.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Update check: polls the npm registry for a newer wisp-agents and, when asked, installs it and
|
|
2
|
+
// relaunches wisp. How depends on how this copy was installed:
|
|
3
|
+
// global npm install -g wisp-agents@latest, then rerun bin/wisp.js from the same folder
|
|
4
|
+
// npx nothing to install: relaunch through `npx -y wisp-agents@latest`
|
|
5
|
+
// git a clone: we only tell you to pull
|
|
6
|
+
// Opt out with WISP_UPDATE_CHECK=0.
|
|
7
|
+
|
|
8
|
+
import { spawn } from 'node:child_process';
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
|
|
13
|
+
const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
14
|
+
const PKG = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
|
|
15
|
+
const REGISTRY = `https://registry.npmjs.org/${PKG.name}/latest`;
|
|
16
|
+
const DAY = 24 * 3600_000;
|
|
17
|
+
const TICK = 30 * 60_000; // timers stall while the machine sleeps, so poll the wall clock instead
|
|
18
|
+
|
|
19
|
+
export function newer(a, b) {
|
|
20
|
+
const p = (v) => String(v).replace(/^v/, '').split(/[-+]/)[0].split('.').map(Number);
|
|
21
|
+
const [x, y] = [p(a), p(b)];
|
|
22
|
+
for (let i = 0; i < 3; i++) if ((x[i] || 0) !== (y[i] || 0)) return (x[i] || 0) > (y[i] || 0);
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function installMethod(root = ROOT) {
|
|
27
|
+
if (fs.existsSync(path.join(root, '.git'))) return 'git';
|
|
28
|
+
if (/[\\/]_npx[\\/]/.test(root)) return 'npx';
|
|
29
|
+
return 'global';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class Updater {
|
|
33
|
+
constructor({ port, onChange = () => {}, env = process.env, fetch = globalThis.fetch, now = Date.now } = {}) {
|
|
34
|
+
this.port = port;
|
|
35
|
+
this.onChange = onChange;
|
|
36
|
+
this.fetch = fetch;
|
|
37
|
+
this.now = now;
|
|
38
|
+
this.checkedAt = -Infinity;
|
|
39
|
+
this.enabled = !/^(0|false|no|off)$/i.test(env.WISP_UPDATE_CHECK || '') && !env.CI;
|
|
40
|
+
this.method = installMethod();
|
|
41
|
+
this.state = { current: PKG.version, latest: null, available: false, method: this.method, status: 'idle', error: null };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
start() {
|
|
45
|
+
if (!this.enabled) return;
|
|
46
|
+
setTimeout(() => this.check(), 5000).unref();
|
|
47
|
+
setInterval(() => this.maybeCheck(), TICK).unref();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// At most once a day, however long wisp stays open (or asleep). Callers fire and forget:
|
|
51
|
+
// a new version reaches the UI through onChange.
|
|
52
|
+
maybeCheck() {
|
|
53
|
+
if (this.enabled && this.now() - this.checkedAt >= DAY) this.check();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
set(patch) {
|
|
57
|
+
Object.assign(this.state, patch);
|
|
58
|
+
this.onChange(this.view());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
view() {
|
|
62
|
+
const s = this.state;
|
|
63
|
+
const command = { global: `npm install -g ${PKG.name}@latest`, npx: `npx -y ${PKG.name}@latest`, git: 'git pull' }[s.method];
|
|
64
|
+
return { ...s, command, auto: s.method !== 'git' };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async check() {
|
|
68
|
+
this.checkedAt = this.now();
|
|
69
|
+
try {
|
|
70
|
+
const r = await this.fetch(REGISTRY, { headers: { accept: 'application/json' }, signal: AbortSignal.timeout(10_000) });
|
|
71
|
+
if (!r.ok) return this.view();
|
|
72
|
+
const { version } = await r.json();
|
|
73
|
+
const available = newer(version, PKG.version);
|
|
74
|
+
if (version !== this.state.latest || available !== this.state.available) this.set({ latest: version, available });
|
|
75
|
+
} catch {} // offline: try again next round
|
|
76
|
+
return this.view();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Installs (global) then hands off to a detached helper that waits for this server's port to
|
|
80
|
+
// free up and starts the new version. The caller shuts this process down.
|
|
81
|
+
async install() {
|
|
82
|
+
if (this.method === 'git') throw Object.assign(new Error('This is a git checkout: run `git pull` to update.'), { status: 400 });
|
|
83
|
+
if (this.state.status === 'installing') throw Object.assign(new Error('Already updating.'), { status: 409 });
|
|
84
|
+
this.set({ status: 'installing', error: null });
|
|
85
|
+
try {
|
|
86
|
+
if (this.method === 'global') await run(`npm install -g ${PKG.name}@latest`);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
this.set({ status: 'idle', error: err.message });
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
const args = process.versions.electron ? '' : ' --browser --no-open'; // desktop: reopen the app; browser: keep the tab
|
|
92
|
+
const node = JSON.stringify(process.versions.electron ? 'node' : process.execPath);
|
|
93
|
+
const relaunch = this.method === 'npx' ? `npx -y ${PKG.name}@latest${args}` : `${node} ${JSON.stringify(path.join(ROOT, 'bin', 'wisp.js'))}${args}`;
|
|
94
|
+
handOff(this.port, relaunch);
|
|
95
|
+
this.set({ status: 'restarting' });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function run(cmd) {
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
const child = spawn(cmd, { shell: true, stdio: ['ignore', 'ignore', 'pipe'], env: withoutElectronNode() });
|
|
102
|
+
let err = '';
|
|
103
|
+
child.stderr.on('data', (d) => (err = (err + d).slice(-2000)));
|
|
104
|
+
child.on('error', reject);
|
|
105
|
+
child.on('exit', (code) => {
|
|
106
|
+
if (code === 0) return resolve();
|
|
107
|
+
const hint = /EACCES|permission denied/i.test(err) ? ' (permission denied — run it yourself, maybe with sudo)' : '';
|
|
108
|
+
reject(Object.assign(new Error(`\`${cmd}\` failed${hint}: ${err.trim().split('\n').slice(-3).join(' ')}`), { status: 400 }));
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Electron's binary only acts as node with this set; never leak it into the relaunched app.
|
|
114
|
+
function withoutElectronNode() {
|
|
115
|
+
const env = { ...process.env };
|
|
116
|
+
delete env.ELECTRON_RUN_AS_NODE;
|
|
117
|
+
return env;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const WAITER = `
|
|
121
|
+
const net = require('node:net'), { spawn } = require('node:child_process');
|
|
122
|
+
const [port, cmd] = process.argv.slice(1);
|
|
123
|
+
delete process.env.ELECTRON_RUN_AS_NODE;
|
|
124
|
+
const start = Date.now();
|
|
125
|
+
(function poll() {
|
|
126
|
+
const s = net.connect(Number(port), '127.0.0.1');
|
|
127
|
+
s.once('connect', () => (s.destroy(), Date.now() - start < 60000 ? setTimeout(poll, 300) : process.exit(1)));
|
|
128
|
+
s.once('error', () => (spawn(cmd, { shell: true, detached: true, stdio: 'ignore' }).unref(), setTimeout(() => process.exit(0), 500)));
|
|
129
|
+
})();`;
|
|
130
|
+
|
|
131
|
+
function handOff(port, cmd) {
|
|
132
|
+
const electron = process.versions.electron;
|
|
133
|
+
const env = electron ? { ...withoutElectronNode(), ELECTRON_RUN_AS_NODE: '1' } : process.env;
|
|
134
|
+
const child = spawn(process.execPath, ['-e', WAITER, String(port), cmd], { detached: true, stdio: 'ignore', env });
|
|
135
|
+
child.unref();
|
|
136
|
+
}
|
package/web/css/app.css
CHANGED
|
@@ -912,6 +912,14 @@ pre.code { margin: 0 0 12px; padding: 12px 14px; border-radius: 10px; background
|
|
|
912
912
|
.modal h4 { margin: 22px 0 8px; font-size: 13px; font-weight: 600; color: var(--ink); }
|
|
913
913
|
.modal .hint { margin: 8px 0 0; font-size: 12.5px; color: var(--muted); }
|
|
914
914
|
.modal .hint code { font-size: 11.5px; padding: 1px 5px; border-radius: 5px; background: var(--bg-2); }
|
|
915
|
+
/* rerun dialog: the launcher's prompt + harness bar, in a modal. Its popovers sit above the overlay. */
|
|
916
|
+
.rr-box { padding: 8px 10px 8px; border-radius: 12px; border: 1px solid var(--line-2); background: var(--surface-2); transition: border-color 0.15s, box-shadow 0.15s; }
|
|
917
|
+
.rr-box:focus-within { border-color: color-mix(in srgb, var(--accent) 55%, var(--line-2)); box-shadow: 0 0 0 3px var(--accent-soft); }
|
|
918
|
+
.rr-prompt { display: block; width: 100%; resize: none; border: 0; outline: 0; background: transparent; padding: 6px 2px; font-size: 14.5px; line-height: 1.55; color: var(--ink); min-height: 72px; }
|
|
919
|
+
.rr-files { display: flex; flex-wrap: wrap; gap: 6px; padding: 2px 2px 6px; }
|
|
920
|
+
.rr-file { display: inline-flex; align-items: center; gap: 5px; height: 24px; padding: 0 8px; border-radius: 7px; font-size: 12px; color: var(--ink-2); background: var(--bg-2); }
|
|
921
|
+
.rr-file .i { width: 12px; height: 12px; color: var(--muted); }
|
|
922
|
+
.overlay:not([hidden]) ~ :is(.picker, .menu) { z-index: 55; }
|
|
915
923
|
.add-row { display: flex; gap: 8px; }
|
|
916
924
|
.path-input { position: relative; flex: 1; display: flex; align-items: center; gap: 8px; height: 38px; padding: 0 12px; border-radius: 11px; border: 1px solid var(--line-2); background: var(--surface-2); }
|
|
917
925
|
.path-input:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
|
|
@@ -950,6 +958,72 @@ pre.code { margin: 0 0 12px; padding: 12px 14px; border-radius: 10px; background
|
|
|
950
958
|
.switch[aria-checked='true']::after { transform: translateX(14px); }
|
|
951
959
|
.switch:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
952
960
|
.settings-modal input { flex: 1; max-width: 340px; height: 30px; border-radius: 9px; border: 1px solid var(--line); background: var(--surface-2); padding: 0 10px; font: 12px var(--mono); outline: 0; }
|
|
961
|
+
.setting #openMemory { margin-left: -4px; }
|
|
962
|
+
|
|
963
|
+
/* memory: what the background retros learned */
|
|
964
|
+
.memory-modal { width: min(680px, 100%); }
|
|
965
|
+
.memory-modal header { gap: 10px; }
|
|
966
|
+
.memory-modal header h2 { margin-right: auto; }
|
|
967
|
+
.mem-live { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted); }
|
|
968
|
+
.mem-live .orb.live { --s: 11px; border-width: 1.5px; }
|
|
969
|
+
.mem-lede { margin: -6px 0 14px; font-size: 13px; color: var(--muted); }
|
|
970
|
+
.mem-tabs { display: flex; align-items: center; gap: 2px; padding: 3px; overflow-x: auto; border-radius: 10px; background: var(--bg-2); border: 1px solid var(--line); }
|
|
971
|
+
.mem-tabs button { flex: none; display: inline-flex; align-items: center; gap: 6px; padding: 5px 10px; border-radius: 7px; font-size: 12.5px; color: var(--muted); white-space: nowrap; transition: background 0.12s, color 0.12s; }
|
|
972
|
+
.mem-tabs button:hover { color: var(--ink); }
|
|
973
|
+
.mem-tabs button.on { background: var(--surface); color: var(--ink); font-weight: 500; box-shadow: var(--shadow-1); }
|
|
974
|
+
.mem-tabs button span { min-width: 16px; padding: 0 4px; border-radius: 5px; font-size: 11px; font-variant-numeric: tabular-nums; text-align: center; background: var(--line); color: var(--muted); }
|
|
975
|
+
.mem-tabs button.on span { background: var(--accent-soft); color: var(--accent-ink); }
|
|
976
|
+
.mem-tabs i { flex: 1; min-width: 8px; }
|
|
977
|
+
.mem-body { padding-top: 6px; }
|
|
978
|
+
.mem-body.off .mem-sec, .mem-body.off .tl { opacity: 0.5; }
|
|
979
|
+
.mem-off { display: flex; align-items: center; gap: 8px; margin: 10px 0 2px; padding: 8px 10px; border-radius: 10px; font-size: 12.5px; color: var(--ink-2); background: var(--bg-2); }
|
|
980
|
+
.mem-off span { flex: 1; }
|
|
981
|
+
.mem-off .i { color: var(--muted); }
|
|
982
|
+
.mem-budget { margin: 10px 2px 0; }
|
|
983
|
+
.mem-sec h4 { display: flex; align-items: baseline; gap: 8px; margin: 18px 0 6px; }
|
|
984
|
+
.mem-sec h4 .muted { font-weight: 400; }
|
|
985
|
+
.mem-sec h4 em { margin-left: auto; font: 500 11px var(--mono); font-style: normal; color: var(--faint); }
|
|
986
|
+
.mem-sec h4 em.full { color: var(--warn); }
|
|
987
|
+
.mem-item { display: flex; align-items: flex-start; gap: 10px; padding: 8px 10px; margin: 0 -10px; border-radius: 10px; transition: background 0.12s; }
|
|
988
|
+
.mem-item:hover { background: var(--surface-2); }
|
|
989
|
+
.mem-ico { display: grid; place-items: center; flex: none; width: 22px; height: 22px; margin-top: 1px; border-radius: 7px; background: var(--accent-soft); color: var(--accent-ink); }
|
|
990
|
+
.mem-sec[data-kind='lessons'] .mem-ico, .mem-sec[data-kind='lessons'] .mem-edit .mem-ico { background: var(--add-bg); color: var(--good); }
|
|
991
|
+
.mem-ico .i { width: 13px; height: 13px; }
|
|
992
|
+
.mem-main { flex: 1; min-width: 0; }
|
|
993
|
+
.mem-main p { margin: 0; font-size: 13.5px; line-height: 1.45; color: var(--ink); overflow-wrap: anywhere; }
|
|
994
|
+
.mem-src { font-size: 11.5px; color: var(--faint); }
|
|
995
|
+
.mem-src a, .tl-head a { color: inherit; text-decoration: none; }
|
|
996
|
+
.mem-src a:hover, .tl-head a:hover { color: var(--accent-ink); text-decoration: underline; }
|
|
997
|
+
.mem-acts { display: flex; gap: 2px; opacity: 0; transition: opacity 0.12s; }
|
|
998
|
+
.mem-item:hover .mem-acts, .mem-item:focus-within .mem-acts { opacity: 1; }
|
|
999
|
+
.mem-edit { align-items: center; padding-top: 7px; padding-bottom: 7px; background: var(--surface-2); box-shadow: inset 0 0 0 1px var(--accent); }
|
|
1000
|
+
.mem-edit input { flex: 1; min-width: 0; border: 0; outline: 0; background: transparent; font: 13.5px var(--sans); color: var(--ink); }
|
|
1001
|
+
.memory-modal .mem-edit input { max-width: none; height: auto; padding: 0; }
|
|
1002
|
+
.mem-add { display: inline-flex; align-items: center; gap: 6px; margin: 4px 0 0; padding: 4px 8px 4px 6px; border-radius: 7px; font-size: 12.5px; color: var(--muted); }
|
|
1003
|
+
.mem-add:hover { color: var(--accent-ink); background: var(--accent-soft); }
|
|
1004
|
+
.mem-add .i { width: 13px; height: 13px; }
|
|
1005
|
+
.mem-empty { margin: 2px 0 4px; font-size: 12.5px; color: var(--faint); }
|
|
1006
|
+
.mem-body > .mem-empty { margin: 22px 0 8px; text-align: center; }
|
|
1007
|
+
/* retros: a timeline, newest first */
|
|
1008
|
+
.tl { position: relative; display: flex; gap: 12px; padding: 12px 0 2px; }
|
|
1009
|
+
.tl:first-of-type { margin-top: 6px; }
|
|
1010
|
+
.tl::before { content: ''; position: absolute; left: 4.5px; top: 26px; bottom: -12px; width: 1px; background: var(--line); }
|
|
1011
|
+
.tl:last-child::before { display: none; }
|
|
1012
|
+
.tl-dot { flex: none; display: grid; place-items: center; width: 10px; height: 10px; margin-top: 4px; border-radius: 50%; background: var(--good); box-shadow: 0 0 0 3px var(--surface); }
|
|
1013
|
+
.tl.nothing .tl-dot { background: var(--line-2); }
|
|
1014
|
+
.tl.failed .tl-dot { background: var(--bad); }
|
|
1015
|
+
.tl.running .tl-dot { background: none; }
|
|
1016
|
+
.tl.running .orb.live { --s: 12px; border-width: 1.5px; }
|
|
1017
|
+
.tl-main { flex: 1; min-width: 0; }
|
|
1018
|
+
.tl-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 3px; }
|
|
1019
|
+
.tl-head b { font-weight: 600; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1020
|
+
.tl-head .muted:last-child { margin-left: auto; flex: none; }
|
|
1021
|
+
.tl-main p { margin: 2px 0; font-size: 12.5px; line-height: 1.45; }
|
|
1022
|
+
.tl-add::before { content: '+ '; color: var(--good); font-weight: 600; }
|
|
1023
|
+
.tl-del { color: var(--muted); }
|
|
1024
|
+
.tl-del::before { content: '− '; font-weight: 600; }
|
|
1025
|
+
.tl-tag { margin-left: 4px; font-size: 11px; color: var(--faint); white-space: nowrap; }
|
|
1026
|
+
.tl-err { color: var(--bad); font: 11.5px/1.45 var(--mono); }
|
|
953
1027
|
.agents { margin-top: 18px; }
|
|
954
1028
|
.agent-row { display: flex; align-items: center; gap: 10px; padding: 4px 0; font-size: 13px; }
|
|
955
1029
|
.keys-modal { width: min(380px, 100%); }
|
|
@@ -964,6 +1038,14 @@ pre.code { margin: 0 0 12px; padding: 12px 14px; border-radius: 10px; background
|
|
|
964
1038
|
.search-btn:hover { border-color: var(--line-2); color: var(--ink); }
|
|
965
1039
|
.search-btn .i { width: 14px; height: 14px; }
|
|
966
1040
|
.search-btn kbd { margin-left: 8px; }
|
|
1041
|
+
.update-btn {
|
|
1042
|
+
display: inline-flex; align-items: center; gap: 6px; height: 30px; padding: 0 10px; border-radius: 9px;
|
|
1043
|
+
background: var(--accent-soft); color: var(--accent-ink); font-size: 13px; font-weight: 500; transition: background 0.15s;
|
|
1044
|
+
}
|
|
1045
|
+
.update-btn[hidden] { display: none; }
|
|
1046
|
+
.update-btn:hover { background: var(--accent); color: #fff; }
|
|
1047
|
+
.update-btn.busy { opacity: 0.7; cursor: progress; }
|
|
1048
|
+
.update-btn .i { width: 14px; height: 14px; }
|
|
967
1049
|
.overlay:has(.palette) { place-items: start center; padding-top: min(14vh, 120px); background: rgba(0, 0, 0, 0.18); }
|
|
968
1050
|
.palette {
|
|
969
1051
|
display: flex; flex-direction: column; width: min(640px, 100%); max-height: min(560px, 72vh); overflow: hidden; border-radius: 14px;
|
|
@@ -1021,7 +1103,7 @@ pre.code { margin: 0 0 12px; padding: 12px 14px; border-radius: 10px; background
|
|
|
1021
1103
|
/* ---------- responsive ---------- */
|
|
1022
1104
|
@media (max-width: 720px) {
|
|
1023
1105
|
.topbar { padding: 0 12px; gap: 10px; }
|
|
1024
|
-
.search-btn span, .search-btn kbd { display: none; }
|
|
1106
|
+
.search-btn span, .search-btn kbd, .update-btn span { display: none; }
|
|
1025
1107
|
.search-btn { padding: 0 8px; border-color: transparent; background: none; }
|
|
1026
1108
|
.feed-wrap { padding: 14px 16px 80px; }
|
|
1027
1109
|
.it-side .q-proj, .it-actions span, .c-done span { display: none; }
|
package/web/index.html
CHANGED
|
@@ -44,9 +44,11 @@
|
|
|
44
44
|
<symbol id="i-agent" viewBox="0 0 24 24"><circle cx="9" cy="8.5" r="3.2" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M3 19c.8-3.2 3.2-5 6-5s5.2 1.8 6 5M16 5.5a3 3 0 010 6M18 14.3c1.6.6 2.6 2.2 3 4.7" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></symbol>
|
|
45
45
|
<symbol id="i-plan" viewBox="0 0 24 24"><path d="M9 6.5h11M9 12h11M9 17.5h11" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/><path d="M3.8 6.5l1.2 1.2 2-2.2M3.8 12l1.2 1.2 2-2.2" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><circle cx="5" cy="17.5" r="1.2" fill="currentColor"/></symbol>
|
|
46
46
|
<symbol id="i-tool" viewBox="0 0 24 24"><path d="M14.5 6.5a4 4 0 00-5.2 5.2L4 17l3 3 5.3-5.3a4 4 0 005.2-5.2l-2.5 2.5-2.5-.5-.5-2.5 2.5-2.5z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></symbol>
|
|
47
|
+
<symbol id="i-rerun" viewBox="0 0 24 24"><path d="M19.5 12a7.5 7.5 0 11-1.8-4.9M20 4v4h-4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></symbol>
|
|
47
48
|
<symbol id="i-replay" viewBox="0 0 24 24"><path d="M4.5 12a7.5 7.5 0 101.8-4.9M4 4v4h4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></symbol>
|
|
48
49
|
<symbol id="i-text" viewBox="0 0 24 24"><path d="M5 7h14M5 12h14M5 17h9" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></symbol>
|
|
49
50
|
<symbol id="i-back" viewBox="0 0 24 24"><path d="M15 6l-6 6 6 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></symbol>
|
|
51
|
+
<symbol id="i-update" viewBox="0 0 24 24"><path d="M12 16V4m-5 5l5-5 5 5M5 20h14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></symbol>
|
|
50
52
|
<symbol id="i-menu" viewBox="0 0 24 24"><path d="M4 7h16M4 12h16M4 17h16" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></symbol>
|
|
51
53
|
</defs>
|
|
52
54
|
</svg>
|
|
@@ -58,6 +60,9 @@
|
|
|
58
60
|
<span class="word">wisp</span>
|
|
59
61
|
</a>
|
|
60
62
|
<div class="projects" id="projects"></div>
|
|
63
|
+
<button class="update-btn" id="updateBtn" hidden>
|
|
64
|
+
<svg class="i"><use href="#i-update"/></svg><span>Update</span>
|
|
65
|
+
</button>
|
|
61
66
|
<button class="search-btn" id="searchBtn" title="Search">
|
|
62
67
|
<svg class="i"><use href="#i-search"/></svg><span>Search</span><kbd class="mod-k"></kbd>
|
|
63
68
|
</button>
|