clideck 1.22.7 → 1.23.1
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/.github/pull_request_template.md +27 -0
- package/CONTRIBUTING.md +53 -0
- package/README.md +5 -1
- package/activity.js +6 -1
- package/handlers.js +94 -20
- package/package.json +1 -1
- package/plugin-loader.js +12 -2
- package/public/img/clideck-logo-icon.png +0 -0
- package/public/img/clideck-logo-terminal-panel.png +0 -0
- package/public/index.html +101 -0
- package/public/js/app.js +300 -1
- package/public/js/prompts.js +13 -3
- package/public/js/settings.js +1 -1
- package/public/js/terminals.js +29 -4
- package/public/tailwind.css +1 -1
- package/server.js +1 -1
- package/sessions.js +28 -12
- package/telemetry-receiver.js +0 -8
- package/transcript.js +168 -5
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
## What changed
|
|
2
|
+
|
|
3
|
+
<!-- One or two sentences. What does this PR do? -->
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
<!-- What problem does this solve? Link to an issue or discussion if one exists. -->
|
|
8
|
+
|
|
9
|
+
## How to verify
|
|
10
|
+
|
|
11
|
+
<!--
|
|
12
|
+
Steps to test this locally:
|
|
13
|
+
1. Run `node server.js`
|
|
14
|
+
2. Open http://localhost:4000
|
|
15
|
+
3. ...
|
|
16
|
+
-->
|
|
17
|
+
|
|
18
|
+
## What's out of scope
|
|
19
|
+
|
|
20
|
+
<!-- What did you intentionally NOT change? Helps reviewers stay focused. -->
|
|
21
|
+
|
|
22
|
+
## Checklist
|
|
23
|
+
|
|
24
|
+
- [ ] Tested locally with `node server.js`
|
|
25
|
+
- [ ] One focused change, not bundled with unrelated fixes
|
|
26
|
+
- [ ] No new external service dependencies
|
|
27
|
+
- [ ] Does not read, store, or intercept agent prompts/responses
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Contributing to CliDeck
|
|
2
|
+
|
|
3
|
+
This document explains what we accept, what needs discussion first, and how to structure a PR so it gets reviewed fast.
|
|
4
|
+
|
|
5
|
+
## What to contribute
|
|
6
|
+
|
|
7
|
+
**Bug fixes and small improvements** — open a PR directly.
|
|
8
|
+
Crashes, rendering glitches, typos, broken links, edge cases, performance fixes.
|
|
9
|
+
|
|
10
|
+
**New features, architecture changes, or new agent presets** — [start a Discussion](https://github.com/rustykuntz/clideck/discussions) first.
|
|
11
|
+
Describe the problem, your proposed approach, and any trade-offs. If the idea gets a thumbs up, then open a PR.
|
|
12
|
+
|
|
13
|
+
**Plugins** — the best way to contribute new functionality.
|
|
14
|
+
CliDeck has a plugin system so features can be added without touching core code. If your idea can be a plugin, it should be. See the [documentation](https://docs.clideck.dev/) for how to build one.
|
|
15
|
+
|
|
16
|
+
## What we will reject
|
|
17
|
+
|
|
18
|
+
- PRs that change multiple unrelated things. One change per PR.
|
|
19
|
+
- PRs with no description of what changed or why.
|
|
20
|
+
- Formatting-only changes (whitespace, semicolons, reordering imports).
|
|
21
|
+
- Features that add external service dependencies or phone home.
|
|
22
|
+
- Changes that break the zero-interference guarantee — CliDeck never reads, stores, or intercepts agent prompts and responses.
|
|
23
|
+
|
|
24
|
+
## Before you open a PR
|
|
25
|
+
|
|
26
|
+
1. **Test locally.** Run `node server.js`, open `http://localhost:4000`, and verify your change works.
|
|
27
|
+
2. **Keep it focused.** If you found a bug while working on a feature, that's two PRs.
|
|
28
|
+
3. **Match the existing style.** No linter is enforced, but stay consistent with surrounding code.
|
|
29
|
+
4. **Don't bundle dependency changes** unless your PR specifically requires them.
|
|
30
|
+
|
|
31
|
+
## PR structure
|
|
32
|
+
|
|
33
|
+
Fill out the PR template. The key fields:
|
|
34
|
+
|
|
35
|
+
- **What changed** — one or two sentences.
|
|
36
|
+
- **Why** — the problem this solves.
|
|
37
|
+
- **How to verify** — steps to confirm it works.
|
|
38
|
+
- **What's out of scope** — what you intentionally did not change.
|
|
39
|
+
|
|
40
|
+
Small PRs get reviewed faster. If your diff is over 300 lines, consider splitting it.
|
|
41
|
+
|
|
42
|
+
## Built a fork?
|
|
43
|
+
|
|
44
|
+
If you've forked CliDeck and built something — a new agent integration, a workflow improvement, a plugin — open a Discussion in **Show & Tell**. The best fork features get upstreamed.
|
|
45
|
+
|
|
46
|
+
## Licensing of contributions
|
|
47
|
+
|
|
48
|
+
By submitting a contribution that is accepted into the repository, you agree
|
|
49
|
+
that it will be licensed under the MIT License.
|
|
50
|
+
|
|
51
|
+
## Conduct
|
|
52
|
+
|
|
53
|
+
Be constructive. Review comments are about the code, not the person. If a PR is rejected, the reason will be explained.
|
package/README.md
CHANGED
package/activity.js
CHANGED
|
@@ -51,6 +51,11 @@ function stop() {
|
|
|
51
51
|
interval = null;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
function isActive(id) {
|
|
55
|
+
const s = stream[id];
|
|
56
|
+
return s ? (Date.now() - s.lastOutAt < 2000) : false;
|
|
57
|
+
}
|
|
58
|
+
|
|
54
59
|
function clear(id) { delete net[id]; delete stream[id]; }
|
|
55
60
|
|
|
56
|
-
module.exports = { start, stop, trackIn, trackOut, clear };
|
|
61
|
+
module.exports = { start, stop, trackIn, trackOut, isActive, clear };
|
package/handlers.js
CHANGED
|
@@ -11,6 +11,23 @@ for (const p of presets) if (p.presetId === 'shell') p.command = defaultShell;
|
|
|
11
11
|
const transcript = require('./transcript');
|
|
12
12
|
const plugins = require('./plugin-loader');
|
|
13
13
|
|
|
14
|
+
const opencodePluginDir = join(
|
|
15
|
+
process.platform === 'win32' ? (process.env.APPDATA || join(os.homedir(), 'AppData', 'Roaming')) : join(os.homedir(), '.config'),
|
|
16
|
+
'opencode', 'plugins'
|
|
17
|
+
);
|
|
18
|
+
// Resolve opencode preset paths for current platform
|
|
19
|
+
for (const p of presets) {
|
|
20
|
+
if (p.presetId !== 'opencode') continue;
|
|
21
|
+
const bridgePath = join(opencodePluginDir, 'clideck-bridge.js');
|
|
22
|
+
if (p.pluginPath) p.pluginPath = bridgePath;
|
|
23
|
+
if (p.pluginSetup) {
|
|
24
|
+
const copyCmd = process.platform === 'win32'
|
|
25
|
+
? `copy opencode-plugin\\clideck-bridge.js "${opencodePluginDir}\\"`
|
|
26
|
+
: `cp opencode-plugin/clideck-bridge.js ${opencodePluginDir}/`;
|
|
27
|
+
p.pluginSetup = `Install the CliDeck bridge plugin to enable real-time status and resume.\n\n${copyCmd}`;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
14
31
|
// Check which agent binaries are available on PATH
|
|
15
32
|
const whichCmd = process.platform === 'win32' ? 'where' : 'which';
|
|
16
33
|
function checkAvailability() {
|
|
@@ -47,8 +64,7 @@ function detectTelemetryConfig(c) {
|
|
|
47
64
|
detected = !!s.telemetry?.enabled && (s.telemetry?.otlpEndpoint || '').includes(`localhost:${port}`);
|
|
48
65
|
} catch {}
|
|
49
66
|
} else if (preset.presetId === 'opencode') {
|
|
50
|
-
|
|
51
|
-
detected = existsSync(join(ocPlugins, 'clideck-bridge.js')) || existsSync(join(ocPlugins, 'termix-bridge.js'));
|
|
67
|
+
detected = existsSync(join(opencodePluginDir, 'clideck-bridge.js')) || existsSync(join(opencodePluginDir, 'termix-bridge.js'));
|
|
52
68
|
} else { continue; }
|
|
53
69
|
if (detected !== !!cmd.telemetryEnabled) {
|
|
54
70
|
cmd.telemetryEnabled = detected;
|
|
@@ -60,14 +76,18 @@ function detectTelemetryConfig(c) {
|
|
|
60
76
|
return changed;
|
|
61
77
|
}
|
|
62
78
|
|
|
79
|
+
function configForClient() {
|
|
80
|
+
return { ...cfg, pluginsDir: plugins.PLUGINS_DIR };
|
|
81
|
+
}
|
|
82
|
+
|
|
63
83
|
function onConnection(ws) {
|
|
64
84
|
sessions.clients.add(ws);
|
|
65
85
|
|
|
66
|
-
ws.send(JSON.stringify({ type: 'config', config:
|
|
86
|
+
ws.send(JSON.stringify({ type: 'config', config: configForClient() }));
|
|
67
87
|
ws.send(JSON.stringify({ type: 'themes', themes }));
|
|
68
88
|
ws.send(JSON.stringify({ type: 'presets', presets }));
|
|
69
89
|
ws.send(JSON.stringify({ type: 'sessions', list: sessions.list() }));
|
|
70
|
-
ws.send(JSON.stringify({ type: 'sessions.resumable', list: sessions.getResumable() }));
|
|
90
|
+
ws.send(JSON.stringify({ type: 'sessions.resumable', list: sessions.getResumable(cfg) }));
|
|
71
91
|
ws.send(JSON.stringify({ type: 'transcript.cache', cache: transcript.getCache() }));
|
|
72
92
|
ws.send(JSON.stringify({ type: 'plugins', list: plugins.getInfo() }));
|
|
73
93
|
sessions.sendBuffers(ws);
|
|
@@ -82,14 +102,20 @@ function onConnection(ws) {
|
|
|
82
102
|
case 'session.restart': console.log('[handler] session.restart', msg.id); sessions.restart(msg, ws, cfg); break;
|
|
83
103
|
case 'input': sessions.input(msg); break;
|
|
84
104
|
case 'session.statusReport':
|
|
85
|
-
if (sessions.getSessions().has(msg.id))
|
|
105
|
+
if (sessions.getSessions().has(msg.id)) {
|
|
106
|
+
plugins.notifyStatus(msg.id, !!msg.working);
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
case 'terminal.buffer':
|
|
110
|
+
require('./transcript').storeBuffer(msg.id, msg.lines);
|
|
111
|
+
sessions.broadcast({ type: 'screen.updated', id: msg.id });
|
|
86
112
|
break;
|
|
87
113
|
case 'resize': sessions.resize(msg); break;
|
|
88
114
|
case 'rename': sessions.rename(msg); break;
|
|
89
|
-
case 'close': sessions.close(msg); break;
|
|
115
|
+
case 'close': sessions.close(msg, cfg); break;
|
|
90
116
|
|
|
91
117
|
case 'config.get':
|
|
92
|
-
ws.send(JSON.stringify({ type: 'config', config:
|
|
118
|
+
ws.send(JSON.stringify({ type: 'config', config: configForClient() }));
|
|
93
119
|
break;
|
|
94
120
|
|
|
95
121
|
case 'checkAvailability':
|
|
@@ -98,10 +124,11 @@ function onConnection(ws) {
|
|
|
98
124
|
break;
|
|
99
125
|
|
|
100
126
|
case 'config.update':
|
|
127
|
+
delete msg.config.pluginsDir;
|
|
101
128
|
cfg = { ...cfg, ...msg.config };
|
|
102
129
|
detectTelemetryConfig(cfg);
|
|
103
130
|
config.save(cfg);
|
|
104
|
-
sessions.broadcast({ type: 'config', config:
|
|
131
|
+
sessions.broadcast({ type: 'config', config: configForClient() });
|
|
105
132
|
break;
|
|
106
133
|
|
|
107
134
|
case 'session.theme': {
|
|
@@ -129,7 +156,7 @@ function onConnection(ws) {
|
|
|
129
156
|
}
|
|
130
157
|
}
|
|
131
158
|
config.save(cfg);
|
|
132
|
-
sessions.broadcast({ type: 'config', config:
|
|
159
|
+
sessions.broadcast({ type: 'config', config: configForClient() });
|
|
133
160
|
ws.send(JSON.stringify({
|
|
134
161
|
type: 'telemetry.autosetup.result',
|
|
135
162
|
presetId: msg.presetId,
|
|
@@ -159,7 +186,7 @@ function onConnection(ws) {
|
|
|
159
186
|
}
|
|
160
187
|
}
|
|
161
188
|
config.save(cfg);
|
|
162
|
-
sessions.broadcast({ type: 'config', config:
|
|
189
|
+
sessions.broadcast({ type: 'config', config: configForClient() });
|
|
163
190
|
break;
|
|
164
191
|
}
|
|
165
192
|
|
|
@@ -185,11 +212,11 @@ function onConnection(ws) {
|
|
|
185
212
|
if (!proj) break;
|
|
186
213
|
// Kill all sessions in this project
|
|
187
214
|
for (const s of sessions.list()) {
|
|
188
|
-
if (s.projectId === msg.id) sessions.close({ id: s.id });
|
|
215
|
+
if (s.projectId === msg.id) sessions.close({ id: s.id }, cfg);
|
|
189
216
|
}
|
|
190
217
|
cfg.projects = cfg.projects.filter(p => p.id !== msg.id);
|
|
191
218
|
config.save(cfg);
|
|
192
|
-
sessions.broadcast({ type: 'config', config:
|
|
219
|
+
sessions.broadcast({ type: 'config', config: configForClient() });
|
|
193
220
|
break;
|
|
194
221
|
}
|
|
195
222
|
|
|
@@ -207,6 +234,55 @@ function onConnection(ws) {
|
|
|
207
234
|
sessions.broadcast({ type: 'plugins', list: plugins.getInfo() });
|
|
208
235
|
break;
|
|
209
236
|
|
|
237
|
+
case 'remote.status': {
|
|
238
|
+
let installed = false;
|
|
239
|
+
try { execFileSync(whichCmd, ['clideck-remote'], { stdio: 'ignore' }); installed = true; } catch {}
|
|
240
|
+
if (!installed) { ws.send(JSON.stringify({ type: 'remote.status', installed: false })); break; }
|
|
241
|
+
require('child_process').execFile('clideck-remote', ['status', '--json'], { timeout: 5000 }, (err, stdout) => {
|
|
242
|
+
if (err) { ws.send(JSON.stringify({ type: 'remote.status', installed: true })); return; }
|
|
243
|
+
try { ws.send(JSON.stringify({ type: 'remote.status', installed: true, ...JSON.parse(stdout) })); }
|
|
244
|
+
catch { ws.send(JSON.stringify({ type: 'remote.status', installed: true })); }
|
|
245
|
+
});
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
case 'remote.pair': {
|
|
250
|
+
require('child_process').execFile('clideck-remote', ['pair', '--json'], { timeout: 15000 }, (err, stdout) => {
|
|
251
|
+
if (err) { ws.send(JSON.stringify({ type: 'remote.error', error: err.message })); return; }
|
|
252
|
+
try { ws.send(JSON.stringify({ type: 'remote.paired', ...JSON.parse(stdout) })); }
|
|
253
|
+
catch { ws.send(JSON.stringify({ type: 'remote.error', error: 'Invalid response from clideck-remote' })); }
|
|
254
|
+
});
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
case 'remote.unpair': {
|
|
259
|
+
require('child_process').execFile('clideck-remote', ['unpair', '--json'], { timeout: 5000 }, (err) => {
|
|
260
|
+
if (err) {
|
|
261
|
+
ws.send(JSON.stringify({ type: 'remote.error', error: err.message }));
|
|
262
|
+
} else {
|
|
263
|
+
sessions.broadcast({ type: 'remote.unpaired' });
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
case 'remote.getHistory': {
|
|
270
|
+
const turns = transcript.getScreenTurns(msg.id, sessions.getSessions().get(msg.id)?.presetId)
|
|
271
|
+
|| transcript.getLastTurns(msg.id, msg.limit || 50);
|
|
272
|
+
ws.send(JSON.stringify({ type: 'remote.history', id: msg.id, turns: turns || [] }));
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
case 'remote.install': {
|
|
277
|
+
const proc = require('child_process').spawn('npm', ['install', '-g', 'clideck-remote'], {
|
|
278
|
+
shell: true, stdio: ['ignore', 'pipe', 'pipe'],
|
|
279
|
+
});
|
|
280
|
+
proc.stdout.on('data', d => ws.send(JSON.stringify({ type: 'remote.install.progress', text: d.toString() })));
|
|
281
|
+
proc.stderr.on('data', d => ws.send(JSON.stringify({ type: 'remote.install.progress', text: d.toString() })));
|
|
282
|
+
proc.on('close', code => ws.send(JSON.stringify({ type: 'remote.install.done', success: code === 0 })));
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
|
|
210
286
|
default:
|
|
211
287
|
if (msg.type?.startsWith('plugin.')) plugins.handleMessage(msg);
|
|
212
288
|
break;
|
|
@@ -256,14 +332,13 @@ function applyTelemetryConfig(preset) {
|
|
|
256
332
|
}
|
|
257
333
|
|
|
258
334
|
if (preset.presetId === 'opencode') {
|
|
259
|
-
const pluginDir = join(home, '.config', 'opencode', 'plugins');
|
|
260
335
|
const src = join(__dirname, 'opencode-plugin', 'clideck-bridge.js');
|
|
261
|
-
mkdirSync(
|
|
262
|
-
copyFileSync(src, join(
|
|
336
|
+
mkdirSync(opencodePluginDir, { recursive: true });
|
|
337
|
+
copyFileSync(src, join(opencodePluginDir, 'clideck-bridge.js'));
|
|
263
338
|
// Remove old termix-bridge.js if present
|
|
264
|
-
const old = join(
|
|
339
|
+
const old = join(opencodePluginDir, 'termix-bridge.js');
|
|
265
340
|
if (existsSync(old)) try { unlinkSync(old); } catch {}
|
|
266
|
-
return { success: true, message:
|
|
341
|
+
return { success: true, message: `Installed bridge plugin to ${opencodePluginDir}` };
|
|
267
342
|
}
|
|
268
343
|
|
|
269
344
|
return { success: false, message: `No auto-setup for ${preset.presetId}` };
|
|
@@ -297,9 +372,8 @@ function removeTelemetryConfig(preset) {
|
|
|
297
372
|
}
|
|
298
373
|
|
|
299
374
|
if (preset.presetId === 'opencode') {
|
|
300
|
-
|
|
301
|
-
try { unlinkSync(join(
|
|
302
|
-
try { unlinkSync(join(dir, 'termix-bridge.js')); } catch {}
|
|
375
|
+
try { unlinkSync(join(opencodePluginDir, 'clideck-bridge.js')); } catch {}
|
|
376
|
+
try { unlinkSync(join(opencodePluginDir, 'termix-bridge.js')); } catch {}
|
|
303
377
|
return { success: true, message: 'Removed bridge plugin' };
|
|
304
378
|
}
|
|
305
379
|
|
package/package.json
CHANGED
package/plugin-loader.js
CHANGED
|
@@ -32,6 +32,7 @@ const plugins = new Map();
|
|
|
32
32
|
const inputHooks = [];
|
|
33
33
|
const outputHooks = [];
|
|
34
34
|
const statusHooks = [];
|
|
35
|
+
const transcriptHooks = [];
|
|
35
36
|
const sessionStatus = new Map(); // sessionId → boolean (dedup multi-client reports)
|
|
36
37
|
const frontendHandlers = new Map();
|
|
37
38
|
let broadcastFn = null;
|
|
@@ -41,7 +42,7 @@ let saveConfigFn = null;
|
|
|
41
42
|
const settingsChangeHandlers = new Map(); // pluginId → [fn]
|
|
42
43
|
|
|
43
44
|
function removeHooks(pluginId) {
|
|
44
|
-
for (const arr of [inputHooks, outputHooks, statusHooks]) {
|
|
45
|
+
for (const arr of [inputHooks, outputHooks, statusHooks, transcriptHooks]) {
|
|
45
46
|
for (let i = arr.length - 1; i >= 0; i--) {
|
|
46
47
|
if (arr[i].pluginId === pluginId) arr.splice(i, 1);
|
|
47
48
|
}
|
|
@@ -111,6 +112,7 @@ function buildApi(pluginId, pluginDir, state) {
|
|
|
111
112
|
onSessionInput(fn) { inputHooks.push({ pluginId, fn }); },
|
|
112
113
|
onSessionOutput(fn) { outputHooks.push({ pluginId, fn }); },
|
|
113
114
|
onStatusChange(fn) { statusHooks.push({ pluginId, fn }); },
|
|
115
|
+
onTranscriptEntry(fn) { transcriptHooks.push({ pluginId, fn }); },
|
|
114
116
|
|
|
115
117
|
sendToFrontend(event, data = {}) {
|
|
116
118
|
broadcastFn?.({ ...data, type: `plugin.${pluginId}.${event}` });
|
|
@@ -184,6 +186,13 @@ function notifyStatus(id, working) {
|
|
|
184
186
|
}
|
|
185
187
|
}
|
|
186
188
|
|
|
189
|
+
function notifyTranscript(id, role, text) {
|
|
190
|
+
for (const h of transcriptHooks) {
|
|
191
|
+
try { h.fn(id, role, text); }
|
|
192
|
+
catch (e) { console.error(`[plugin:${h.pluginId}] transcript error: ${e.message}`); }
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
187
196
|
function updateSetting(pluginId, key, value) {
|
|
188
197
|
// Validate plugin exists (also prevents __proto__ pollution — Map lookup returns undefined)
|
|
189
198
|
const plugin = plugins.get(pluginId);
|
|
@@ -281,7 +290,8 @@ function shutdown() {
|
|
|
281
290
|
function clearStatus(id) { sessionStatus.delete(id); }
|
|
282
291
|
|
|
283
292
|
module.exports = {
|
|
293
|
+
PLUGINS_DIR,
|
|
284
294
|
init, shutdown,
|
|
285
|
-
transformInput, notifyOutput, notifyStatus, clearStatus,
|
|
295
|
+
transformInput, notifyOutput, notifyStatus, notifyTranscript, clearStatus,
|
|
286
296
|
handleMessage, updateSetting, getInfo, resolveFile,
|
|
287
297
|
};
|
|
Binary file
|
|
Binary file
|
package/public/index.html
CHANGED
|
@@ -148,6 +148,9 @@
|
|
|
148
148
|
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v4m0 12v4M2 12h4m12 0h4"/><circle cx="12" cy="12" r="3"/><path d="M12 8V6m0 12v-2M8 12H6m12 0h-2"/></svg>
|
|
149
149
|
</button>
|
|
150
150
|
<div class="flex-1"></div>
|
|
151
|
+
<button class="rail-btn w-9 h-9 flex items-center justify-center rounded-lg text-slate-500 hover:text-slate-300 hover:bg-slate-800/50 transition-colors" id="btn-remote" title="Mobile Remote">
|
|
152
|
+
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>
|
|
153
|
+
</button>
|
|
151
154
|
<button class="theme-toggle rail-btn w-9 h-9 flex items-center justify-center rounded-lg text-slate-500 hover:text-slate-300 hover:bg-slate-800/50 transition-colors" id="btn-theme-toggle" title="Toggle light/dark mode">
|
|
152
155
|
<svg class="icon-sun w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>
|
|
153
156
|
<svg class="icon-moon w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
|
@@ -338,6 +341,104 @@
|
|
|
338
341
|
|
|
339
342
|
</div>
|
|
340
343
|
|
|
344
|
+
<!-- Remote modal -->
|
|
345
|
+
<div id="remote-modal" class="absolute inset-0 z-[260] bg-black/60 backdrop-blur-sm hidden items-center justify-center">
|
|
346
|
+
<div style="background:var(--color-dialog);border:1px solid color-mix(in srgb, var(--color-muted) 40%, transparent);box-shadow:0 25px 60px -12px var(--color-shadow)" class="rounded-2xl w-[340px] flex flex-col overflow-hidden">
|
|
347
|
+
<div class="px-5 py-3.5 flex items-center justify-between">
|
|
348
|
+
<span class="text-[13px] font-semibold text-slate-200">Mobile Remote</span>
|
|
349
|
+
<button id="remote-close" class="w-6 h-6 flex items-center justify-center rounded-md text-slate-500 hover:text-slate-300 hover:bg-slate-700/50 transition-colors">
|
|
350
|
+
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
|
351
|
+
</button>
|
|
352
|
+
</div>
|
|
353
|
+
|
|
354
|
+
<!-- Intro (not installed) -->
|
|
355
|
+
<div id="remote-intro" class="hidden px-6 py-6 flex flex-col items-center gap-4">
|
|
356
|
+
<svg class="w-12 h-12 text-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>
|
|
357
|
+
<h3 class="text-[13px] font-semibold text-slate-200">CliDeck Mobile Remote</h3>
|
|
358
|
+
<p class="text-xs text-slate-400 text-center leading-relaxed">Control your AI agents from your phone. See live status, send messages, and get notifications — all end-to-end encrypted.</p>
|
|
359
|
+
<button id="remote-add" class="mt-1 w-full px-4 py-2.5 text-[13px] font-medium bg-blue-600 hover:bg-blue-500 text-white rounded-lg transition-colors">Add to CliDeck</button>
|
|
360
|
+
<p class="text-[11px] text-slate-600 text-center">Installs the <code class="text-slate-500">clideck-remote</code> package via npm</p>
|
|
361
|
+
</div>
|
|
362
|
+
|
|
363
|
+
<!-- Installing -->
|
|
364
|
+
<div id="remote-installing" class="hidden px-5 py-4 flex flex-col gap-3">
|
|
365
|
+
<div class="flex items-center gap-2">
|
|
366
|
+
<svg class="w-4 h-4 animate-spin text-blue-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M12 2a10 10 0 0 1 10 10"/></svg>
|
|
367
|
+
<span class="text-xs text-slate-300">Installing clideck-remote…</span>
|
|
368
|
+
</div>
|
|
369
|
+
<pre id="remote-install-log" class="max-h-[160px] overflow-y-auto px-3 py-2 bg-slate-900/70 rounded-lg border border-slate-700/40 text-[11px] text-slate-400 font-mono leading-relaxed whitespace-pre-wrap"></pre>
|
|
370
|
+
</div>
|
|
371
|
+
|
|
372
|
+
<!-- Connecting -->
|
|
373
|
+
<div id="remote-connecting" class="hidden px-5 py-6 flex flex-col items-center gap-2">
|
|
374
|
+
<svg class="w-6 h-6 animate-spin text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M12 2a10 10 0 0 1 10 10"/></svg>
|
|
375
|
+
<span class="text-xs text-slate-400">Connecting to relay…</span>
|
|
376
|
+
</div>
|
|
377
|
+
|
|
378
|
+
<!-- QR / waiting for phone -->
|
|
379
|
+
<div id="remote-qr" class="hidden flex flex-col items-center w-full">
|
|
380
|
+
<div id="remote-url-box" class="hidden"></div>
|
|
381
|
+
<div class="py-6 px-6 flex flex-col items-center w-full">
|
|
382
|
+
<p class="text-sm font-medium text-slate-300 mb-4">Scan with your phone</p>
|
|
383
|
+
<img id="remote-qr-img" class="w-[180px] h-[180px] rounded-2xl hidden" alt="QR code" style="padding:12px;background:white">
|
|
384
|
+
<p class="mt-4 text-xs text-slate-400">or <button id="remote-copy" class="font-semibold underline underline-offset-2 text-slate-300 hover:text-slate-200 transition-colors">copy the link</button> to open on your device</p>
|
|
385
|
+
<div class="mt-5 w-full px-4 py-3 rounded-lg text-center" style="background:color-mix(in srgb, var(--color-surface) 70%, transparent)">
|
|
386
|
+
<div class="flex items-center justify-center gap-1.5">
|
|
387
|
+
<span id="remote-plan-badge" class="text-[9px] font-semibold uppercase tracking-wider px-1.5 py-px rounded-full bg-slate-700 text-slate-400">Free</span>
|
|
388
|
+
<span class="text-[11px] text-slate-500">· 1 active session</span>
|
|
389
|
+
</div>
|
|
390
|
+
<p class="mt-1 text-[11px] text-slate-500"><a id="remote-upgrade" href="https://clideck.dev/pro" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">Upgrade to Pro</a> for unlimited sessions</p>
|
|
391
|
+
</div>
|
|
392
|
+
<button id="remote-disconnect" class="mt-3 w-full py-2 text-[11px] text-slate-500 hover:text-slate-400 transition-colors">Disconnect</button>
|
|
393
|
+
</div>
|
|
394
|
+
<div class="w-full border-t border-slate-700/40 px-6 py-3 flex items-center justify-center gap-1.5 text-slate-400">
|
|
395
|
+
<svg class="w-3 h-3 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
|
396
|
+
<span class="text-[11px] font-medium">Your sessions are end-to-end encrypted</span>
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
|
|
400
|
+
<!-- Paired / active -->
|
|
401
|
+
<div id="remote-active" class="hidden flex flex-col items-center w-full">
|
|
402
|
+
<div class="py-6 px-6 flex flex-col items-center w-full gap-4">
|
|
403
|
+
<div class="flex items-center gap-2 text-emerald-400">
|
|
404
|
+
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path d="M5 13l4 4L19 7"/></svg>
|
|
405
|
+
<span class="text-[13px] font-medium">Connected</span>
|
|
406
|
+
</div>
|
|
407
|
+
<p id="remote-device-info" class="text-[11px] text-slate-500 -mt-2"></p>
|
|
408
|
+
|
|
409
|
+
<div id="remote-stats" class="w-full grid grid-cols-2 gap-2 text-center">
|
|
410
|
+
<div class="px-3 py-2.5 rounded-lg" style="background:color-mix(in srgb, var(--color-surface) 70%, transparent)">
|
|
411
|
+
<div id="remote-stat-time" class="text-sm font-mono text-slate-200">0:00</div>
|
|
412
|
+
<div class="text-[10px] text-slate-500 uppercase tracking-wider mt-0.5">Connected</div>
|
|
413
|
+
</div>
|
|
414
|
+
<div class="px-3 py-2.5 rounded-lg" style="background:color-mix(in srgb, var(--color-surface) 70%, transparent)">
|
|
415
|
+
<div id="remote-stat-sessions" class="text-sm font-mono text-slate-200">0</div>
|
|
416
|
+
<div class="text-[10px] text-slate-500 uppercase tracking-wider mt-0.5">Sessions</div>
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
|
|
420
|
+
<button id="remote-disconnect2" class="w-full py-2.5 text-[11px] font-medium text-red-400 hover:text-red-300 rounded-lg transition-colors" style="background:color-mix(in srgb, var(--color-surface) 70%, transparent)">Disconnect</button>
|
|
421
|
+
</div>
|
|
422
|
+
<div class="w-full border-t border-slate-700/40 px-6 py-3 flex items-center justify-between">
|
|
423
|
+
<div class="flex items-center gap-1.5 text-emerald-500/70">
|
|
424
|
+
<svg class="w-3 h-3 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
|
425
|
+
<span class="text-[11px]">End-to-end encrypted</span>
|
|
426
|
+
</div>
|
|
427
|
+
<div id="remote-plan-active" class="flex items-center gap-1.5">
|
|
428
|
+
<span id="remote-plan-badge2" class="text-[9px] font-semibold uppercase tracking-wider px-1.5 py-px rounded-full bg-slate-700 text-slate-400">Free</span>
|
|
429
|
+
<a id="remote-upgrade2" href="https://clideck.dev/pro" target="_blank" class="text-[11px] text-blue-400 hover:text-blue-300 transition-colors">Upgrade</a>
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
</div>
|
|
433
|
+
|
|
434
|
+
<!-- Error -->
|
|
435
|
+
<div id="remote-error" class="hidden px-5 py-4 flex flex-col items-center gap-3">
|
|
436
|
+
<p id="remote-error-text" class="text-xs text-red-400 text-center"></p>
|
|
437
|
+
<button id="remote-error-dismiss" class="px-4 py-2 text-xs text-slate-400 hover:text-slate-300 transition-colors">Close</button>
|
|
438
|
+
</div>
|
|
439
|
+
</div>
|
|
440
|
+
</div>
|
|
441
|
+
|
|
341
442
|
<!-- Confirmation dialog -->
|
|
342
443
|
<div id="confirm-close" class="absolute inset-0 z-[250] bg-black/60 backdrop-blur-sm hidden items-center justify-center">
|
|
343
444
|
<div class="bg-slate-800 border border-slate-600 rounded-xl shadow-2xl shadow-black/50 w-80 flex flex-col">
|