clay-server 4.8.0-beta.1 → 4.8.0-beta.3
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/lib/bundled-skills/clay-agent-browser/SKILL.md +36 -0
- package/lib/clay-skill-install.js +33 -0
- package/lib/clay-skill-storage.js +49 -0
- package/lib/linear-api.js +62 -0
- package/lib/linear-store.js +52 -0
- package/lib/project-http.js +14 -5
- package/lib/project-linear-launch.js +70 -0
- package/lib/project-session-linear.js +197 -0
- package/lib/project-shared-browser.js +256 -0
- package/lib/project.js +74 -2
- package/lib/public/app.js +7 -0
- package/lib/public/css/filebrowser.css +2 -25
- package/lib/public/css/issues.css +1 -14
- package/lib/public/css/linear.css +35 -0
- package/lib/public/css/mcp-skills-workbench.css +51 -0
- package/lib/public/css/project-logs.css +0 -11
- package/lib/public/css/right-workbench.css +28 -0
- package/lib/public/css/scheduled-tasks.css +1 -1
- package/lib/public/css/settings-ui.css +295 -0
- package/lib/public/css/shared-browser-skins.css +28 -0
- package/lib/public/css/shared-browser.css +91 -0
- package/lib/public/css/sticky-notes.css +0 -11
- package/lib/public/index.html +50 -22
- package/lib/public/modules/app-messages.js +6 -0
- package/lib/public/modules/app-projects.js +1 -1
- package/lib/public/modules/extension-settings.js +110 -0
- package/lib/public/modules/issues.js +4 -1
- package/lib/public/modules/linear-panel.js +120 -0
- package/lib/public/modules/linear-settings.js +61 -0
- package/lib/public/modules/mcp-skills-workbench.js +67 -0
- package/lib/public/modules/mcp-ui.js +9 -12
- package/lib/public/modules/notifications.js +2 -107
- package/lib/public/modules/project-settings.js +19 -1
- package/lib/public/modules/session-github.js +15 -5
- package/lib/public/modules/shared-browser-activity.js +66 -0
- package/lib/public/modules/shared-browser-input.js +73 -0
- package/lib/public/modules/shared-browser-tabs.js +33 -0
- package/lib/public/modules/shared-browser-viewport-menu.js +76 -0
- package/lib/public/modules/shared-browser.js +214 -0
- package/lib/public/modules/sidebar-mobile.js +3 -3
- package/lib/public/modules/skills-actions.js +247 -0
- package/lib/public/modules/skills-detail.js +148 -0
- package/lib/public/modules/skills-state.js +10 -0
- package/lib/public/modules/skills.js +64 -459
- package/lib/public/modules/sticky-notes.js +23 -6
- package/lib/public/modules/tool-palette-order.js +11 -13
- package/lib/public/modules/user-settings.js +4 -0
- package/lib/public/style.css +8 -0
- package/lib/sdk-bridge.js +22 -1
- package/lib/server-linear.js +50 -0
- package/lib/server.js +5 -0
- package/lib/sessions.js +7 -0
- package/lib/shared-browser-activity-history.js +17 -0
- package/lib/shared-browser-policy.js +26 -0
- package/lib/shared-browser-runtime.js +82 -0
- package/lib/shared-browser-tools.js +21 -0
- package/lib/shared-browser-worker.js +160 -0
- package/lib/ws-schema.js +10 -0
- package/lib/yoke/skill-discovery.js +22 -2
- package/package.json +1 -1
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
var activityHistory = require("./shared-browser-activity-history");
|
|
2
|
+
var crypto = require("crypto");
|
|
3
|
+
var policy = require("./shared-browser-policy");
|
|
4
|
+
var browserTools = require("./shared-browser-tools");
|
|
5
|
+
var createRuntime = require("./shared-browser-runtime").createRuntime;
|
|
6
|
+
|
|
7
|
+
function attachSharedBrowser(ctx) {
|
|
8
|
+
var records = new Map();
|
|
9
|
+
var viewers = new Map();
|
|
10
|
+
var destroyed = false;
|
|
11
|
+
function allowed(session, ws) {
|
|
12
|
+
try {
|
|
13
|
+
if (ctx.isMate || !session || ctx.sm.sessions.get(session.localId) !== session) return false;
|
|
14
|
+
if (ctx.usersModule.isMultiUser()) {
|
|
15
|
+
var user = ctx.usersModule.findUserById(session.ownerId);
|
|
16
|
+
if (!user || (ws && (!ws._clayUser || String(ws._clayUser.id) !== String(user.id)))) return false;
|
|
17
|
+
var actor = { _clayUser: user };
|
|
18
|
+
if (!ctx.requestAccess.canAccessProject(actor, ctx.slug) || !ctx.requestAccess.hasPermission(actor, "terminal")) return false;
|
|
19
|
+
} else if (ws && !ctx.requestAccess.hasPermission(ws, "terminal")) return false;
|
|
20
|
+
if (ctx.osUsers && !ctx.getIdentity(session)) return false;
|
|
21
|
+
return true;
|
|
22
|
+
} catch (e) { return false; }
|
|
23
|
+
}
|
|
24
|
+
function identityKey(session) {
|
|
25
|
+
var identity = ctx.getIdentity(session);
|
|
26
|
+
return identity ? JSON.stringify([identity.uid, identity.gid, identity.home]) : "default";
|
|
27
|
+
}
|
|
28
|
+
function recordAllowed(record, ws) {
|
|
29
|
+
try { return allowed(record.session, ws) && record.ownerId === record.session.ownerId && record.identity === identityKey(record.session); }
|
|
30
|
+
catch (e) { return false; }
|
|
31
|
+
}
|
|
32
|
+
function requireAccess(session, ws) {
|
|
33
|
+
if (!allowed(session, ws)) throw new Error("Shared browser access is not permitted for this session");
|
|
34
|
+
}
|
|
35
|
+
function projection(record) {
|
|
36
|
+
if (!record) return null;
|
|
37
|
+
return { id: record.id, phase: record.phase, control: record.control, epoch: record.epoch, handoff: !!record.handoff,
|
|
38
|
+
url: record.url, width: record.width, height: record.height, activity: record.activity || null, activityHistory: record.activityHistory || [], pointer: record.pointer || null, error: record.error || "" };
|
|
39
|
+
}
|
|
40
|
+
function sessionRecords(session) {
|
|
41
|
+
return Array.from(records.values()).filter(function (record) { return record.session === session && recordAllowed(record); });
|
|
42
|
+
}
|
|
43
|
+
function selectRecord(session, id) {
|
|
44
|
+
var tabs = sessionRecords(session);
|
|
45
|
+
if (id) {
|
|
46
|
+
var record = records.get(id);
|
|
47
|
+
if (!record || record.session !== session || !recordAllowed(record)) throw new Error("Browser tab expired");
|
|
48
|
+
return record;
|
|
49
|
+
}
|
|
50
|
+
if (tabs.length > 1) throw new Error("Choose a browserId from status before operating a tab");
|
|
51
|
+
return tabs[0] || null;
|
|
52
|
+
}
|
|
53
|
+
function sendState(ws, session, record, present, focus) {
|
|
54
|
+
if (!ws || ws.readyState !== 1 || !allowed(session, ws) || (record && !recordAllowed(record, ws))) return;
|
|
55
|
+
ctx.sendTo(ws, { type: "shared_browser_state", sessionId: session.localId, browser: projection(record), browsers: sessionRecords(session).map(projection), present: !!present, focus: !!focus });
|
|
56
|
+
}
|
|
57
|
+
function broadcast(record, present) {
|
|
58
|
+
ctx.clients.forEach(function (ws) {
|
|
59
|
+
if (ctx.getSessionForWs(ws) === record.session) sendState(ws, record.session, record, present);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function watching(record, ws) {
|
|
63
|
+
return ws.readyState === 1 && viewers.get(ws) === record.id && ctx.getSessionForWs(ws) === record.session && recordAllowed(record, ws);
|
|
64
|
+
}
|
|
65
|
+
function visibility(record) {
|
|
66
|
+
if (!record.runtime) return;
|
|
67
|
+
var visible = false;
|
|
68
|
+
viewers.forEach(function (id, ws) { if (id === record.id && watching(record, ws)) visible = true; });
|
|
69
|
+
record.runtime.viewing(visible);
|
|
70
|
+
}
|
|
71
|
+
function end(record, reason) {
|
|
72
|
+
if (record.runtime) record.runtime.close();
|
|
73
|
+
record.runtime = null;
|
|
74
|
+
record.phase = "ended";
|
|
75
|
+
record.error = reason || "";
|
|
76
|
+
record.control = "user";
|
|
77
|
+
activityHistory.interruptActivity(record); record.pointer = null;
|
|
78
|
+
record.epoch++;
|
|
79
|
+
record.rejectReady(new Error(reason || "Browser ended"));
|
|
80
|
+
broadcast(record);
|
|
81
|
+
}
|
|
82
|
+
function receive(record, message) {
|
|
83
|
+
if (destroyed || records.get(record.id) !== record || !record.runtime) return;
|
|
84
|
+
if (!recordAllowed(record)) { end(record, "Browser access was revoked"); records.delete(record.id); return; }
|
|
85
|
+
if (message.type === "activity" || message.type === "pointer") {
|
|
86
|
+
if (message.epoch !== record.epoch || message.generation !== Number(record.session._sdkQueryGeneration || 0) || record.control !== "agent" || record.handoff) return;
|
|
87
|
+
record.activityGeneration = message.generation;
|
|
88
|
+
if (message.type === "activity") activityHistory.recordActivity(record, message.activity);
|
|
89
|
+
else record.pointer = message.pointer;
|
|
90
|
+
broadcast(record); return;
|
|
91
|
+
}
|
|
92
|
+
if ((record.activity || record.pointer) && record.activityGeneration !== Number(record.session._sdkQueryGeneration || 0)) {
|
|
93
|
+
activityHistory.interruptActivity(record); record.pointer = null; broadcast(record);
|
|
94
|
+
}
|
|
95
|
+
if (message.type === "ready") { record.phase = "live"; record.resolveReady(); }
|
|
96
|
+
if (message.type === "error") { end(record, message.error); return; }
|
|
97
|
+
if (message.url !== undefined) record.url = policy.visibleUrl(message.url);
|
|
98
|
+
if (message.width) { record.width = message.width; record.height = message.height; }
|
|
99
|
+
if (message.type === "frame") {
|
|
100
|
+
record.frame = { type: "shared_browser_frame", sessionId: record.session.localId, browserId: record.id,
|
|
101
|
+
data: message.data, width: record.width, height: record.height };
|
|
102
|
+
viewers.forEach(function (id, ws) {
|
|
103
|
+
if (watching(record, ws) && ws.bufferedAmount < 1024 * 1024) ctx.sendTo(ws, record.frame);
|
|
104
|
+
});
|
|
105
|
+
visibility(record);
|
|
106
|
+
} else broadcast(record);
|
|
107
|
+
}
|
|
108
|
+
function create(session, control, newTab) {
|
|
109
|
+
requireAccess(session);
|
|
110
|
+
var tabs = sessionRecords(session);
|
|
111
|
+
var previous = !newTab && tabs.find(function (record) { return record.runtime && record.control === control; });
|
|
112
|
+
if (previous) return previous;
|
|
113
|
+
if (tabs.length >= 12) {
|
|
114
|
+
var expired = tabs.find(function (record) { return !record.runtime; });
|
|
115
|
+
if (expired) records.delete(expired.id);
|
|
116
|
+
else throw new Error("Close a browser tab before opening another");
|
|
117
|
+
}
|
|
118
|
+
var active = 0;
|
|
119
|
+
records.forEach(function (record) { if (record.runtime && record.ownerId === session.ownerId) active++; });
|
|
120
|
+
if (active >= 4) throw new Error("End another browser before opening more than four active tabs");
|
|
121
|
+
var record = { session: session, ownerId: session.ownerId, identity: identityKey(session), id: crypto.randomUUID(), phase: "starting", control: control, epoch: 0,
|
|
122
|
+
url: "", width: 1280, height: 800, touched: Date.now(), runtime: null, frame: null };
|
|
123
|
+
record.ready = new Promise(function (resolve, reject) { record.resolveReady = resolve; record.rejectReady = reject; });
|
|
124
|
+
record.ready.catch(function () {});
|
|
125
|
+
records.set(record.id, record);
|
|
126
|
+
try {
|
|
127
|
+
record.runtime = (ctx.createRuntime || createRuntime)({ identity: ctx.getIdentity(session), onEvent: function (message) { receive(record, message); } });
|
|
128
|
+
} catch (e) { end(record, e.message); }
|
|
129
|
+
broadcast(record, true);
|
|
130
|
+
return record;
|
|
131
|
+
}
|
|
132
|
+
async function changeControl(record, control) {
|
|
133
|
+
if (!record.runtime) throw new Error("Open a browser first");
|
|
134
|
+
record.control = control;
|
|
135
|
+
activityHistory.interruptActivity(record); record.pointer = null;
|
|
136
|
+
record.epoch++;
|
|
137
|
+
record.touched = Date.now();
|
|
138
|
+
record.handoff = true;
|
|
139
|
+
var epoch = record.epoch;
|
|
140
|
+
broadcast(record);
|
|
141
|
+
try {
|
|
142
|
+
await record.runtime.control(epoch);
|
|
143
|
+
if (record.runtime && record.epoch === epoch) { record.handoff = false; broadcast(record); }
|
|
144
|
+
} catch (e) { if (record.runtime && record.epoch === epoch) end(record, e.message); }
|
|
145
|
+
|
|
146
|
+
}
|
|
147
|
+
async function run(session, record, actor, action, event, intent) {
|
|
148
|
+
requireAccess(session);
|
|
149
|
+
if (!record || !record.runtime || !recordAllowed(record)) throw new Error("Open a browser first");
|
|
150
|
+
if (record.handoff) throw new Error("Browser control is changing; wait for the handoff");
|
|
151
|
+
if (action !== "inspect" && record.control !== actor) throw new Error(actor === "agent" ? "The user controls this browser. Ask them to choose Give control." : "The Driver controls this browser. Choose Take control first.");
|
|
152
|
+
var epoch = record.epoch;
|
|
153
|
+
var generation = Number(session._sdkQueryGeneration || 0);
|
|
154
|
+
await record.ready;
|
|
155
|
+
if (actor === "agent" && Number(session._sdkQueryGeneration || 0) !== generation) throw new Error("Browser request belongs to an older query");
|
|
156
|
+
requireAccess(session);
|
|
157
|
+
if (!recordAllowed(record) || !record.runtime || record.epoch !== epoch) throw new Error("Browser control changed; retry after the handoff");
|
|
158
|
+
record.touched = Date.now();
|
|
159
|
+
var result;
|
|
160
|
+
try { result = await record.runtime.request(action, event, epoch, actor === "agent" ? { intent: intent || "", generation: generation } : null); }
|
|
161
|
+
catch (error) {
|
|
162
|
+
if (actor === "agent" && record.epoch === epoch && record.activity && record.activity.phase === "running") {
|
|
163
|
+
activityHistory.recordActivity(record, Object.assign({}, record.activity, { phase: "failed" })); broadcast(record);
|
|
164
|
+
}
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
requireAccess(session);
|
|
168
|
+
if (actor === "agent" && Number(session._sdkQueryGeneration || 0) !== generation) throw new Error("Browser result belongs to an older query");
|
|
169
|
+
if (!recordAllowed(record) || records.get(record.id) !== record || record.epoch !== epoch) throw new Error("Browser control changed during the action");
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
async function tool(session, args) {
|
|
173
|
+
try {
|
|
174
|
+
requireAccess(session);
|
|
175
|
+
if (args.intent !== undefined && (typeof args.intent !== "string" || args.intent.length > 160)) throw new Error("Browser intent must be at most 160 characters");
|
|
176
|
+
var record = args.action === "status" || args.action === "open" ? null : selectRecord(session, args.browserId);
|
|
177
|
+
var value;
|
|
178
|
+
if (args.action === "status") value = { browsers: sessionRecords(session).map(projection) };
|
|
179
|
+
else if (args.action === "open") {
|
|
180
|
+
var url = args.url ? policy.browserUrl(args.url) : null;
|
|
181
|
+
record = args.browserId ? selectRecord(session, args.browserId) : create(session, "agent", args.newTab === true);
|
|
182
|
+
if (url) await run(session, record, "agent", "action", { kind: "navigate", url: url }, args.intent);
|
|
183
|
+
value = projection(record);
|
|
184
|
+
} else if (args.action === "finish") {
|
|
185
|
+
if (!record || record.control !== "agent") throw new Error("The Driver does not control this browser");
|
|
186
|
+
await changeControl(record, "user"); value = projection(record);
|
|
187
|
+
} else if (args.action === "inspect") {
|
|
188
|
+
value = await run(session, record, "agent", "inspect", null, args.intent);
|
|
189
|
+
value.browserId = record.id;
|
|
190
|
+
var image = value.image; delete value.image;
|
|
191
|
+
return { content: [{ type: "text", text: JSON.stringify(value) }, { type: "image", mimeType: "image/jpeg", data: image }] };
|
|
192
|
+
} else {
|
|
193
|
+
value = await run(session, record, "agent", "action", policy.validateAction(Object.assign({}, args, { kind: args.action })), args.intent);
|
|
194
|
+
}
|
|
195
|
+
return { content: [{ type: "text", text: JSON.stringify(value) }] };
|
|
196
|
+
} catch (e) { return { isError: true, content: [{ type: "text", text: e.message }] }; }
|
|
197
|
+
}
|
|
198
|
+
function handleMessage(ws, msg) {
|
|
199
|
+
if (!msg.type || msg.type.indexOf("shared_browser_") !== 0) return false;
|
|
200
|
+
var session = ctx.getSessionForWs(ws);
|
|
201
|
+
Promise.resolve().then(async function () {
|
|
202
|
+
requireAccess(session, ws);
|
|
203
|
+
if (String(msg.sessionId) !== String(session.localId)) throw new Error("Browser session changed");
|
|
204
|
+
var record;
|
|
205
|
+
if (msg.type === "shared_browser_state_request") { sendState(ws, session, sessionRecords(session)[0]); return; }
|
|
206
|
+
if (msg.type === "shared_browser_open") { record = create(session, "user", msg.newTab === true); sendState(ws, session, record, true, true); return; }
|
|
207
|
+
record = selectRecord(session, msg.browserId);
|
|
208
|
+
if (!record || !recordAllowed(record, ws) || msg.browserId !== record.id) throw new Error("Browser session expired");
|
|
209
|
+
if (msg.type === "shared_browser_view") {
|
|
210
|
+
if (msg.visible) { var previous = records.get(viewers.get(ws)); viewers.set(ws, record.id); if (previous && previous !== record) visibility(previous); if (record.frame) ctx.sendTo(ws, record.frame); }
|
|
211
|
+
else viewers.delete(ws);
|
|
212
|
+
visibility(record);
|
|
213
|
+
} else if (msg.type === "shared_browser_control") {
|
|
214
|
+
if (!["agent", "user"].includes(msg.control)) throw new Error("Invalid control mode");
|
|
215
|
+
await changeControl(record, msg.control);
|
|
216
|
+
} else if (msg.type === "shared_browser_close") {
|
|
217
|
+
end(record); records.delete(record.id);
|
|
218
|
+
sendState(ws, session, sessionRecords(session)[0]);
|
|
219
|
+
ctx.clients.forEach(function (peer) { if (peer !== ws && ctx.getSessionForWs(peer) === session) sendState(peer, session, sessionRecords(session)[0]); });
|
|
220
|
+
} else if (msg.type === "shared_browser_end") end(record);
|
|
221
|
+
else if (msg.type === "shared_browser_input") {
|
|
222
|
+
if (msg.epoch !== record.epoch) throw new Error("Browser control changed");
|
|
223
|
+
await run(session, record, "user", "action", policy.validateAction(msg.event));
|
|
224
|
+
}
|
|
225
|
+
}).catch(function (e) {
|
|
226
|
+
if (ws.readyState === 1) ctx.sendTo(ws, { type: "shared_browser_error", sessionId: msg.sessionId, error: e.message });
|
|
227
|
+
});
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
var sweep = setInterval(function () {
|
|
231
|
+
viewers.forEach(function (id, ws) { if (ws.readyState !== 1) viewers.delete(ws); });
|
|
232
|
+
records.forEach(function (record, key) {
|
|
233
|
+
if (!recordAllowed(record)) { end(record, "Browser session is no longer available"); records.delete(key); }
|
|
234
|
+
else if (Date.now() - record.touched > 30 * 60 * 1000) { end(record, "Browser expired after 30 minutes without activity"); records.delete(key); }
|
|
235
|
+
else visibility(record);
|
|
236
|
+
});
|
|
237
|
+
}, 15000);
|
|
238
|
+
sweep.unref();
|
|
239
|
+
function getToolDefs(session) {
|
|
240
|
+
var generation = Number(session && session._sdkQueryGeneration || 0);
|
|
241
|
+
return ctx.isMate ? [] : browserTools.getToolDefs(function (args) {
|
|
242
|
+
if (session && Number(session._sdkQueryGeneration || 0) !== generation) {
|
|
243
|
+
return Promise.resolve({ isError: true, content: [{ type: "text", text: "This browser tool belongs to an older query" }] });
|
|
244
|
+
}
|
|
245
|
+
return tool(session, args || {});
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
handleMessage: handleMessage,
|
|
250
|
+
getToolDefs: getToolDefs,
|
|
251
|
+
getSystemPrompt: function () { return ctx.isMate ? "" : browserTools.PROMPT; },
|
|
252
|
+
createMcpServer: function (adapter, session) { return ctx.isMate || !adapter || typeof adapter.createToolServer !== "function" ? null : adapter.createToolServer({ name: "clay-shared-browser", version: "1.0.0", tools: getToolDefs(session) }); },
|
|
253
|
+
destroy: function () { destroyed = true; clearInterval(sweep); records.forEach(function (record) { end(record); }); records.clear(); viewers.clear(); },
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
module.exports = { attachSharedBrowser: attachSharedBrowser };
|
package/lib/project.js
CHANGED
|
@@ -62,6 +62,7 @@ var { attachDriverContinuation } = require("./project-driver-continuation");
|
|
|
62
62
|
var driverContinuationAccess = require("./driver-continuation-access");
|
|
63
63
|
var { attachSessionNotes, composeSystemPrompts } = require("./project-session-notes");
|
|
64
64
|
var { attachSessionDocument } = require("./project-session-document");
|
|
65
|
+
var { attachSharedBrowser } = require("./project-shared-browser");
|
|
65
66
|
var { attachCapsuleCatalog } = require("./project-capsule-catalog");
|
|
66
67
|
var { attachProjectWorkspaceQuery } = require("./project-workspace-query");
|
|
67
68
|
var { attachProjectLogs } = require("./project-logs");
|
|
@@ -753,6 +754,15 @@ function createProjectContext(opts) {
|
|
|
753
754
|
}
|
|
754
755
|
},
|
|
755
756
|
});
|
|
757
|
+
var _sharedBrowser = attachSharedBrowser({
|
|
758
|
+
slug: slug, isMate: isMate, sm: sm, clients: clients, sendTo: sendTo,
|
|
759
|
+
usersModule: usersModule, requestAccess: _requestAccess, osUsers: osUsers,
|
|
760
|
+
getSessionForWs: getSessionForWs,
|
|
761
|
+
getIdentity: function (session) {
|
|
762
|
+
var linuxUser = getLinuxUserForSession(session);
|
|
763
|
+
return linuxUser ? resolveOsUserInfo(linuxUser) : null;
|
|
764
|
+
},
|
|
765
|
+
});
|
|
756
766
|
var _sessionDocument = attachSessionDocument({
|
|
757
767
|
cwd: cwd,
|
|
758
768
|
isMate: isMate,
|
|
@@ -832,6 +842,32 @@ function createProjectContext(opts) {
|
|
|
832
842
|
return !usersModule.isMultiUser() || !!(ws._clayUser && ws._clayUser.id === session.ownerId);
|
|
833
843
|
},
|
|
834
844
|
});
|
|
845
|
+
var _sessionLinear = require("./project-session-linear").attachSessionLinear({
|
|
846
|
+
sm: sm, isMate: isMate, storage: opts.linearStorage, projectSlug: slug,
|
|
847
|
+
isMultiUser: usersModule.isMultiUser, sendTo: sendTo,
|
|
848
|
+
canAccess: function (ws) {
|
|
849
|
+
return !isMate && clients.has(ws) && ws.readyState === 1 && (!usersModule.isMultiUser() ||
|
|
850
|
+
!!(ws._clayUser && usersModule.findUserById(ws._clayUser.id) && opts.canAccessProjectSlug(ws._clayUser.id, slug)));
|
|
851
|
+
},
|
|
852
|
+
startWork: require('./project-linear-launch').attachLinearLaunch({
|
|
853
|
+
sm: sm, resolveDefaultAi: opts.resolveDefaultAi, getSdk: function () { return sdk; }, osUsers: osUsers,
|
|
854
|
+
getLinuxUserForSession: getLinuxUserForSession, ensureProjectAccessForSession: ensureProjectAccessForSession,
|
|
855
|
+
getProjectAccess: function () { return opts.getProjectAccess ? opts.getProjectAccess() : { visibility: 'public', ownerId: projectOwnerId || null }; },
|
|
856
|
+
onProcessingChanged: onProcessingChanged,
|
|
857
|
+
}),
|
|
858
|
+
canUse: function (session) {
|
|
859
|
+
if (session.hidden || session.delegated || session._pairDelegation || session._delegatedBy || require("./session-provenance").isWorker(session)) return false;
|
|
860
|
+
if (!usersModule.isMultiUser()) return true;
|
|
861
|
+
var owner = session.ownerId && usersModule.findUserById(session.ownerId);
|
|
862
|
+
return !!(owner && opts.canAccessProjectSlug && opts.canAccessProjectSlug(owner.id, slug));
|
|
863
|
+
},
|
|
864
|
+
canRead: function (caller, other) {
|
|
865
|
+
return !usersModule.isMultiUser() || usersModule.canAccessSession(caller.ownerId, other, opts.getProjectAccess ? opts.getProjectAccess() : { visibility: "public" });
|
|
866
|
+
},
|
|
867
|
+
canRefresh: function (ws, session) {
|
|
868
|
+
return !usersModule.isMultiUser() || !!(ws._clayUser && ws._clayUser.id === session.ownerId);
|
|
869
|
+
},
|
|
870
|
+
});
|
|
835
871
|
var _mateKnowledge = attachProjectMateKnowledge({
|
|
836
872
|
service: opts.mateKnowledgeService || null,
|
|
837
873
|
sm: sm,
|
|
@@ -1014,6 +1050,8 @@ function createProjectContext(opts) {
|
|
|
1014
1050
|
// get the read-only cross-project set only when the Mate registry confirms
|
|
1015
1051
|
// authoritative builtin Clay.
|
|
1016
1052
|
try {
|
|
1053
|
+
var linearMcpConfig = _sessionLinear.createMcpServer(adapter, null);
|
|
1054
|
+
if (linearMcpConfig) servers['clay-linear'] = linearMcpConfig;
|
|
1017
1055
|
var githubMcpConfig = _sessionGithub.createMcpServer(adapter, null);
|
|
1018
1056
|
if (githubMcpConfig) servers["clay-github"] = githubMcpConfig;
|
|
1019
1057
|
var issuesMcpConfig = _issues.createMcpServer(adapter, null);
|
|
@@ -1045,6 +1083,9 @@ function createProjectContext(opts) {
|
|
|
1045
1083
|
}
|
|
1046
1084
|
}
|
|
1047
1085
|
|
|
1086
|
+
var sharedBrowserConfig = _sharedBrowser.createMcpServer(adapter);
|
|
1087
|
+
if (sharedBrowserConfig) servers["clay-shared-browser"] = sharedBrowserConfig;
|
|
1088
|
+
|
|
1048
1089
|
// Explicit agent signal for user-requested Markdown presentation.
|
|
1049
1090
|
if (!isMate) {
|
|
1050
1091
|
try {
|
|
@@ -1199,6 +1240,11 @@ function createProjectContext(opts) {
|
|
|
1199
1240
|
}
|
|
1200
1241
|
continue;
|
|
1201
1242
|
}
|
|
1243
|
+
if (name === "clay-linear" && forSession) {
|
|
1244
|
+
var boundLinear = _sessionLinear.createMcpServer(adapter, forSession);
|
|
1245
|
+
if (boundLinear) { filtered[name] = boundLinear; hasAny = true; }
|
|
1246
|
+
continue;
|
|
1247
|
+
}
|
|
1202
1248
|
if (name === "clay-github" && forSession) {
|
|
1203
1249
|
var boundGithub = _sessionGithub.createMcpServer(adapter, forSession);
|
|
1204
1250
|
if (boundGithub) { filtered[name] = boundGithub; hasAny = true; }
|
|
@@ -1263,6 +1309,11 @@ function createProjectContext(opts) {
|
|
|
1263
1309
|
}
|
|
1264
1310
|
continue;
|
|
1265
1311
|
}
|
|
1312
|
+
if (name === "clay-shared-browser" && forSession) {
|
|
1313
|
+
var boundBrowser = _sharedBrowser.createMcpServer(adapter, forSession);
|
|
1314
|
+
if (boundBrowser) { filtered[name] = boundBrowser; hasAny = true; }
|
|
1315
|
+
continue;
|
|
1316
|
+
}
|
|
1266
1317
|
if (name === "clay-documents" && forSession) {
|
|
1267
1318
|
try {
|
|
1268
1319
|
var boundDocuments = _sessionDocument.createMcpServer(adapter, forSession);
|
|
@@ -1374,10 +1425,12 @@ function createProjectContext(opts) {
|
|
|
1374
1425
|
_sessionHandoff.getSystemPrompt(session),
|
|
1375
1426
|
_driverContinuation.getSystemPrompt(session),
|
|
1376
1427
|
_sessionDocument.getSystemPrompt(session),
|
|
1428
|
+
_sharedBrowser.getSystemPrompt(session),
|
|
1377
1429
|
_capsuleCatalog.getSystemPrompt(session),
|
|
1378
1430
|
_projectLogs.getSystemPrompt(session),
|
|
1379
1431
|
_issues.getSystemPrompt(session),
|
|
1380
1432
|
_sessionGithub.getSystemPrompt(session),
|
|
1433
|
+
_sessionLinear.getSystemPrompt(session),
|
|
1381
1434
|
_scheduledTasks ? _scheduledTasks.getSystemPrompt(session) : "",
|
|
1382
1435
|
_mateKnowledge.getSystemPrompt(session),
|
|
1383
1436
|
_autonomousRun ? _autonomousRun.getSystemPrompt(session) : "",
|
|
@@ -1393,6 +1446,7 @@ function createProjectContext(opts) {
|
|
|
1393
1446
|
.concat(_sessionHandoff.getToolDefs(session))
|
|
1394
1447
|
.concat(_driverContinuation.getToolDefs(session, Number(session._sdkQueryGeneration || 0)))
|
|
1395
1448
|
.concat(_sessionDocument.getToolDefs(session))
|
|
1449
|
+
.concat(_sharedBrowser.getToolDefs(session))
|
|
1396
1450
|
.concat(_debateProposal.getToolDefs(session))
|
|
1397
1451
|
.concat(_mateCreationProposal.getToolDefs(session))
|
|
1398
1452
|
.concat(_workspaceQuery.getDynamicToolDefs(session))
|
|
@@ -1400,6 +1454,7 @@ function createProjectContext(opts) {
|
|
|
1400
1454
|
.concat(_projectLogs.getDynamicToolDefs(session))
|
|
1401
1455
|
.concat(_issues.getDynamicToolDefs(session))
|
|
1402
1456
|
.concat(_sessionGithub.getDynamicToolDefs(session))
|
|
1457
|
+
.concat(_sessionLinear.getDynamicToolDefs(session))
|
|
1403
1458
|
.concat(_mateKnowledge.getDynamicToolDefs(session))
|
|
1404
1459
|
.concat(_loopInterview ? _loopInterview.getToolDefs(session) : [])
|
|
1405
1460
|
.concat(_scheduledTasks ? _scheduledTasks.getToolDefs(session) : [])
|
|
@@ -1792,6 +1847,7 @@ function createProjectContext(opts) {
|
|
|
1792
1847
|
if (_projectLogs.handleLogsMessage(ws, msg)) return;
|
|
1793
1848
|
if (_issues.handleMessage(ws, msg)) return;
|
|
1794
1849
|
if (_sessionGithub.handleMessage(ws, msg)) return;
|
|
1850
|
+
if (_sessionLinear.handleMessage(ws, msg)) return;
|
|
1795
1851
|
|
|
1796
1852
|
// --- Notifications (delegated to project-notifications.js) ---
|
|
1797
1853
|
if (_notifications.handleNotificationMessage(ws, msg)) return;
|
|
@@ -1821,6 +1877,7 @@ function createProjectContext(opts) {
|
|
|
1821
1877
|
if (_shellCommand.handleShellCommand(ws, msg)) return;
|
|
1822
1878
|
|
|
1823
1879
|
// --- Vendor login flow (delegated to project-vendor-login.js) ---
|
|
1880
|
+
if (_sharedBrowser.handleMessage(ws, msg)) return;
|
|
1824
1881
|
if (_vendorLogin.handleVendorLoginMessage(ws, msg)) return;
|
|
1825
1882
|
|
|
1826
1883
|
// --- Notes, terminals, context, user message (delegated to project-user-message.js) ---
|
|
@@ -2344,6 +2401,11 @@ function createProjectContext(opts) {
|
|
|
2344
2401
|
inputSchema: normalizeToolSchema(continuationTools[cti].inputSchema),
|
|
2345
2402
|
});
|
|
2346
2403
|
}
|
|
2404
|
+
var sharedTools = _sharedBrowser.getToolDefs(boundSession);
|
|
2405
|
+
sharedTools.forEach(function (definition) {
|
|
2406
|
+
tools.push({ server: "clay-shared-browser", name: definition.name, description: definition.description,
|
|
2407
|
+
inputSchema: normalizeToolSchema(definition.inputSchema) });
|
|
2408
|
+
});
|
|
2347
2409
|
var documentTools = _sessionDocument.getToolDefs(boundSession);
|
|
2348
2410
|
for (var dti = 0; dti < documentTools.length; dti++) {
|
|
2349
2411
|
tools.push({
|
|
@@ -2363,6 +2425,7 @@ function createProjectContext(opts) {
|
|
|
2363
2425
|
for (var cht = 0; cht < historyTools.length; cht++) tools.push(historyTools[cht]);
|
|
2364
2426
|
tools = tools.concat(_issues.getBridgeTools(boundSession, normalizeToolSchema));
|
|
2365
2427
|
tools = tools.concat(_sessionGithub.getBridgeTools(boundSession, normalizeToolSchema));
|
|
2428
|
+
tools = tools.concat(_sessionLinear.getBridgeTools(boundSession, normalizeToolSchema));
|
|
2366
2429
|
var logsTools = _projectLogs.getBridgeTools(boundSession, normalizeToolSchema);
|
|
2367
2430
|
for (var plti = 0; plti < logsTools.length; plti++) tools.push(logsTools[plti]);
|
|
2368
2431
|
var knowledgeTools = _mateKnowledge.getBridgeTools(boundSession, normalizeToolSchema);
|
|
@@ -2387,10 +2450,11 @@ function createProjectContext(opts) {
|
|
|
2387
2450
|
var inAppNames = Object.keys(localMcp);
|
|
2388
2451
|
for (var i = 0; i < inAppNames.length; i++) {
|
|
2389
2452
|
if (inAppNames[i] === "clay-sessions" || inAppNames[i] === "clay-notes" || inAppNames[i] === "clay-handoff" || inAppNames[i] === "clay-documents") continue;
|
|
2453
|
+
if (inAppNames[i] === "clay-shared-browser") continue;
|
|
2390
2454
|
// Debate tools were added above only when this request had an
|
|
2391
2455
|
// exact session. Never duplicate them or advertise the static,
|
|
2392
2456
|
// intentionally unbound proposal server.
|
|
2393
|
-
if (inAppNames[i] === "clay-debate" || inAppNames[i] === "clay-mate-creation" || inAppNames[i] === "clay-workspace" || inAppNames[i] === "clay-history" || inAppNames[i] === "clay-github" || inAppNames[i] === "clay-issues" || inAppNames[i] === "clay-logs" || inAppNames[i] === "clay-knowledge") continue;
|
|
2457
|
+
if (inAppNames[i] === "clay-debate" || inAppNames[i] === "clay-mate-creation" || inAppNames[i] === "clay-workspace" || inAppNames[i] === "clay-history" || inAppNames[i] === "clay-github" || inAppNames[i] === "clay-linear" || inAppNames[i] === "clay-issues" || inAppNames[i] === "clay-logs" || inAppNames[i] === "clay-knowledge") continue;
|
|
2394
2458
|
extractServerTools(inAppNames[i], localMcp[inAppNames[i]]);
|
|
2395
2459
|
}
|
|
2396
2460
|
}
|
|
@@ -2446,6 +2510,12 @@ function createProjectContext(opts) {
|
|
|
2446
2510
|
}
|
|
2447
2511
|
}
|
|
2448
2512
|
}
|
|
2513
|
+
if (boundSession && serverName === "clay-shared-browser") {
|
|
2514
|
+
var sharedTools = _sharedBrowser.getToolDefs(boundSession);
|
|
2515
|
+
for (var bti = 0; bti < sharedTools.length; bti++) {
|
|
2516
|
+
if (sharedTools[bti].name === toolName) return Promise.resolve(sharedTools[bti].handler(args || {}));
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2449
2519
|
if (boundSession && serverName === "clay-documents") {
|
|
2450
2520
|
var documentTools = _sessionDocument.getToolDefs(boundSession);
|
|
2451
2521
|
for (var dti = 0; dti < documentTools.length; dti++) {
|
|
@@ -2476,6 +2546,7 @@ function createProjectContext(opts) {
|
|
|
2476
2546
|
if (boundSession && serverName === "clay-history") {
|
|
2477
2547
|
return _workspaceQuery.callHistoryBridgeTool(boundSession, toolName, args);
|
|
2478
2548
|
}
|
|
2549
|
+
if (boundSession && serverName === "clay-linear") return _sessionLinear.callBridgeTool(boundSession, toolName, args);
|
|
2479
2550
|
if (boundSession && serverName === "clay-github") return _sessionGithub.callBridgeTool(boundSession, toolName, args);
|
|
2480
2551
|
if (boundSession && serverName === "clay-issues") return _issues.callBridgeTool(boundSession, toolName, args);
|
|
2481
2552
|
if (boundSession && serverName === "clay-logs") {
|
|
@@ -2484,7 +2555,7 @@ function createProjectContext(opts) {
|
|
|
2484
2555
|
if (boundSession && serverName === "clay-knowledge") {
|
|
2485
2556
|
return _mateKnowledge.callBridgeTool(boundSession, toolName, args);
|
|
2486
2557
|
}
|
|
2487
|
-
if (serverName === "clay-sessions" || serverName === "clay-notes" || serverName === "clay-handoff" || serverName === "clay-documents" || serverName === "clay-debate" || serverName === "clay-mate-creation" || serverName === "clay-workspace" || serverName === "clay-history" || serverName === "clay-github" || serverName === "clay-issues" || serverName === "clay-logs" || serverName === "clay-knowledge") {
|
|
2558
|
+
if (serverName === "clay-sessions" || serverName === "clay-notes" || serverName === "clay-handoff" || serverName === "clay-documents" || serverName === "clay-shared-browser" || serverName === "clay-debate" || serverName === "clay-mate-creation" || serverName === "clay-workspace" || serverName === "clay-history" || serverName === "clay-github" || serverName === "clay-linear" || serverName === "clay-issues" || serverName === "clay-logs" || serverName === "clay-knowledge") {
|
|
2488
2559
|
return Promise.reject(new Error("Session-bound tool requires a valid Clay session: " + serverName + "/" + toolName));
|
|
2489
2560
|
}
|
|
2490
2561
|
if (sessionOnly) return Promise.reject(new Error("Session tool not found: " + serverName + "/" + toolName));
|
|
@@ -2602,6 +2673,7 @@ function createProjectContext(opts) {
|
|
|
2602
2673
|
|
|
2603
2674
|
// --- Destroy ---
|
|
2604
2675
|
function destroy() {
|
|
2676
|
+
_sharedBrowser.destroy();
|
|
2605
2677
|
_vendorLogin.destroy();
|
|
2606
2678
|
if (_scheduledMessages) _scheduledMessages.shutdown();
|
|
2607
2679
|
if (_autonomousRun) _autonomousRun.shutdown();
|
package/lib/public/app.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { initSharedBrowser } from './modules/shared-browser.js';
|
|
1
2
|
import { initGenerationStop } from './modules/generation-stop.js';
|
|
2
3
|
import { initIssueLinks } from './modules/clay-issue-links.js';
|
|
3
4
|
import { initIssues, closeIssues } from './modules/issues.js';
|
|
@@ -269,6 +270,11 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
269
270
|
createStore({
|
|
270
271
|
networkSettings: { loaded: false, busy: false, confirming: false, draft: "", saved: "", status: "", errorKind: "", requestId: 0 },
|
|
271
272
|
vendorLoginBrowser: null,
|
|
273
|
+
sharedBrowser: null,
|
|
274
|
+
sharedBrowsers: [],
|
|
275
|
+
mcpSkillsWorkbench: {},
|
|
276
|
+
sharedBrowserOpen: false,
|
|
277
|
+
sharedBrowserUi: {},
|
|
272
278
|
// session / routing
|
|
273
279
|
basePath: basePath,
|
|
274
280
|
wsPath: wsPath,
|
|
@@ -1175,6 +1181,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
1175
1181
|
fileViewerEl: $("file-viewer"),
|
|
1176
1182
|
});
|
|
1177
1183
|
initGitPanel();
|
|
1184
|
+
initSharedBrowser();
|
|
1178
1185
|
initWorkerPaneLock();
|
|
1179
1186
|
|
|
1180
1187
|
// --- Terminal ---
|
|
@@ -1054,12 +1054,6 @@
|
|
|
1054
1054
|
flex-direction: column;
|
|
1055
1055
|
overflow: hidden;
|
|
1056
1056
|
flex-shrink: 0;
|
|
1057
|
-
box-shadow:
|
|
1058
|
-
4px 8px 18px rgba(var(--shadow-rgb), 0.14),
|
|
1059
|
-
2px 2px 4px rgba(var(--shadow-rgb), 0.08),
|
|
1060
|
-
inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
|
1061
|
-
animation: workbench-panel-in 0.18s cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
1062
|
-
transition: max-width 0.2s ease, width 0.2s ease, box-shadow 0.2s ease;
|
|
1063
1057
|
}
|
|
1064
1058
|
#file-viewer.hidden { display: none; }
|
|
1065
1059
|
#file-viewer.file-viewer-wide {
|
|
@@ -1078,15 +1072,10 @@
|
|
|
1078
1072
|
border: 0;
|
|
1079
1073
|
border-radius: 0;
|
|
1080
1074
|
background: var(--bg);
|
|
1081
|
-
box-shadow: none;
|
|
1082
|
-
animation: none;
|
|
1083
1075
|
}
|
|
1084
1076
|
}
|
|
1085
1077
|
|
|
1086
|
-
|
|
1087
|
-
from { opacity: 0; transform: translateX(10px) scale(0.995); }
|
|
1088
|
-
to { opacity: 1; transform: translateX(0) scale(1); }
|
|
1089
|
-
}
|
|
1078
|
+
|
|
1090
1079
|
|
|
1091
1080
|
/* Narrow screens: full overlay */
|
|
1092
1081
|
@media (max-width: 1023px) {
|
|
@@ -2026,11 +2015,6 @@
|
|
|
2026
2015
|
flex-direction: column;
|
|
2027
2016
|
overflow: hidden;
|
|
2028
2017
|
flex-shrink: 0;
|
|
2029
|
-
box-shadow:
|
|
2030
|
-
4px 8px 18px rgba(var(--shadow-rgb), 0.14),
|
|
2031
|
-
2px 2px 4px rgba(var(--shadow-rgb), 0.08),
|
|
2032
|
-
inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
|
2033
|
-
animation: workbench-panel-in 0.18s cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
2034
2018
|
}
|
|
2035
2019
|
#terminal-container.hidden { display: none; }
|
|
2036
2020
|
#terminal-container.panel-fullscreen {
|
|
@@ -2041,17 +2025,10 @@
|
|
|
2041
2025
|
margin: 0;
|
|
2042
2026
|
border: 0;
|
|
2043
2027
|
border-radius: 0;
|
|
2044
|
-
box-shadow: none;
|
|
2045
|
-
animation: none;
|
|
2046
2028
|
}
|
|
2047
2029
|
}
|
|
2048
2030
|
|
|
2049
|
-
|
|
2050
|
-
#file-viewer,
|
|
2051
|
-
#terminal-container {
|
|
2052
|
-
animation: none;
|
|
2053
|
-
}
|
|
2054
|
-
}
|
|
2031
|
+
|
|
2055
2032
|
|
|
2056
2033
|
/* Narrow screens: full overlay */
|
|
2057
2034
|
@media (max-width: 1023px) {
|
|
@@ -1,20 +1,7 @@
|
|
|
1
1
|
#issues-panel { position: relative; width: 50%; max-width: 720px; min-width: 360px; height: calc(100% - 16px); margin: 8px 8px 8px 10px; min-height: 0; align-self: center; display: flex; flex-direction: column; background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; flex-shrink: 0; }
|
|
2
|
-
@media (min-width: 1024px) {
|
|
3
|
-
#issues-panel {
|
|
4
|
-
box-shadow:
|
|
5
|
-
4px 8px 18px rgba(var(--shadow-rgb), 0.14),
|
|
6
|
-
2px 2px 4px rgba(var(--shadow-rgb), 0.08),
|
|
7
|
-
inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
|
8
|
-
animation: workbench-panel-in 0.18s cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
9
|
-
transition: max-width 0.2s ease, width 0.2s ease, box-shadow 0.2s ease;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
@media (prefers-reduced-motion: reduce) {
|
|
13
|
-
#issues-panel { animation: none; }
|
|
14
|
-
}
|
|
15
2
|
#issues-panel.hidden { display: none; }
|
|
16
3
|
#issues-panel.issues-wide { width: 70%; max-width: 1200px; }
|
|
17
|
-
#issues-panel.panel-fullscreen { width: 100%; max-width: none; min-width: 0; height: 100%; margin: 0; border: 0; border-radius: 0;
|
|
4
|
+
#issues-panel.panel-fullscreen { width: 100%; max-width: none; min-width: 0; height: 100%; margin: 0; border: 0; border-radius: 0; }
|
|
18
5
|
#issues-panel header { height: 48px; box-sizing: border-box; display: flex; align-items: center; gap: 12px; padding: 0 16px; border-bottom: 1px solid var(--border-subtle); flex: 0 0 auto; }
|
|
19
6
|
#issues-panel .issues-title { display: inline-flex; align-items: center; gap: 7px; font-weight: 700; }
|
|
20
7
|
#issues-panel .issues-title .lucide { width: 15px; color: var(--accent); }
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#linear-panel { width: 45%; max-width: 720px; min-width: 340px; min-height: 0; height: calc(100% - 16px); margin: 8px; align-self: center; display: flex; flex-direction: column; flex-shrink: 0; border: 1px solid var(--border); border-radius: 12px; background: var(--bg); color: var(--text); overflow: hidden; }
|
|
2
|
+
#linear-panel.hidden { display: none; }
|
|
3
|
+
.linear-topbar, .linear-search, .linear-actions { display: flex; align-items: center; gap: 8px; }
|
|
4
|
+
.linear-topbar { padding: 10px 12px; border-bottom: 1px solid var(--border); }
|
|
5
|
+
.linear-topbar strong { flex: 1; }
|
|
6
|
+
#linear-panel button { font: inherit; color: var(--text-secondary); background: var(--bg-alt); border: 1px solid var(--border); border-radius: 6px; padding: 6px 9px; cursor: pointer; }
|
|
7
|
+
#linear-panel button:hover { background: var(--input-bg); color: var(--text); }
|
|
8
|
+
#linear-panel button:disabled { opacity: .5; cursor: wait; }
|
|
9
|
+
#linear-panel button:focus-visible, #linear-panel input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
10
|
+
.linear-topbar svg { width: 16px; height: 16px; }
|
|
11
|
+
.linear-search { padding: 12px; }
|
|
12
|
+
.linear-search input { width: 100%; min-width: 0; padding: 8px; background: var(--input-bg); color: var(--text); border: 1px solid var(--border); border-radius: 6px; font: inherit; }
|
|
13
|
+
.linear-notice { margin: 0; padding: 0 12px; color: var(--text-muted); font-size: 12px; }
|
|
14
|
+
.linear-notice:not(:empty) { padding-bottom: 10px; }
|
|
15
|
+
.linear-content { overflow: auto; min-height: 0; flex: 1; }
|
|
16
|
+
.linear-empty { padding: 20px; color: var(--text-muted); }
|
|
17
|
+
.linear-actions { flex-wrap: wrap; font-size: 12px; }
|
|
18
|
+
#linear-panel a { color: var(--link); }
|
|
19
|
+
#linear-panel .issue-row { width: 100%; text-align: left; }
|
|
20
|
+
#linear-panel .issue-row small, #linear-panel .issue-row strong { display: block; }
|
|
21
|
+
#linear-panel .issue-discussion article { padding: 14px 0; border-top: 1px solid var(--border-subtle); }
|
|
22
|
+
#linear-panel .issue-doc-body { overflow-wrap: anywhere; }
|
|
23
|
+
#linear-panel .issue-doc-body pre { overflow-x: auto; }
|
|
24
|
+
.session-linear-link { max-width: 360px; min-width: 0; flex-shrink: 1; }
|
|
25
|
+
.session-linear-link > span { white-space: nowrap; }
|
|
26
|
+
.session-linear-link .linear-work-id, .session-linear-link .linear-work-state { flex-shrink: 0; }
|
|
27
|
+
.session-linear-link .linear-work-title { overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
|
28
|
+
.linear-work-state { color: var(--text-secondary); }
|
|
29
|
+
#header-github-links:has(.session-linear-link) { max-width: min(440px, 40vw); }
|
|
30
|
+
#header-github-links:has(.session-linear-link) > .session-github-links { display: flex; }
|
|
31
|
+
@media (max-width: 767px) {
|
|
32
|
+
#header-github-links:has(.session-linear-link) { max-width: 170px; }
|
|
33
|
+
#header-github-links .linear-work-title, #header-github-links .linear-work-state { display: none; }
|
|
34
|
+
}
|
|
35
|
+
@media (max-width: 767px) { #linear-panel { position: fixed; inset: 8px; width: auto; max-width: none; min-width: 0; height: auto; z-index: 300; margin: 0; } }
|