wisp-agents 0.21.0 → 0.22.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 +24 -1
- package/README.md +13 -8
- package/bin/wisp.js +10 -3
- package/package.json +4 -3
- package/server/adapters/claude.js +8 -4
- package/server/adapters/codex.js +9 -6
- package/server/adapters/cursor.js +4 -1
- package/server/adapters/grok.js +2 -1
- package/server/adapters/index.js +7 -2
- package/server/adapters/opencode.js +5 -2
- package/server/btw.js +76 -0
- package/server/client.js +19 -0
- package/server/git.js +80 -4
- package/server/handoff.js +13 -7
- package/server/index.js +4 -0
- package/server/machines.js +15 -4
- package/server/openapi.js +27 -2
- package/server/remote.js +4 -2
- package/server/routes/machines.js +1 -1
- package/server/routes/proxy.js +6 -0
- package/server/routes/sessions.js +32 -2
- package/server/routes/settings.js +24 -38
- package/server/routes/uploads.js +2 -0
- package/server/runner.js +154 -8
- package/server/services/launch.js +32 -4
- package/server/services/move.js +130 -0
- package/server/services/phone.js +20 -1
- package/server/services/places.js +150 -0
- package/server/services/uploads.js +23 -4
- package/server/store.js +39 -6
- package/skills/wisp/SKILL.md +1 -0
- package/web/css/blocks.css +2 -0
- package/web/css/composer.css +23 -1
- package/web/css/feed.css +49 -1
- package/web/css/machines.css +4 -0
- package/web/css/modals.css +19 -0
- package/web/css/ship.css +2 -0
- package/web/css/transcript.css +115 -0
- package/web/index.html +2 -0
- package/web/js/core/state.js +3 -2
- package/web/js/features/actions.js +42 -10
- package/web/js/features/btw.js +145 -0
- package/web/js/features/clones.js +7 -6
- package/web/js/features/commands.js +3 -1
- package/web/js/features/composer.js +162 -18
- package/web/js/features/feed.js +1 -1
- package/web/js/features/keys.js +2 -1
- package/web/js/features/launcher.js +104 -94
- package/web/js/features/place-menu.js +44 -0
- package/web/js/features/places.js +15 -5
- package/web/js/features/projects.js +14 -12
- package/web/js/features/settings.js +24 -3
- package/web/js/features/slash.js +63 -0
- package/web/js/features/transcript.js +290 -0
- package/web/js/features/turns.js +99 -28
- package/web/js/slash-text.js +22 -0
- package/web/js/ui/kbd.js +1 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// Places: this computer and the machines. A project is one repo that every place can run, so a
|
|
2
|
+
// task launched on a machine that has no checkout of it yet gets one first: a clone of the same
|
|
3
|
+
// repo that's already there (registered or in the usual folders), else a fresh clone of its
|
|
4
|
+
// origin. The checkout is remembered as the project's copy on that machine (store.setCopy).
|
|
5
|
+
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { cloneRepo, git, repoOf, sameRepo } from '../git.js';
|
|
10
|
+
import { HERE } from '../store.js';
|
|
11
|
+
import { bad, expand } from '../util.js';
|
|
12
|
+
|
|
13
|
+
const CLONE_TIMEOUT_MS = 10 * 60_000;
|
|
14
|
+
|
|
15
|
+
// Where people keep their repos, for one-click "add project".
|
|
16
|
+
export const REPO_ROOTS = ['code', 'Code', 'dev', 'Developer', 'projects', 'Projects', 'src', 'repos', 'work', 'git', 'GitHub', 'Documents', 'Downloads', 'Desktop'];
|
|
17
|
+
|
|
18
|
+
// Where a pasted repo URL is cloned: defaults.cloneDir, else the first usual repo folder that
|
|
19
|
+
// exists (not Documents/Downloads/Desktop), else ~/code.
|
|
20
|
+
export function cloneRoot(config) {
|
|
21
|
+
if (config.defaults?.cloneDir) return expand(config.defaults.cloneDir);
|
|
22
|
+
const dir = REPO_ROOTS.slice(0, -3)
|
|
23
|
+
.map((r) => path.join(os.homedir(), r))
|
|
24
|
+
.find((d) => fs.existsSync(d) && fs.statSync(d).isDirectory());
|
|
25
|
+
return dir || path.join(os.homedir(), 'code');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Git repos up to two levels under the usual folders, newest first, skipping known ones.
|
|
29
|
+
export function discoverRepos(known = new Set()) {
|
|
30
|
+
const found = [];
|
|
31
|
+
const walk = (dir, depth) => {
|
|
32
|
+
if (found.length >= 60) return;
|
|
33
|
+
let entries;
|
|
34
|
+
try {
|
|
35
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
36
|
+
} catch {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (entries.some((e) => e.name === '.git')) {
|
|
40
|
+
if (!known.has(dir)) found.push({ path: dir, name: path.basename(dir), mtime: fs.statSync(dir).mtimeMs });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (depth <= 0) return;
|
|
44
|
+
for (const e of entries) if (e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules') walk(path.join(dir, e.name), depth - 1);
|
|
45
|
+
};
|
|
46
|
+
for (const r of REPO_ROOTS) walk(path.join(os.homedir(), r), 2);
|
|
47
|
+
return found.sort((a, b) => b.mtime - a.mtime).slice(0, 24);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const originAt = (dir) => git(dir, ['remote', 'get-url', 'origin'], { timeout: 5000 }).catch(() => '');
|
|
51
|
+
|
|
52
|
+
// A checkout of `url` on this computer: a project (or copy) here that is that repo, else one in
|
|
53
|
+
// the usual folders, else a fresh clone into the clone folder. Resolves { dir, existed }.
|
|
54
|
+
export async function checkout(store, url) {
|
|
55
|
+
const repo = repoOf(url);
|
|
56
|
+
if (!repo) throw bad(`Not a repo URL: ${url}`);
|
|
57
|
+
const mine = store.config.projects.map((p) => [p, store.pathAt(p)]).filter(([, dir]) => dir && fs.existsSync(dir));
|
|
58
|
+
for (const [p, dir] of mine) if (sameRepo(p.repo || (await originAt(dir)), url)) return { dir, existed: true };
|
|
59
|
+
const found = discoverRepos();
|
|
60
|
+
const origins = await Promise.all(found.map((f) => originAt(f.path)));
|
|
61
|
+
const hit = found.find((f, i) => sameRepo(origins[i], url));
|
|
62
|
+
if (hit) return { dir: hit.path, existed: true };
|
|
63
|
+
return cloneRepo(repo, cloneRoot(store.config), { timeout: CLONE_TIMEOUT_MS });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function createPlaces(app) {
|
|
67
|
+
const { store, machines } = app;
|
|
68
|
+
const placeName = (mid) => (mid ? machines.get(mid).name : 'this computer');
|
|
69
|
+
const inflight = new Map(); // `${project}@${place}` -> promise, so two launches clone once
|
|
70
|
+
|
|
71
|
+
// The project's origin URL, asked of whichever place has a checkout; kept on the project.
|
|
72
|
+
async function originOf(p) {
|
|
73
|
+
if (p.repo) return p.repo;
|
|
74
|
+
const places = [p.machine || null, ...Object.keys(p.copies || {}).map((k) => (k === HERE ? null : k))];
|
|
75
|
+
for (const mid of places) {
|
|
76
|
+
const dir = store.pathAt(p, mid);
|
|
77
|
+
let url = '';
|
|
78
|
+
if (!mid) url = fs.existsSync(dir) ? await originAt(dir) : '';
|
|
79
|
+
else if (machines.isOnline(mid)) url = (await machines.json(mid, 'GET', `/projects/origin?path=${encodeURIComponent(dir)}`).catch(() => null))?.origin || '';
|
|
80
|
+
if (url) {
|
|
81
|
+
p.repo = url;
|
|
82
|
+
store.saveConfig();
|
|
83
|
+
return url;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return '';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function remoteCheckout(mid, repo, name) {
|
|
90
|
+
try {
|
|
91
|
+
const r = await machines.json(mid, 'POST', '/projects/checkout', { repo, name }, { timeout: CLONE_TIMEOUT_MS });
|
|
92
|
+
return { dir: r.path, existed: !!r.existed };
|
|
93
|
+
} catch (err) {
|
|
94
|
+
if (err.status !== 404) throw err;
|
|
95
|
+
}
|
|
96
|
+
// An older wisp there: its POST /projects clones the repo (or reuses a clone of it).
|
|
97
|
+
const r = await machines.json(mid, 'POST', '/projects', { repo }, { timeout: CLONE_TIMEOUT_MS });
|
|
98
|
+
return { dir: r.path, existed: !!r.cloned?.existed };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// The project's folder on `mid` (null = this computer). fresh: the one wisp knew is gone.
|
|
102
|
+
async function ensureAt(p, mid = null, { fresh = false } = {}) {
|
|
103
|
+
const known = store.pathAt(p, mid);
|
|
104
|
+
if (known && !fresh && (mid || fs.existsSync(known))) return known;
|
|
105
|
+
const key = `${p.id}@${mid || HERE}`;
|
|
106
|
+
if (!inflight.has(key)) {
|
|
107
|
+
const job = (async () => {
|
|
108
|
+
const repo = await originOf(p);
|
|
109
|
+
if (!repo) throw bad(`${p.name} has no git remote (origin) to clone from, so wisp can’t set it up on ${placeName(mid)}. Push it to a remote first, or add its folder there in Projects.`, 409);
|
|
110
|
+
const out = mid ? await remoteCheckout(mid, repo, p.name) : await checkout(store, repo);
|
|
111
|
+
store.setCopy(p, mid, out.dir);
|
|
112
|
+
app.configChanged();
|
|
113
|
+
return out.dir;
|
|
114
|
+
})().finally(() => inflight.delete(key));
|
|
115
|
+
inflight.set(key, job);
|
|
116
|
+
}
|
|
117
|
+
return inflight.get(key);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// A task's folder, for running it on `mid`: the same folder in that place's checkout of its
|
|
121
|
+
// project. Folders that are no project's (and ones already on that place) stay as they are.
|
|
122
|
+
async function dirAt(dir, mid = null) {
|
|
123
|
+
if (!dir) return dir;
|
|
124
|
+
if (!mid) dir = expand(dir);
|
|
125
|
+
if (store.projectFor(dir, mid)) return dir;
|
|
126
|
+
for (const from of [null, ...machines.list.map((m) => m.id)]) {
|
|
127
|
+
if (from === (mid || null)) continue;
|
|
128
|
+
const p = store.projectFor(dir, from);
|
|
129
|
+
if (!p) continue;
|
|
130
|
+
// A folder that's here too (the same layout on both computers) is used as it is, unless the
|
|
131
|
+
// project already has its own checkout here.
|
|
132
|
+
if (!mid && fs.existsSync(dir) && !store.pathAt(p)) return dir;
|
|
133
|
+
const rest = dir.slice(store.pathAt(p, from).length).split(/[\\/]/).filter(Boolean);
|
|
134
|
+
const there = await ensureAt(p, mid);
|
|
135
|
+
return rest.length ? (mid ? path.posix : path).join(there, ...rest) : there;
|
|
136
|
+
}
|
|
137
|
+
return dir;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// A launch body with its folders (cwd, also) moved to `mid`.
|
|
141
|
+
async function placeBody(body, mid = null) {
|
|
142
|
+
if (body.scratch === true) return body;
|
|
143
|
+
const out = { ...body };
|
|
144
|
+
if (body.cwd) out.cwd = await dirAt(String(body.cwd), mid);
|
|
145
|
+
if (Array.isArray(body.also)) out.also = await Promise.all(body.also.map((x) => (typeof x === 'string' ? dirAt(x, mid) : x?.cwd ? dirAt(String(x.cwd), mid) : x)));
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { originOf, ensureAt, dirAt, placeBody, placeName };
|
|
150
|
+
}
|
|
@@ -31,6 +31,28 @@ export class Uploads {
|
|
|
31
31
|
return Array.isArray(ids) ? ids.map((i) => this.byId.get(i)).filter(Boolean) : [];
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// A file to fill in here (a moved task's bundle): its path, and add() once it's written.
|
|
35
|
+
reserve(name) {
|
|
36
|
+
const id = newId();
|
|
37
|
+
const file = path.join(this.dir, `${id}-${name}`);
|
|
38
|
+
return { id, file, add: () => this.#record({ id, name, size: fs.statSync(file).size, mime: 'application/octet-stream', path: file }) };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
remove(id) {
|
|
42
|
+
const u = this.byId.get(id);
|
|
43
|
+
if (!u) return false;
|
|
44
|
+
this.byId.delete(id);
|
|
45
|
+
fs.rmSync(u.path, { force: true });
|
|
46
|
+
fs.rmSync(path.join(this.dir, `${id}.json`), { force: true });
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#record(meta) {
|
|
51
|
+
this.byId.set(meta.id, meta);
|
|
52
|
+
fs.writeFileSync(path.join(this.dir, `${meta.id}.json`), JSON.stringify(meta));
|
|
53
|
+
return meta;
|
|
54
|
+
}
|
|
55
|
+
|
|
34
56
|
async receive(req, { name, type }) {
|
|
35
57
|
name = path.basename(name || 'file').replace(/[^\w.\- ]+/g, '_').slice(-120) || 'file';
|
|
36
58
|
const id = newId();
|
|
@@ -51,9 +73,6 @@ export class Uploads {
|
|
|
51
73
|
out.on('finish', resolve);
|
|
52
74
|
out.on('error', reject);
|
|
53
75
|
});
|
|
54
|
-
|
|
55
|
-
this.byId.set(id, meta);
|
|
56
|
-
fs.writeFileSync(path.join(this.dir, `${id}.json`), JSON.stringify(meta));
|
|
57
|
-
return meta;
|
|
76
|
+
return this.#record({ id, name, size, mime: type || MIME[path.extname(name).toLowerCase()] || 'application/octet-stream', path: file });
|
|
58
77
|
}
|
|
59
78
|
}
|
package/server/store.js
CHANGED
|
@@ -4,11 +4,12 @@ import fs from 'node:fs';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import { sameRepo } from './git.js';
|
|
7
8
|
import { parsePresentation } from './protocol.js';
|
|
8
9
|
import { writeJsonAtomic } from './util.js';
|
|
9
10
|
|
|
10
11
|
const DEFAULT_CONFIG = {
|
|
11
|
-
projects: [], // [{ id, name, path, color, addedAt, machine? }]
|
|
12
|
+
projects: [], // [{ id, name, path, color, addedAt, machine?, repo?, copies? }]: see projectFor()
|
|
12
13
|
defaults: { agent: 'auto', mode: 'auto', model: '', effort: '' }, // agent auto: wisp picks harness and model (server/router.js)
|
|
13
14
|
recipes: [], // saved prompts with {params}: server/recipes.js
|
|
14
15
|
triggers: [], // schedules and GitHub watchers: server/triggers.js
|
|
@@ -20,6 +21,9 @@ const DEFAULT_CONFIG = {
|
|
|
20
21
|
const IMAGE_EXT = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg']);
|
|
21
22
|
const MAX_KEPT_IMAGE = 25 * 1024 * 1024;
|
|
22
23
|
|
|
24
|
+
// The key for this computer in a project's copies (machine ids are 6 hex characters).
|
|
25
|
+
export const HERE = 'here';
|
|
26
|
+
|
|
23
27
|
export const PROJECT_COLORS = ['violet', 'teal', 'amber', 'rose', 'sky', 'lime', 'orange', 'indigo'];
|
|
24
28
|
|
|
25
29
|
export class Store {
|
|
@@ -85,26 +89,47 @@ export class Store {
|
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
// --- projects: plain folders, matched to sessions by path prefix ---
|
|
92
|
+
//
|
|
93
|
+
// A project is one repo, and every place can run it. path (on machine, null = this computer) is
|
|
94
|
+
// where it was added; copies are its checkouts elsewhere, which wisp found or cloned there the
|
|
95
|
+
// first time a task ran there: { [machine id, or HERE]: path }. repo: its origin URL, once known.
|
|
96
|
+
|
|
97
|
+
// The project's folder on `machine` (null = this computer), or null when it has none there yet.
|
|
98
|
+
pathAt(p, machine = null) {
|
|
99
|
+
return (p.machine || null) === (machine || null) ? p.path : p.copies?.[machine || HERE] || null;
|
|
100
|
+
}
|
|
88
101
|
|
|
89
|
-
// machine: a
|
|
102
|
+
// machine: a folder on another computer (paths there are POSIX); null = this one.
|
|
90
103
|
projectFor(cwd = '', machine = null) {
|
|
91
104
|
if (!machine && this.isScratch(cwd)) return null; // never inside a project, even one at ~
|
|
92
105
|
const sep = machine ? '/' : path.sep;
|
|
93
106
|
let best = null;
|
|
94
|
-
|
|
95
|
-
|
|
107
|
+
let len = 0;
|
|
108
|
+
for (const p of this.config.projects) {
|
|
109
|
+
const dir = this.pathAt(p, machine);
|
|
110
|
+
if (dir && (cwd === dir || cwd.startsWith(dir + sep)) && dir.length > len) [best, len] = [p, dir.length];
|
|
111
|
+
}
|
|
96
112
|
return best;
|
|
97
113
|
}
|
|
98
114
|
|
|
99
|
-
|
|
100
|
-
|
|
115
|
+
// repo: the folder's origin URL. A folder of a repo that's already a project becomes that
|
|
116
|
+
// project's copy on this place, so the same repo is one project wherever it's checked out.
|
|
117
|
+
addProject(dir, { name, color, machine, repo } = {}) {
|
|
118
|
+
const existing = this.config.projects.find((p) => this.pathAt(p, machine) === dir);
|
|
101
119
|
if (existing) return existing;
|
|
120
|
+
const same = repo && this.config.projects.find((p) => p.repo && sameRepo(p.repo, repo) && !this.pathAt(p, machine));
|
|
121
|
+
if (same) {
|
|
122
|
+
(same.copies ||= {})[machine || HERE] = dir;
|
|
123
|
+
this.saveConfig();
|
|
124
|
+
return same;
|
|
125
|
+
}
|
|
102
126
|
const used = new Set(this.config.projects.map((p) => p.color));
|
|
103
127
|
const project = {
|
|
104
128
|
id: Math.random().toString(36).slice(2, 10),
|
|
105
129
|
name: name || (machine ? path.posix.basename(dir) : path.basename(dir)) || dir,
|
|
106
130
|
path: dir,
|
|
107
131
|
...(machine && { machine }),
|
|
132
|
+
...(repo && { repo }),
|
|
108
133
|
color: color || PROJECT_COLORS.find((c) => !used.has(c)) || PROJECT_COLORS[this.config.projects.length % PROJECT_COLORS.length],
|
|
109
134
|
addedAt: Date.now(),
|
|
110
135
|
};
|
|
@@ -113,6 +138,14 @@ export class Store {
|
|
|
113
138
|
return project;
|
|
114
139
|
}
|
|
115
140
|
|
|
141
|
+
// A checkout of the project on `machine`, found or cloned there (services/places.js).
|
|
142
|
+
setCopy(p, machine, dir) {
|
|
143
|
+
if (this.pathAt(p, machine) === dir) return;
|
|
144
|
+
if ((p.machine || null) === (machine || null)) p.path = dir;
|
|
145
|
+
else (p.copies ||= {})[machine || HERE] = dir;
|
|
146
|
+
this.saveConfig();
|
|
147
|
+
}
|
|
148
|
+
|
|
116
149
|
// Folder-less tasks run in a throwaway directory of their own under ~/.wisp/scratch.
|
|
117
150
|
scratchDir(id) {
|
|
118
151
|
const dir = path.join(this.home, 'scratch', id);
|
package/skills/wisp/SKILL.md
CHANGED
|
@@ -37,6 +37,7 @@ wisp wait <id> # block until it finishes or asks a question; prints t
|
|
|
37
37
|
wisp show <id> # current state + latest presentation as markdown
|
|
38
38
|
wisp ls --state running,waiting
|
|
39
39
|
wisp reply <id> "also cover the empty-list case" # follow-up; answers a pending question too
|
|
40
|
+
wisp btw <id> "did you touch the migrations?" # side question: answered without interrupting the run
|
|
40
41
|
wisp stop <id>
|
|
41
42
|
```
|
|
42
43
|
|
package/web/css/blocks.css
CHANGED
|
@@ -412,6 +412,8 @@ pre.code { margin: 0 0 12px; padding: 12px 14px; border-radius: 10px; background
|
|
|
412
412
|
.tests-rerun-out { margin-top: 12px; padding: 10px 12px; border-radius: 10px; background: var(--term-bg); color: var(--term-ink); }
|
|
413
413
|
.tests-rerun-out pre { margin: 0; font: 12px/1.55 var(--mono); white-space: pre-wrap; word-break: break-word; max-height: 260px; overflow-y: auto; color: #b4b4b0; }
|
|
414
414
|
.rev-tag { display: inline-flex; align-items: center; padding: 1px 6px; border-radius: 5px; font: 500 11px var(--sans); background: var(--bg-2); color: var(--muted); border: 1px solid var(--line); }
|
|
415
|
+
.t-moved { display: inline-flex; align-items: center; gap: 3px; font-size: 11px; color: var(--faint); }
|
|
416
|
+
.t-moved .i { width: 11px; height: 11px; }
|
|
415
417
|
.cp-sha { font: 500 11px var(--mono); color: var(--faint); padding: 1px 5px; border-radius: 5px; background: var(--surface); border: 1px solid var(--line); }
|
|
416
418
|
.b-diff.is-reverted .diff-scroll { opacity: 0.55; }
|
|
417
419
|
.b-diff.is-reverted .path .base, .b-files li.is-reverted .path .base { text-decoration: line-through; color: var(--muted); }
|
package/web/css/composer.css
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* ---------- per-item composer ---------- */
|
|
2
2
|
.composer {
|
|
3
|
-
padding: 6px 8px 8px 12px; border-radius: 10px; background: var(--surface-2); border: 1px solid var(--line);
|
|
3
|
+
position: relative; padding: 6px 8px 8px 12px; border-radius: 10px; background: var(--surface-2); border: 1px solid var(--line);
|
|
4
4
|
transition: box-shadow 0.15s, border-color 0.15s;
|
|
5
5
|
}
|
|
6
6
|
.composer:focus-within { background: var(--surface); border-color: color-mix(in srgb, var(--accent) 55%, var(--line-2)); box-shadow: 0 0 0 3px var(--accent-soft); }
|
|
@@ -32,6 +32,22 @@
|
|
|
32
32
|
.c-queue.on { color: var(--ink); background: var(--bg-2); }
|
|
33
33
|
.c-done:hover { color: var(--good); }
|
|
34
34
|
|
|
35
|
+
/* "/btw …" in the box: a side question. The pickers are for the next follow-up, so they step back. */
|
|
36
|
+
.composer.is-btw .c-mode, .composer.is-btw .c-runner, .composer.is-btw .c-place, .composer.is-btw .c-queue { opacity: 0.35; }
|
|
37
|
+
.c-side { font-weight: 600; color: var(--accent-ink); }
|
|
38
|
+
|
|
39
|
+
/* the / menu (features/slash.js): opens above the box, like the box's own popovers */
|
|
40
|
+
.slash-menu {
|
|
41
|
+
position: absolute; left: 8px; bottom: calc(100% + 6px); z-index: 30; min-width: 280px; max-width: calc(100% - 16px); padding: 5px;
|
|
42
|
+
border-radius: 12px; background: var(--surface); border: 1px solid var(--line); box-shadow: var(--shadow-2); animation: pop 0.18s var(--ease);
|
|
43
|
+
}
|
|
44
|
+
.slash-item { display: flex; align-items: baseline; gap: 8px; width: 100%; padding: 7px 10px; border-radius: 8px; text-align: left; font-size: 13px; color: var(--ink-2); }
|
|
45
|
+
.slash-item b { font: 600 12.5px var(--mono); color: var(--ink); }
|
|
46
|
+
.slash-args { font: 12px var(--mono); color: var(--faint); }
|
|
47
|
+
.slash-hint { margin-left: auto; padding-left: 12px; font-size: 12px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
48
|
+
.slash-item.on, .slash-item:hover { background: var(--bg-2); }
|
|
49
|
+
.slash-item.on b { color: var(--accent-ink); }
|
|
50
|
+
|
|
35
51
|
/* attachments tray */
|
|
36
52
|
.tray { display: flex; flex-wrap: wrap; gap: 8px; padding: 4px 0 8px; }
|
|
37
53
|
.att {
|
|
@@ -59,6 +75,12 @@
|
|
|
59
75
|
.c-runner.changed { padding-right: 8px; }
|
|
60
76
|
.c-runner.changed .rb-logo { margin-right: 4px; }
|
|
61
77
|
.c-runner:focus-visible { outline: 0; box-shadow: 0 0 0 2px var(--accent-soft), 0 0 0 1px var(--accent); }
|
|
78
|
+
/* where the next follow-up runs: a quiet icon, the computer's name once it's switched */
|
|
79
|
+
.c-place { display: inline-flex; align-items: center; gap: 5px; height: 30px; min-width: 30px; padding: 0 7px; border-radius: 8px; font-size: 12px; color: var(--muted); transition: background 0.15s, color 0.15s; }
|
|
80
|
+
.c-place .i { width: 15px; height: 15px; }
|
|
81
|
+
.c-place:hover, .c-place[aria-expanded='true'], .c-place.changed { background: var(--bg-2); color: var(--ink); }
|
|
82
|
+
.c-place.changed { padding-right: 9px; color: var(--accent); }
|
|
83
|
+
.c-place:focus-visible { outline: 0; box-shadow: 0 0 0 2px var(--accent-soft), 0 0 0 1px var(--accent); }
|
|
62
84
|
.cr-label { max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
|
|
63
85
|
|
|
64
86
|
/* ---------- task mentions (features/mentions.js) ---------- */
|
package/web/css/feed.css
CHANGED
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
.toggle-done { cursor: pointer; user-select: none; }
|
|
27
27
|
.toggle-done:hover { color: var(--ink-2); }
|
|
28
28
|
.toggle-done .chev { width: 12px; height: 12px; transition: transform 0.2s; }
|
|
29
|
-
|
|
29
|
+
/* Only the group's own heading: the cards inside have chevrons of their own. */
|
|
30
|
+
.f-group.done:not(.collapsed) > .toggle-done .chev { transform: rotate(90deg); }
|
|
30
31
|
/* Folded groups are just a row of headings: stack them tight instead of leaving a section gap each. */
|
|
31
32
|
.f-group.collapsed { margin-bottom: 2px; }
|
|
32
33
|
.f-group.collapsed h4 { margin: 0; }
|
|
@@ -184,8 +185,55 @@
|
|
|
184
185
|
.log { max-height: 340px; overflow-y: auto; margin-top: 12px; padding: 10px 12px; border-radius: 10px; background: var(--surface-2); border: 1px solid var(--line); }
|
|
185
186
|
.w-actions { display: flex; align-items: center; gap: 12px; margin-top: 12px; }
|
|
186
187
|
.w-queued { font-size: 12px; color: var(--accent-ink); font-weight: 500; }
|
|
188
|
+
/* the /btw tip (features/btw.js): until it's been used once */
|
|
189
|
+
.w-tip { display: inline-flex; align-items: center; gap: 2px; min-width: 0; animation: fade-in 0.4s 0.6s var(--ease) both; }
|
|
190
|
+
.w-tip-go { display: inline-flex; align-items: center; gap: 6px; min-width: 0; font-size: 12px; color: var(--muted); }
|
|
191
|
+
.w-tip-go:hover { color: var(--ink); }
|
|
192
|
+
.w-tip-k { color: var(--faint); }
|
|
193
|
+
.w-tip-go code { flex: none; padding: 1px 5px; border-radius: 5px; background: var(--bg-2); font: 500 11.5px var(--mono); color: var(--ink-2); }
|
|
194
|
+
.w-tip-t { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
195
|
+
.w-tip-x { flex: none; display: grid; place-items: center; width: 20px; height: 20px; border-radius: 6px; color: var(--faint); opacity: 0; transition: opacity 0.15s; }
|
|
196
|
+
.w-tip-x .i { width: 11px; height: 11px; }
|
|
197
|
+
.w-tip:hover .w-tip-x, .w-tip-x:focus-visible { opacity: 1; }
|
|
198
|
+
.w-tip-x:hover { background: var(--bg-2); color: var(--ink); }
|
|
199
|
+
@media (hover: none) { .w-tip-x { opacity: 1; } }
|
|
200
|
+
.w-actions > .peek { flex: none; white-space: nowrap; }
|
|
201
|
+
/* No room beside Show all activity and Stop on a phone: the tip gets its own line under them. */
|
|
202
|
+
@media (max-width: 720px) {
|
|
203
|
+
.w-actions:has(.w-tip) { flex-wrap: wrap; row-gap: 8px; }
|
|
204
|
+
.w-tip { order: 1; flex-basis: 100%; }
|
|
205
|
+
}
|
|
187
206
|
@keyframes tick-in { from { opacity: 0; transform: translateY(-4px); } }
|
|
188
207
|
|
|
208
|
+
/* /btw side questions (features/btw.js): plain hairline cards on the turn they were asked during */
|
|
209
|
+
.btw-list { display: grid; gap: 8px; margin-top: 14px; }
|
|
210
|
+
.btw-list:empty { display: none; }
|
|
211
|
+
.btw { position: relative; padding: 10px 40px 11px 14px; border-radius: var(--radius); background: var(--surface); border: 1px solid var(--line); }
|
|
212
|
+
.btw.new { animation: rise 0.35s var(--ease) both; }
|
|
213
|
+
.btw.landed > :is(.btw-a, .btw-err) { animation: fade-in 0.4s var(--ease) both; }
|
|
214
|
+
.btw-q { display: flex; align-items: baseline; gap: 8px; font-size: 13.5px; line-height: 20px; color: var(--ink); }
|
|
215
|
+
.btw-tag {
|
|
216
|
+
flex: none; display: inline-flex; align-items: center; gap: 4px; padding: 1px 6px 1px 4px; border-radius: 5px; cursor: default;
|
|
217
|
+
font: 600 11px/18px var(--sans); color: var(--accent-ink); background: var(--accent-soft); align-self: flex-start; margin-top: 1px;
|
|
218
|
+
}
|
|
219
|
+
.btw-tag .i { width: 12px; height: 12px; }
|
|
220
|
+
.btw-text { min-width: 0; white-space: pre-wrap; overflow-wrap: anywhere; font-weight: 500; }
|
|
221
|
+
.btw-atts { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
|
|
222
|
+
.btw-a { margin-top: 6px; font-size: 13.5px; }
|
|
223
|
+
.btw-a p, .btw-a ul, .btw-a ol, .btw-a pre { margin-bottom: 8px; }
|
|
224
|
+
.btw-wait { display: flex; align-items: center; gap: 8px; margin-top: 7px; font-size: 12.5px; color: var(--muted); }
|
|
225
|
+
.btw-wait .elapsed { margin-left: auto; color: var(--faint); font-variant-numeric: tabular-nums; }
|
|
226
|
+
.btw-dots { display: inline-flex; gap: 3px; }
|
|
227
|
+
.btw-dots i { width: 5px; height: 5px; border-radius: 50%; background: var(--accent); animation: pulse 1.2s ease-in-out infinite; }
|
|
228
|
+
.btw-dots i:nth-child(2) { animation-delay: 0.2s; }
|
|
229
|
+
.btw-dots i:nth-child(3) { animation-delay: 0.4s; }
|
|
230
|
+
.btw-err { display: flex; align-items: baseline; gap: 6px; margin-top: 6px; font-size: 12.5px; color: var(--bad); }
|
|
231
|
+
.btw-err .i { flex: none; width: 13px; height: 13px; translate: 0 2px; }
|
|
232
|
+
.btw-x { position: absolute; top: 6px; right: 6px; width: 26px; height: 26px; border-radius: 7px; color: var(--faint); opacity: 0; transition: opacity 0.15s, background 0.15s, color 0.15s; }
|
|
233
|
+
.btw-x .i { width: 12px; height: 12px; }
|
|
234
|
+
.btw:hover .btw-x, .btw-x:focus-visible, .btw.st-running .btw-x { opacity: 1; }
|
|
235
|
+
@media (hover: none) { .btw-x { opacity: 1; } }
|
|
236
|
+
|
|
189
237
|
/* ask card (mid-run question) */
|
|
190
238
|
.ask-card, .b-question {
|
|
191
239
|
position: relative; padding: 16px 18px; border-radius: var(--radius); margin: 4px 0 14px;
|
package/web/css/machines.css
CHANGED
|
@@ -38,6 +38,10 @@
|
|
|
38
38
|
.wm-row:hover .wm-only { opacity: 1; }
|
|
39
39
|
.wm-only:hover { background: var(--bg-2); color: var(--ink); }
|
|
40
40
|
.wm-sep { height: 1px; margin: 5px 4px; background: var(--line); }
|
|
41
|
+
.wm-note { margin: 0 10px 6px; max-width: 300px; font-size: 12px; line-height: 1.4; color: var(--muted); }
|
|
42
|
+
.wm-note.warn { color: var(--warn); }
|
|
43
|
+
.place-menu .wm-check { border-radius: 50%; }
|
|
44
|
+
.place-menu .wm-row[disabled] { opacity: 0.45; cursor: default; }
|
|
41
45
|
.p-chip .p-where { width: 11px; height: 11px; color: var(--muted); }
|
|
42
46
|
.projects-modal .where { margin-bottom: 12px; }
|
|
43
47
|
.projects-modal .where .i { width: 13px; height: 13px; color: var(--muted); }
|
package/web/css/modals.css
CHANGED
|
@@ -22,6 +22,21 @@
|
|
|
22
22
|
.rr-files { display: flex; flex-wrap: wrap; gap: 6px; padding: 2px 2px 6px; }
|
|
23
23
|
.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); }
|
|
24
24
|
.rr-file .i { width: 12px; height: 12px; color: var(--muted); }
|
|
25
|
+
/* the bar never pushes Rerun out of the box: the harness name gives way first (the logo still says
|
|
26
|
+
which), then the model; on a narrow dialog the mode and place labels fold to their icons */
|
|
27
|
+
.rerun-modal { width: min(660px, 100%); }
|
|
28
|
+
.rr-box { container: rerun / inline-size; }
|
|
29
|
+
.rr-box .l-bar > .runner-btn { flex: 0 1 auto; min-width: 0; overflow: hidden; }
|
|
30
|
+
.rr-box .runner-btn :is(.rb-logo, .rb-e, .chev) { flex: none; }
|
|
31
|
+
.rr-box .runner-btn .rb-h { flex: 0 100 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
32
|
+
.rr-box .runner-btn .rb-m { flex: 0 1 auto; min-width: 0; }
|
|
33
|
+
.rr-box .where-btn span { max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
34
|
+
@container rerun (max-width: 520px) {
|
|
35
|
+
.rr-box .l-bar :is(.mode-btn, .where-btn) span, .rr-box .runner-btn .rb-h { display: none; }
|
|
36
|
+
.rr-box .runner-btn .rb-m { margin-left: 0; }
|
|
37
|
+
.rr-box .runner-btn .rb-m::before { display: none; }
|
|
38
|
+
}
|
|
39
|
+
@container rerun (max-width: 400px) { .rr-box .runner-btn :is(.rb-m, .chev) { display: none; } }
|
|
25
40
|
.overlay:not([hidden]) ~ :is(.picker, .menu) { z-index: 55; }
|
|
26
41
|
.add-row { display: flex; gap: 8px; }
|
|
27
42
|
.path-input { position: relative; flex: 1; }
|
|
@@ -47,6 +62,10 @@
|
|
|
47
62
|
.discover h4 .i { width: 14px; height: 14px; }
|
|
48
63
|
.discover h4 .muted { font-weight: 400; }
|
|
49
64
|
.prow.cloning .ppath { color: var(--muted); }
|
|
65
|
+
/* Where a project is checked out: one quiet tag per place. */
|
|
66
|
+
.ppath:has(.pplace) { display: flex; gap: 8px; font-family: inherit; }
|
|
67
|
+
.pplace { display: inline-flex; align-items: center; gap: 4px; }
|
|
68
|
+
.pplace .ic { width: 11px; height: 11px; }
|
|
50
69
|
.discover h4 .link { margin-left: auto; font-size: 12px; }
|
|
51
70
|
.prow.cloning .orb.live { margin: 0; }
|
|
52
71
|
.hint .clonedir { width: 220px; height: 24px; padding: 0 7px; margin: 0 4px; border-radius: 7px; border: 1px solid var(--line-2); background: var(--surface); color: var(--ink); font-size: 12px; outline: 0; }
|
package/web/css/ship.css
CHANGED
|
@@ -44,6 +44,8 @@
|
|
|
44
44
|
.it-meta .m.branch { font: 11px var(--mono); color: var(--ink-2); }
|
|
45
45
|
.it-meta .m.also { display: inline-flex; align-items: center; gap: 5px; color: var(--ink-2); }
|
|
46
46
|
.it-meta .m .i { width: 12px; height: 12px; flex: none; color: var(--faint); }
|
|
47
|
+
.it-meta .m.moved-to { color: var(--accent-ink); cursor: pointer; }
|
|
48
|
+
.it-meta .m.moved-to:hover { text-decoration: underline; }
|
|
47
49
|
.it-meta .m.m-plan, .it-meta .m.m-plan .i { color: var(--accent-ink); }
|
|
48
50
|
.it-meta .m.m-auto, .it-meta .m.m-auto .i { color: var(--warn); }
|
|
49
51
|
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/* ---------- full transcript (features/transcript.js) ----------
|
|
2
|
+
A timeline under the prompt: a hairline rail with each step's icon on it, the agent's messages as
|
|
3
|
+
prose, and the presentation after the rail ends. */
|
|
4
|
+
|
|
5
|
+
.ghost.tx-toggle.on { color: var(--accent-ink); background: var(--accent-soft); }
|
|
6
|
+
|
|
7
|
+
.tx { display: flex; flex-direction: column; }
|
|
8
|
+
.tx-head { display: flex; align-items: center; gap: 6px; min-height: 22px; margin: -6px 0 8px; font-size: 12px; color: var(--faint); white-space: nowrap; }
|
|
9
|
+
.tx-title { display: inline-flex; align-items: center; gap: 6px; font-weight: 500; color: var(--muted); }
|
|
10
|
+
.tx-count { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
11
|
+
.tx-title .i { width: 14px; height: 14px; }
|
|
12
|
+
.tx-title + .tx-count::before { content: '·'; margin-right: 6px; }
|
|
13
|
+
.tx-jump { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; }
|
|
14
|
+
.tx-jump .i { width: 12px; height: 12px; transform: rotate(90deg); }
|
|
15
|
+
|
|
16
|
+
.tx-list { position: relative; list-style: none; margin: 0 0 18px; padding: 0; }
|
|
17
|
+
.tx-list::before { content: ''; position: absolute; left: 11.5px; top: 12px; bottom: 15px; width: 1px; background: var(--line-2); }
|
|
18
|
+
/* Mid-run the rail fades out into the working panel: more is coming. */
|
|
19
|
+
.tx.live .tx-list { margin-bottom: 10px; }
|
|
20
|
+
.tx.live .tx-list::before { bottom: -10px; background: linear-gradient(var(--line-2) calc(100% - 28px), transparent); }
|
|
21
|
+
|
|
22
|
+
.tx-step, .tx-fold, .tx-end, .tx-msg { position: relative; padding-left: 28px; }
|
|
23
|
+
.tx-ic {
|
|
24
|
+
position: absolute; left: 2px; top: 5px; width: 20px; height: 20px; display: grid; place-items: center;
|
|
25
|
+
border-radius: 50%; background: var(--surface); color: var(--faint);
|
|
26
|
+
}
|
|
27
|
+
.tx-ic .i { width: 14px; height: 14px; }
|
|
28
|
+
|
|
29
|
+
/* a step: one line, opening to its command and output, its diff, or its error */
|
|
30
|
+
.tx-row {
|
|
31
|
+
display: flex; align-items: center; gap: 10px; width: 100%; min-width: 0; min-height: 30px; padding: 4px 8px;
|
|
32
|
+
border-radius: 7px; font-size: 13px; color: var(--ink-2); text-align: left; outline: 0;
|
|
33
|
+
}
|
|
34
|
+
.tx-row[role='button'] { cursor: pointer; transition: background 0.15s, color 0.15s; }
|
|
35
|
+
.tx-row[role='button']:hover, .tx-row[role='button']:focus-visible, .tx-step.open > .tx-row { background: var(--bg-2); color: var(--ink); }
|
|
36
|
+
.tx-row[role='button']:focus-visible { box-shadow: 0 0 0 2px var(--accent-soft); }
|
|
37
|
+
/* Both give way on a narrow row, the command three times faster. (Shrink factors that sum under 1
|
|
38
|
+
would leave part of the overflow in place.) */
|
|
39
|
+
.tx-label { flex: 0 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
40
|
+
.tx-cmd { flex: 0 3 auto; min-width: 0; font: 12px/1.4 var(--mono); color: var(--faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
41
|
+
.tx-row .nums { margin-left: auto; }
|
|
42
|
+
.tx-t { margin-left: auto; flex: none; font-size: 11px; color: var(--faint); font-variant-numeric: tabular-nums; opacity: 0; transition: opacity 0.15s; }
|
|
43
|
+
.tx-row .nums + .tx-t { margin-left: 0; }
|
|
44
|
+
.tx-step:hover .tx-t, .tx-msg:hover .tx-t, .tx-step.open .tx-t { opacity: 1; }
|
|
45
|
+
.tx-open {
|
|
46
|
+
flex: none; display: grid; place-items: center; width: 22px; height: 22px; margin: -2px -4px -2px 0; border-radius: 6px;
|
|
47
|
+
color: var(--faint); opacity: 0; transition: opacity 0.15s, background 0.15s, color 0.15s;
|
|
48
|
+
}
|
|
49
|
+
.tx-open .i { width: 13px; height: 13px; }
|
|
50
|
+
.tx-step:hover .tx-open, .tx-open:focus-visible { opacity: 1; }
|
|
51
|
+
.tx-open:hover { color: var(--ink); background: var(--surface); }
|
|
52
|
+
.tx-row .chev { flex: none; width: 12px; height: 12px; color: var(--faint); transition: transform 0.2s var(--ease); }
|
|
53
|
+
.tx-step.open > .tx-row .chev { transform: rotate(90deg); }
|
|
54
|
+
.tx-step.loading > .tx-row { opacity: 0.6; cursor: progress; }
|
|
55
|
+
|
|
56
|
+
.tx-step.k-error .tx-ic, .tx-step.k-error .tx-label { color: var(--bad); }
|
|
57
|
+
.tx-step:is(.k-ask, .k-wait) .tx-ic, .tx-step.k-ask .tx-label { color: var(--warn); }
|
|
58
|
+
.tx-step.k-answer .tx-ic { color: var(--good); }
|
|
59
|
+
.tx-step.by-wisp .tx-ic { color: var(--accent); }
|
|
60
|
+
.tx-step.by-wisp .tx-label { color: var(--muted); }
|
|
61
|
+
/* What you were asked and answered, and plan lines, read in full. */
|
|
62
|
+
.tx-step:is(.k-ask, .k-answer, .k-plan, .k-check, .k-background, .by-wisp) .tx-label { white-space: normal; }
|
|
63
|
+
|
|
64
|
+
.tx-more { margin: 2px 0 8px 8px; animation: rise 0.3s var(--ease); }
|
|
65
|
+
.tx-out {
|
|
66
|
+
margin: 0; padding: 10px 12px; max-height: 340px; overflow: auto; border-radius: 8px;
|
|
67
|
+
background: var(--surface-2); border: 1px solid var(--line); font: 12px/1.55 var(--mono); white-space: pre-wrap; word-break: break-word;
|
|
68
|
+
}
|
|
69
|
+
.tx-cmdline, .tx-res { display: block; }
|
|
70
|
+
.tx-cmdline { color: var(--ink); }
|
|
71
|
+
.tx-cmdline b { color: var(--faint); font-weight: 500; }
|
|
72
|
+
.tx-res { color: var(--muted); }
|
|
73
|
+
.tx-cmdline + .tx-res { margin-top: 8px; padding-top: 8px; border-top: 1px dashed var(--line-2); }
|
|
74
|
+
.tx-out.err { color: var(--bad); background: var(--del-bg); border-color: color-mix(in srgb, var(--bad) 22%, var(--line)); }
|
|
75
|
+
.tx-none { margin: 0; font-size: 12.5px; color: var(--muted); }
|
|
76
|
+
.tx-diff .presentation { gap: 8px; }
|
|
77
|
+
.tx-diff .b-diff.bare { border: 1px solid var(--line); border-radius: 8px; background: var(--surface); overflow: hidden; }
|
|
78
|
+
|
|
79
|
+
/* a message: the agent's own words, on a dot of the rail */
|
|
80
|
+
.tx-msg { margin: 8px 0 12px; padding-top: 2px; padding-right: 64px; }
|
|
81
|
+
.tx-dot { position: absolute; left: 8px; top: 9px; width: 8px; height: 8px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 0 4px var(--surface); }
|
|
82
|
+
.tx-msg .prose { padding-left: 8px; font-size: 14px; color: var(--ink); }
|
|
83
|
+
.tx-msg .tx-t { position: absolute; right: 4px; top: 4px; }
|
|
84
|
+
.tx-msg:first-child { margin-top: 0; }
|
|
85
|
+
|
|
86
|
+
/* a long run of steps folds its middle */
|
|
87
|
+
.tx-fold .tx-row { color: var(--muted); cursor: pointer; transition: background 0.15s, color 0.15s; }
|
|
88
|
+
.tx-fold .tx-row:hover { background: var(--bg-2); color: var(--ink); }
|
|
89
|
+
|
|
90
|
+
.tx-end { display: flex; align-items: center; min-height: 30px; padding-left: 36px; font-size: 12.5px; font-weight: 500; color: var(--muted); }
|
|
91
|
+
.tx-end .tx-ic { color: var(--good); }
|
|
92
|
+
.tx-end.st-error .tx-ic { color: var(--bad); }
|
|
93
|
+
.tx-end.st-stopped .tx-ic { color: var(--warn); }
|
|
94
|
+
|
|
95
|
+
.tx-step.new, .tx-msg.new { animation: tick-in 0.45s var(--ease); }
|
|
96
|
+
|
|
97
|
+
/* The working panel under a live transcript: status and Stop on one line, a question under them. */
|
|
98
|
+
.w-panel.tx-panel { padding: 10px 14px; gap: 12px; }
|
|
99
|
+
.tx-panel .w-orb { padding-top: 0; align-self: flex-start; }
|
|
100
|
+
.tx-panel .w-body { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; column-gap: 12px; }
|
|
101
|
+
.tx-panel .w-status { grid-column: 1; grid-row: 1; flex-wrap: wrap; row-gap: 2px; margin: 0; }
|
|
102
|
+
.tx-panel .w-status > * { white-space: nowrap; }
|
|
103
|
+
.tx-panel .w-actions { grid-column: 2; grid-row: 1; margin: 0; }
|
|
104
|
+
.tx-panel .w-body > :not(.w-status, .w-actions) { grid-column: 1 / -1; }
|
|
105
|
+
.tx-panel .ask-slot:not(:empty) { margin-top: 10px; }
|
|
106
|
+
|
|
107
|
+
@media (max-width: 720px) {
|
|
108
|
+
/* The /btw tip doesn't fit beside the status: the row with it and Stop goes under it. */
|
|
109
|
+
.tx-panel .w-body:has(.w-tip) { display: block; }
|
|
110
|
+
.tx-panel .w-body:has(.w-tip) .w-actions { margin-top: 8px; }
|
|
111
|
+
.tx-title { display: none; }
|
|
112
|
+
.tx-cmd { display: none; }
|
|
113
|
+
.tx-msg { padding-right: 8px; }
|
|
114
|
+
.tx-msg .tx-t { display: none; }
|
|
115
|
+
}
|
package/web/index.html
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
<link rel="stylesheet" href="/css/automations.css">
|
|
23
23
|
<link rel="stylesheet" href="/css/subagents.css">
|
|
24
24
|
<link rel="stylesheet" href="/css/auto.css">
|
|
25
|
+
<link rel="stylesheet" href="/css/transcript.css">
|
|
25
26
|
</head>
|
|
26
27
|
<body>
|
|
27
28
|
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
|
|
@@ -66,6 +67,7 @@
|
|
|
66
67
|
<symbol id="i-stats" viewBox="0 0 24 24"><path d="M5 19V11M12 19V5M19 19v-6" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></symbol>
|
|
67
68
|
<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>
|
|
68
69
|
<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>
|
|
70
|
+
<symbol id="i-transcript" viewBox="0 0 24 24"><path d="M6 7.5v9" stroke="currentColor" stroke-width="1.3" stroke-dasharray="1.6 1.8" stroke-linecap="round"/><circle cx="6" cy="5.5" r="1.9" fill="currentColor"/><circle cx="6" cy="12" r="1.9" fill="currentColor"/><circle cx="6" cy="18.5" r="1.9" fill="currentColor"/><path d="M11 5.5h9M11 12h6.5M11 18.5h8" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></symbol>
|
|
69
71
|
<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>
|
|
70
72
|
<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>
|
|
71
73
|
<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>
|
package/web/js/core/state.js
CHANGED
|
@@ -30,13 +30,14 @@ export const S = {
|
|
|
30
30
|
drafts: new Map(),
|
|
31
31
|
replyModes: new Map(), // session id → the mode its next follow-up switches to
|
|
32
32
|
replyRunners: new Map(), // session id → { agent, model, effort } its next follow-up switches to
|
|
33
|
+
replyPlaces: new Map(), // session id → the place ('' = this computer) its next follow-up moves it to
|
|
33
34
|
};
|
|
34
35
|
export const saveOpen = () => save('wisp.open', [...S.open]);
|
|
35
36
|
export const setAgents = (list) => ((S.agents = list), save('wisp.agents', list));
|
|
36
37
|
|
|
37
38
|
// Where the launcher sends a prompt: '' = this computer, else machine ids. Several run in parallel,
|
|
38
|
-
//
|
|
39
|
-
// work in more
|
|
39
|
+
// all in the project picked (launch.picks['']; any project runs on any place), and show up as one
|
|
40
|
+
// task. A run can work in more projects too (launch.also['']): one agent, several folders.
|
|
40
41
|
export const launch = {
|
|
41
42
|
project: load('wisp.launchProject', null), // the project picked in the first place
|
|
42
43
|
where: load('wisp.launchWhere', ['']),
|