clay-server 4.4.1-beta.1 → 4.5.0-beta.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/lib/project-issues.js +11 -0
- package/lib/project-session-github.js +93 -0
- package/lib/project.js +32 -2
- package/lib/public/app.js +4 -0
- package/lib/public/css/session-github.css +112 -0
- package/lib/public/modules/app-messages.js +2 -0
- package/lib/public/modules/git-panel.js +6 -2
- package/lib/public/modules/issues-render.js +2 -0
- package/lib/public/modules/session-github.js +153 -0
- package/lib/public/modules/sidebar-mobile.js +8 -1
- package/lib/public/modules/sidebar-sessions.js +2 -0
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +11 -0
- package/lib/session-github.js +48 -0
- package/lib/sessions.js +3 -0
- package/package.json +1 -1
package/lib/project-issues.js
CHANGED
|
@@ -39,6 +39,17 @@ function attachProjectIssues(ctx) {
|
|
|
39
39
|
else if (msg.type === "issue_comment") notify(result, "issue_commented");
|
|
40
40
|
else if (msg.type === "issue_delete") notify(result, "issue_deleted");
|
|
41
41
|
}
|
|
42
|
+
if (msg.type === "issue_read" && result) {
|
|
43
|
+
result = Object.assign({}, result, { githubLinks: [] });
|
|
44
|
+
(result.linkedWorkSessions || []).forEach(function (link) {
|
|
45
|
+
try {
|
|
46
|
+
var work = bound.resolveWorkSession({ ref: result.ref, sessionOriginId: link.sessionId });
|
|
47
|
+
(work.githubLinks || []).forEach(function (item) {
|
|
48
|
+
if (!result.githubLinks.some(function (existing) { return existing.url === item.url; })) result.githubLinks.push(item);
|
|
49
|
+
});
|
|
50
|
+
} catch (error) { /* Linked work may no longer be visible. */ }
|
|
51
|
+
});
|
|
52
|
+
}
|
|
42
53
|
ctx.sendTo(ws, { type: "issues_result", requestId: msg.requestId, action: msg.type, result: result, canDelete: bound.canDelete === true });
|
|
43
54
|
}).catch(function (error) {
|
|
44
55
|
var result = error && error.sessionId ? { sessionId: error.sessionId } : undefined;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
var github = require("./session-github");
|
|
2
|
+
var buildShape = require("./session-spawn-mcp-server").buildShape;
|
|
3
|
+
var CONTRACT = "GitHub work links: when an issue is clearly the task this session is implementing, or you create/work on its PR, call link_session_github with its exact GitHub URL. Mere mentions, examples, and reference material must not be linked. Do not infer a session's PR from the repository's current branch: sessions may share a checkout. This records local metadata only and does not post to GitHub. Use get_session_github to inspect/refresh linked status and discover accessible sessions already working on the same reference before starting duplicate work. Keep Issues as quiet background records, not a mandatory workflow. Unlink an incorrect association with unlink_session_github.";
|
|
4
|
+
function attachSessionGithub(ctx) {
|
|
5
|
+
var inflight = new Map();
|
|
6
|
+
var attempts = new Map();
|
|
7
|
+
function authorize(session) {
|
|
8
|
+
if (!session || ctx.sm.sessions.get(session.localId) !== session || ctx.isMate || !ctx.canUse(session)) throw new Error("GitHub links require an authorized project session.");
|
|
9
|
+
var identity = ctx.identity(session);
|
|
10
|
+
// Account login does not imply OS isolation. Match the project's execution mode.
|
|
11
|
+
if (ctx.osUsers && !identity) throw new Error("GitHub requires a mapped OS user with gh authentication in OS-isolated mode.");
|
|
12
|
+
return identity;
|
|
13
|
+
}
|
|
14
|
+
function persist(session) {
|
|
15
|
+
if (ctx.sm.saveSessionFile(session) === false) throw new Error("Could not save GitHub links.");
|
|
16
|
+
ctx.sm.broadcastSessionList();
|
|
17
|
+
}
|
|
18
|
+
async function refresh(session, force) {
|
|
19
|
+
var identity = authorize(session);
|
|
20
|
+
var id = session.localId;
|
|
21
|
+
if (inflight.has(id)) return inflight.get(id);
|
|
22
|
+
if (!force && Date.now() - (attempts.get(id) || 0) < 60000) return session.githubLinks || [];
|
|
23
|
+
attempts.set(id, Date.now());
|
|
24
|
+
var pending = (async function () {
|
|
25
|
+
var links = (session.githubLinks || []).slice();
|
|
26
|
+
var updated = [];
|
|
27
|
+
for (var link of links) {
|
|
28
|
+
try { updated.push(await github.readReference(ctx.cwd, link.url, identity, ctx.run)); }
|
|
29
|
+
catch (error) { updated.push(Object.assign({}, link, { stale: true })); }
|
|
30
|
+
}
|
|
31
|
+
authorize(session);
|
|
32
|
+
// A link/unlink during the request wins over the refresh snapshot.
|
|
33
|
+
session.githubLinks = (session.githubLinks || []).map(function (link) {
|
|
34
|
+
var index = links.indexOf(link);
|
|
35
|
+
return index < 0 ? link : updated[index];
|
|
36
|
+
});
|
|
37
|
+
if (links.length) persist(session);
|
|
38
|
+
return session.githubLinks || [];
|
|
39
|
+
})();
|
|
40
|
+
inflight.set(id, pending);
|
|
41
|
+
try { return await pending; } finally { inflight.delete(id); }
|
|
42
|
+
}
|
|
43
|
+
async function invoke(session, name, args) {
|
|
44
|
+
var identity = authorize(session);
|
|
45
|
+
if (name === "get_session_github") {
|
|
46
|
+
var links = await refresh(session, false);
|
|
47
|
+
var matches = [];
|
|
48
|
+
if (args.url) {
|
|
49
|
+
var url = github.parseReference(args.url).url.toLowerCase();
|
|
50
|
+
ctx.sm.sessions.forEach(function (other) {
|
|
51
|
+
if (!other.hidden && !other.delegated && !require("./session-provenance").isWorker(other) && ctx.canRead(session, other) && (other.githubLinks || []).some(function (link) { return link.url.toLowerCase() === url; })) matches.push({ sessionId: other.localId, title: other.title });
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return { links: links, matchingSessions: matches.slice(0, 20) };
|
|
55
|
+
}
|
|
56
|
+
var ref = github.parseReference(args.url);
|
|
57
|
+
if (name === "unlink_session_github") {
|
|
58
|
+
session.githubLinks = (session.githubLinks || []).filter(function (link) { return link.url.toLowerCase() !== ref.url.toLowerCase(); });
|
|
59
|
+
} else {
|
|
60
|
+
var verified = await github.readReference(ctx.cwd, ref.url, identity, ctx.run);
|
|
61
|
+
authorize(session);
|
|
62
|
+
var retained = (session.githubLinks || []).filter(function (link) { return link.url.toLowerCase() !== ref.url.toLowerCase(); });
|
|
63
|
+
if (retained.length >= 8) throw new Error("A session can link up to eight GitHub items.");
|
|
64
|
+
session.githubLinks = retained.concat([verified]);
|
|
65
|
+
}
|
|
66
|
+
persist(session);
|
|
67
|
+
return { links: session.githubLinks };
|
|
68
|
+
}
|
|
69
|
+
function defs(session) {
|
|
70
|
+
if (ctx.isMate || (session && !ctx.canUse(session))) return [];
|
|
71
|
+
return ["link_session_github", "get_session_github", "unlink_session_github"].map(function (name) {
|
|
72
|
+
return { name: name, permissionName: "mcp__clay-github__" + name, description: CONTRACT + " " + (name === "get_session_github" ? "Read linked status; optional URL finds visible existing work sessions." : name === "link_session_github" ? "Verify and attach one issue or PR to the current session." : "Remove one local association without modifying GitHub."), inputSchema: buildShape({ url: { type: "string", description: "Exact https://github.com/owner/repo/issues/123 or /pull/123 URL." } }, name === "get_session_github" ? [] : ["url"]), handler: async function (args) {
|
|
73
|
+
try { return { content: [{ type: "text", text: JSON.stringify(await invoke(session, name, args || {})) }] }; }
|
|
74
|
+
catch (error) { return { isError: true, content: [{ type: "text", text: error.message }] }; }
|
|
75
|
+
} };
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
getDynamicToolDefs: defs,
|
|
80
|
+
getSystemPrompt: function () { return ctx.isMate ? "" : CONTRACT; },
|
|
81
|
+
createMcpServer: function (adapter, session) { return ctx.isMate || !adapter.createToolServer ? null : adapter.createToolServer({ name: "clay-github", version: "1.0.0", tools: defs(session) }); },
|
|
82
|
+
getBridgeTools: function (session, normalize) { return defs(session).map(function (tool) { return { server: "clay-github", name: tool.name, description: tool.description, inputSchema: normalize(tool.inputSchema) }; }); },
|
|
83
|
+
callBridgeTool: function (session, name, args) { var tool = defs(session).find(function (item) { return item.name === name; }); return tool ? tool.handler(args) : Promise.reject(new Error("GitHub tool unavailable.")); },
|
|
84
|
+
handleMessage: function (ws, msg) {
|
|
85
|
+
if (msg.type !== "session_github_refresh") return false;
|
|
86
|
+
var session = ctx.sm.sessions.get(ws._clayActiveSession);
|
|
87
|
+
if (!session || !ctx.canRefresh(ws, session) || !(session.githubLinks || []).length) return true;
|
|
88
|
+
refresh(session, false).catch(function () {});
|
|
89
|
+
return true;
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
module.exports = { attachSessionGithub: attachSessionGithub };
|
package/lib/project.js
CHANGED
|
@@ -813,6 +813,24 @@ function createProjectContext(opts) {
|
|
|
813
813
|
service: opts.issuesService, projectSlug: slug, isMate: isMate, sm: sm,
|
|
814
814
|
sendTo: sendTo, getClients: function () { return clients; }, startWork: _issueWork,
|
|
815
815
|
});
|
|
816
|
+
var _sessionGithub = require("./project-session-github").attachSessionGithub({
|
|
817
|
+
cwd: cwd, sm: sm, isMate: isMate, osUsers: osUsers,
|
|
818
|
+
canUse: function (session) {
|
|
819
|
+
if (session.hidden || session.delegated || session._pairDelegation || session._delegatedBy || require("./session-provenance").isWorker(session)) return false;
|
|
820
|
+
if (!usersModule.isMultiUser()) return true;
|
|
821
|
+
var owner = session.ownerId && usersModule.findUserById(session.ownerId);
|
|
822
|
+
return !!(owner && opts.canAccessProjectSlug && opts.canAccessProjectSlug(owner.id, slug));
|
|
823
|
+
},
|
|
824
|
+
identity: function (session) {
|
|
825
|
+
return getOsUserInfoForLinuxUser(getLinuxUserForSession(session));
|
|
826
|
+
},
|
|
827
|
+
canRead: function (caller, other) {
|
|
828
|
+
return !usersModule.isMultiUser() || usersModule.canAccessSession(caller.ownerId, other, opts.getProjectAccess ? opts.getProjectAccess() : { visibility: "public" });
|
|
829
|
+
},
|
|
830
|
+
canRefresh: function (ws, session) {
|
|
831
|
+
return !usersModule.isMultiUser() || !!(ws._clayUser && ws._clayUser.id === session.ownerId);
|
|
832
|
+
},
|
|
833
|
+
});
|
|
816
834
|
var _mateKnowledge = attachProjectMateKnowledge({
|
|
817
835
|
service: opts.mateKnowledgeService || null,
|
|
818
836
|
sm: sm,
|
|
@@ -992,6 +1010,8 @@ function createProjectContext(opts) {
|
|
|
992
1010
|
// get the read-only cross-project set only when the Mate registry confirms
|
|
993
1011
|
// authoritative builtin Clay.
|
|
994
1012
|
try {
|
|
1013
|
+
var githubMcpConfig = _sessionGithub.createMcpServer(adapter, null);
|
|
1014
|
+
if (githubMcpConfig) servers["clay-github"] = githubMcpConfig;
|
|
995
1015
|
var issuesMcpConfig = _issues.createMcpServer(adapter, null);
|
|
996
1016
|
if (issuesMcpConfig) servers["clay-issues"] = issuesMcpConfig;
|
|
997
1017
|
var projectLogsMcpConfig = _projectLogs.createMcpServer(adapter, null);
|
|
@@ -1175,6 +1195,11 @@ function createProjectContext(opts) {
|
|
|
1175
1195
|
}
|
|
1176
1196
|
continue;
|
|
1177
1197
|
}
|
|
1198
|
+
if (name === "clay-github" && forSession) {
|
|
1199
|
+
var boundGithub = _sessionGithub.createMcpServer(adapter, forSession);
|
|
1200
|
+
if (boundGithub) { filtered[name] = boundGithub; hasAny = true; }
|
|
1201
|
+
continue;
|
|
1202
|
+
}
|
|
1178
1203
|
if (name === "clay-issues" && forSession) {
|
|
1179
1204
|
var boundIssues = _issues.createMcpServer(adapter, forSession);
|
|
1180
1205
|
if (boundIssues) { filtered[name] = boundIssues; hasAny = true; }
|
|
@@ -1348,6 +1373,7 @@ function createProjectContext(opts) {
|
|
|
1348
1373
|
_capsuleCatalog.getSystemPrompt(session),
|
|
1349
1374
|
_projectLogs.getSystemPrompt(session),
|
|
1350
1375
|
_issues.getSystemPrompt(session),
|
|
1376
|
+
_sessionGithub.getSystemPrompt(session),
|
|
1351
1377
|
_scheduledTasks ? _scheduledTasks.getSystemPrompt(session) : "",
|
|
1352
1378
|
_mateKnowledge.getSystemPrompt(session),
|
|
1353
1379
|
_autonomousRun ? _autonomousRun.getSystemPrompt(session) : "",
|
|
@@ -1369,6 +1395,7 @@ function createProjectContext(opts) {
|
|
|
1369
1395
|
.concat(_workspaceQuery.getHistoryDynamicToolDefs(session))
|
|
1370
1396
|
.concat(_projectLogs.getDynamicToolDefs(session))
|
|
1371
1397
|
.concat(_issues.getDynamicToolDefs(session))
|
|
1398
|
+
.concat(_sessionGithub.getDynamicToolDefs(session))
|
|
1372
1399
|
.concat(_mateKnowledge.getDynamicToolDefs(session))
|
|
1373
1400
|
.concat(_loopInterview ? _loopInterview.getToolDefs(session) : [])
|
|
1374
1401
|
.concat(_scheduledTasks ? _scheduledTasks.getToolDefs(session) : [])
|
|
@@ -1760,6 +1787,7 @@ function createProjectContext(opts) {
|
|
|
1760
1787
|
// --- Project Logs (delegated to project-logs.js) ---
|
|
1761
1788
|
if (_projectLogs.handleLogsMessage(ws, msg)) return;
|
|
1762
1789
|
if (_issues.handleMessage(ws, msg)) return;
|
|
1790
|
+
if (_sessionGithub.handleMessage(ws, msg)) return;
|
|
1763
1791
|
|
|
1764
1792
|
// --- Notifications (delegated to project-notifications.js) ---
|
|
1765
1793
|
if (_notifications.handleNotificationMessage(ws, msg)) return;
|
|
@@ -2328,6 +2356,7 @@ function createProjectContext(opts) {
|
|
|
2328
2356
|
var historyTools = _workspaceQuery.getHistoryBridgeTools(boundSession, normalizeToolSchema);
|
|
2329
2357
|
for (var cht = 0; cht < historyTools.length; cht++) tools.push(historyTools[cht]);
|
|
2330
2358
|
tools = tools.concat(_issues.getBridgeTools(boundSession, normalizeToolSchema));
|
|
2359
|
+
tools = tools.concat(_sessionGithub.getBridgeTools(boundSession, normalizeToolSchema));
|
|
2331
2360
|
var logsTools = _projectLogs.getBridgeTools(boundSession, normalizeToolSchema);
|
|
2332
2361
|
for (var plti = 0; plti < logsTools.length; plti++) tools.push(logsTools[plti]);
|
|
2333
2362
|
var knowledgeTools = _mateKnowledge.getBridgeTools(boundSession, normalizeToolSchema);
|
|
@@ -2355,7 +2384,7 @@ function createProjectContext(opts) {
|
|
|
2355
2384
|
// Debate tools were added above only when this request had an
|
|
2356
2385
|
// exact session. Never duplicate them or advertise the static,
|
|
2357
2386
|
// intentionally unbound proposal server.
|
|
2358
|
-
if (inAppNames[i] === "clay-debate" || inAppNames[i] === "clay-mate-creation" || inAppNames[i] === "clay-workspace" || inAppNames[i] === "clay-history" || inAppNames[i] === "clay-issues" || inAppNames[i] === "clay-logs" || inAppNames[i] === "clay-knowledge") continue;
|
|
2387
|
+
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;
|
|
2359
2388
|
extractServerTools(inAppNames[i], localMcp[inAppNames[i]]);
|
|
2360
2389
|
}
|
|
2361
2390
|
}
|
|
@@ -2441,6 +2470,7 @@ function createProjectContext(opts) {
|
|
|
2441
2470
|
if (boundSession && serverName === "clay-history") {
|
|
2442
2471
|
return _workspaceQuery.callHistoryBridgeTool(boundSession, toolName, args);
|
|
2443
2472
|
}
|
|
2473
|
+
if (boundSession && serverName === "clay-github") return _sessionGithub.callBridgeTool(boundSession, toolName, args);
|
|
2444
2474
|
if (boundSession && serverName === "clay-issues") return _issues.callBridgeTool(boundSession, toolName, args);
|
|
2445
2475
|
if (boundSession && serverName === "clay-logs") {
|
|
2446
2476
|
return _projectLogs.callBridgeTool(boundSession, toolName, args);
|
|
@@ -2448,7 +2478,7 @@ function createProjectContext(opts) {
|
|
|
2448
2478
|
if (boundSession && serverName === "clay-knowledge") {
|
|
2449
2479
|
return _mateKnowledge.callBridgeTool(boundSession, toolName, args);
|
|
2450
2480
|
}
|
|
2451
|
-
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-issues" || serverName === "clay-logs" || serverName === "clay-knowledge") {
|
|
2481
|
+
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") {
|
|
2452
2482
|
return Promise.reject(new Error("Session-bound tool requires a valid Clay session: " + serverName + "/" + toolName));
|
|
2453
2483
|
}
|
|
2454
2484
|
if (sessionOnly) return Promise.reject(new Error("Session tool not found: " + serverName + "/" + toolName));
|
package/lib/public/app.js
CHANGED
|
@@ -286,6 +286,10 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
286
286
|
projectName: projectName,
|
|
287
287
|
cwd: "",
|
|
288
288
|
currentSlug: currentSlug,
|
|
289
|
+
githubWorkPopoverBound: false,
|
|
290
|
+
githubWorkSessions: [],
|
|
291
|
+
githubWorkProject: null,
|
|
292
|
+
githubWorkRefresh: null,
|
|
289
293
|
activeProjectSlug: null,
|
|
290
294
|
sessionActivatedProjectSlug: null,
|
|
291
295
|
sessionListProjectSlug: null,
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/* Quiet work references share one visual language across sessions and Git. */
|
|
2
|
+
.session-work-column {
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
gap: 3px;
|
|
6
|
+
flex: 1 1 auto;
|
|
7
|
+
min-width: 0;
|
|
8
|
+
overflow: hidden;
|
|
9
|
+
}
|
|
10
|
+
.session-work-column > .session-item-text,
|
|
11
|
+
.session-work-column > .mobile-session-title {
|
|
12
|
+
display: block;
|
|
13
|
+
flex: none;
|
|
14
|
+
width: 100%;
|
|
15
|
+
min-width: 0;
|
|
16
|
+
overflow: hidden;
|
|
17
|
+
text-overflow: ellipsis;
|
|
18
|
+
white-space: nowrap;
|
|
19
|
+
}
|
|
20
|
+
.session-github-links {
|
|
21
|
+
display: flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
gap: 8px;
|
|
24
|
+
min-width: 0;
|
|
25
|
+
overflow-x: auto;
|
|
26
|
+
scrollbar-width: none;
|
|
27
|
+
font-size: 11px;
|
|
28
|
+
line-height: 18px;
|
|
29
|
+
}
|
|
30
|
+
.session-github-link {
|
|
31
|
+
display: inline-flex;
|
|
32
|
+
align-items: center;
|
|
33
|
+
gap: 5px;
|
|
34
|
+
padding: 1px 5px;
|
|
35
|
+
border: 1px solid color-mix(in srgb, var(--text, #888) 12%, transparent);
|
|
36
|
+
background: color-mix(in srgb, var(--text, #888) 3%, transparent);
|
|
37
|
+
flex-shrink: 0;
|
|
38
|
+
color: var(--text-muted, #888);
|
|
39
|
+
text-decoration: none;
|
|
40
|
+
border-radius: 5px;
|
|
41
|
+
}
|
|
42
|
+
.session-github-link svg { width: 13px; height: 13px; }
|
|
43
|
+
.session-github-link:hover { color: var(--text); background: color-mix(in srgb, var(--text, #888) 8%, transparent); border-color: color-mix(in srgb, var(--text, #888) 24%, transparent); }
|
|
44
|
+
.session-github-link:focus-visible, .github-work-action:focus-visible { outline: 2px solid var(--accent, #58a6ff); outline-offset: 2px; }
|
|
45
|
+
.github-state-open .github-work-status svg { color: #3fb950; }
|
|
46
|
+
.github-state-merged .github-work-status svg, .github-state-closed .github-work-status svg { color: #a371f7; }
|
|
47
|
+
.github-state-draft .github-work-status svg, .github-state-unknown .github-work-status svg { color: var(--text-muted, #888); }
|
|
48
|
+
#header-github-links { min-width: 0; max-width: 200px; flex: 0 0 auto; margin-left: 4px; }
|
|
49
|
+
.github-work-details { flex-wrap: wrap; padding: 8px 0; gap: 8px 12px; }
|
|
50
|
+
.github-work-action { color: var(--text-muted, #888); text-decoration: none; font-size: 11px; }
|
|
51
|
+
.github-work-action:hover { text-decoration: underline; }
|
|
52
|
+
@media (max-width: 600px) {
|
|
53
|
+
#header-github-links { max-width: 82px; }
|
|
54
|
+
.mobile-session-item .session-github-link { min-height: 28px; padding-right: 4px; }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.session-item:has(.session-work-column) { height: 52px; }
|
|
58
|
+
|
|
59
|
+
.github-kind-pr.github-state-closed .github-work-status svg { color: #f85149; }
|
|
60
|
+
@media (max-width: 600px) {
|
|
61
|
+
#header-github-links { flex: 0 0 auto; }
|
|
62
|
+
#header-github-links .session-github-link { display: none; }
|
|
63
|
+
#header-github-links .session-github-link:first-child { display: inline-flex; }
|
|
64
|
+
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.github-work-brand, .github-work-status { display: inline-flex; align-items: center; flex: none; }
|
|
68
|
+
.github-work-brand { color: var(--text); opacity: .8; padding-right: 5px; border-right: 1px solid color-mix(in srgb, var(--text, #888) 14%, transparent); }
|
|
69
|
+
#header-github-links .github-work-brand { display: none; }
|
|
70
|
+
@media (max-width: 600px) {
|
|
71
|
+
#header-github-links .github-work-kind { display: none; }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.github-work-more {
|
|
75
|
+
flex: none;
|
|
76
|
+
border: 1px solid color-mix(in srgb, var(--text, #888) 12%, transparent);
|
|
77
|
+
background: transparent;
|
|
78
|
+
color: var(--text-muted, #888);
|
|
79
|
+
border-radius: 5px;
|
|
80
|
+
font: inherit;
|
|
81
|
+
line-height: 18px;
|
|
82
|
+
padding: 1px 5px;
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
}
|
|
85
|
+
.github-work-more:hover, .github-work-more[aria-expanded="true"] { background: color-mix(in srgb, var(--text, #888) 8%, transparent); color: var(--text); }
|
|
86
|
+
.github-work-more:focus-visible { outline: 2px solid var(--accent, #58a6ff); outline-offset: 2px; }
|
|
87
|
+
.github-work-popover {
|
|
88
|
+
position: fixed;
|
|
89
|
+
inset: auto;
|
|
90
|
+
margin: 0;
|
|
91
|
+
width: min(320px, calc(100vw - 16px));
|
|
92
|
+
max-height: min(420px, calc(100dvh - 16px));
|
|
93
|
+
overflow-y: auto;
|
|
94
|
+
padding: 12px;
|
|
95
|
+
box-sizing: border-box;
|
|
96
|
+
border: 1px solid var(--border, #444);
|
|
97
|
+
border-radius: 10px;
|
|
98
|
+
background: var(--bg-alt, #222);
|
|
99
|
+
color: var(--text, #eee);
|
|
100
|
+
box-shadow: 0 8px 30px #0003;
|
|
101
|
+
}
|
|
102
|
+
.github-work-popover > strong { display: block; font-size: 12px; margin-bottom: 10px; }
|
|
103
|
+
.github-work-popover .session-github-links { display: flex; flex-direction: column; align-items: stretch; overflow: visible; gap: 0; }
|
|
104
|
+
.github-work-entry { display: grid; grid-template-columns: 1fr; gap: 4px; padding: 8px 0; }
|
|
105
|
+
.github-work-entry + .github-work-entry { border-top: 1px solid var(--border, #444); }
|
|
106
|
+
.github-work-entry .session-github-link { justify-self: start; }
|
|
107
|
+
.github-work-entry-title { font-size: 12px; overflow-wrap: anywhere; }
|
|
108
|
+
.github-work-entry-meta { color: var(--text-muted, #888); font-size: 11px; overflow-wrap: anywhere; }
|
|
109
|
+
@media (max-width: 600px) {
|
|
110
|
+
#header-github-links { max-width: 120px; }
|
|
111
|
+
.mobile-session-item .github-work-more { min-height: 28px; }
|
|
112
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { syncGithubSessions } from './session-github.js';
|
|
1
2
|
import { handleIssuesMessageInContext as handleIssuesMessage } from './issues-context.js';
|
|
2
3
|
// app-messages.js - WebSocket message router
|
|
3
4
|
// Extracted from app.js (PR-23)
|
|
@@ -711,6 +712,7 @@ export function processMessage(msg) {
|
|
|
711
712
|
break;
|
|
712
713
|
|
|
713
714
|
case "session_list":
|
|
715
|
+
syncGithubSessions(msg.sessions || []);
|
|
714
716
|
markProjectSessionListHydrated();
|
|
715
717
|
renderMateSessionList(msg.sessions || []);
|
|
716
718
|
renderSessionList(msg.sessions || []);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { currentGithubWorkMarkup, refreshGithubWork, resetGithubWork } from './session-github.js';
|
|
1
2
|
import { store } from './store.js';
|
|
2
3
|
import { refreshIcons, iconHtml } from './icons.js';
|
|
3
4
|
import { escapeHtml, showToast } from './utils.js';
|
|
@@ -205,7 +206,7 @@ function render() {
|
|
|
205
206
|
} else if (!currentStatus.isRepository) {
|
|
206
207
|
body.innerHTML = '<div class="git-panel-empty">' + iconHtml("folder-git-2") + '<span>This project is not a Git repository.</span></div>';
|
|
207
208
|
} else {
|
|
208
|
-
body.innerHTML = renderRepository(currentStatus);
|
|
209
|
+
body.innerHTML = currentGithubWorkMarkup() + renderRepository(currentStatus);
|
|
209
210
|
}
|
|
210
211
|
refreshIcons(body);
|
|
211
212
|
var changesList = body.querySelector(".git-section-changes .git-file-list");
|
|
@@ -252,6 +253,7 @@ export function refreshGitSummary() {
|
|
|
252
253
|
// Project switch and reconnect both land here, so no stale repository ever
|
|
253
254
|
// survives into another project and nothing flashes before detection lands.
|
|
254
255
|
export function resetGitSurface() {
|
|
256
|
+
resetGithubWork();
|
|
255
257
|
currentStatus = null;
|
|
256
258
|
currentSummary = null;
|
|
257
259
|
lastError = "";
|
|
@@ -263,6 +265,7 @@ export function resetGitSurface() {
|
|
|
263
265
|
}
|
|
264
266
|
|
|
265
267
|
export function refreshGitStatus() {
|
|
268
|
+
refreshGithubWork();
|
|
266
269
|
if (busy) return Promise.resolve(currentStatus);
|
|
267
270
|
var basePath = store.get('basePath');
|
|
268
271
|
if (statusBasePath !== basePath) {
|
|
@@ -399,7 +402,7 @@ function initGitLifecycle() {
|
|
|
399
402
|
return;
|
|
400
403
|
}
|
|
401
404
|
if (state.dmMode !== prev.dmMode) {
|
|
402
|
-
if (state.dmMode) hideGitPlacard();
|
|
405
|
+
if (state.dmMode) { hideGitPlacard(); resetGithubWork(); }
|
|
403
406
|
else refreshGitSummary();
|
|
404
407
|
return;
|
|
405
408
|
}
|
|
@@ -478,6 +481,7 @@ export function initGitPanel() {
|
|
|
478
481
|
if (summaryTicks < SUMMARY_TICKS) return;
|
|
479
482
|
summaryTicks = 0;
|
|
480
483
|
if (document.hidden || store.get('dmMode') || !store.get('connected') || busy) return;
|
|
484
|
+
refreshGithubWork();
|
|
481
485
|
refreshGitSummary();
|
|
482
486
|
}, POLL_MS);
|
|
483
487
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { githubWorkMarkup } from './session-github.js';
|
|
1
2
|
import { escapeHtml } from './utils.js';
|
|
2
3
|
import { renderMarkdown } from './markdown.js';
|
|
3
4
|
import { authorLine, relativeTime } from './project-logs-render.js';
|
|
@@ -34,6 +35,7 @@ export function renderIssueDetail(entry) {
|
|
|
34
35
|
var time = entry.updatedAt || entry.createdAt;
|
|
35
36
|
var byline = [authorLine(entry), time ? new Date(time).toLocaleString() : '', 'v' + entry.revision].filter(Boolean).join(' · ');
|
|
36
37
|
return '<article class="issue-detail"><div class="issue-doc-chips"><span class="issue-type">' + esc(entry.type) + '</span>' + priorityChip(entry.priority) + statusChip(entry.status) + '</div>' +
|
|
38
|
+
githubWorkMarkup(entry, true) +
|
|
37
39
|
'<h2 class="issue-doc-title">' + esc(entry.title) + '</h2>' + prose(entry.summary, 'issue-doc-summary') + '<div class="issue-byline">' + esc(byline) + '</div>' +
|
|
38
40
|
'<div class="issue-actions">' + action('data-issue-work', 'play', 'Start work', ' class="issue-primary"' + (closed ? ' disabled title="Reopen this issue to start work"' : '')) +
|
|
39
41
|
action('data-issue-edit', 'pencil', 'Edit') + action('data-issue-history', 'history', 'History') + action('data-issue-copy', 'link', 'Copy reference') +
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { getWs } from './ws-ref.js';
|
|
3
|
+
import { githubLinkTarget } from './github-links.js';
|
|
4
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
5
|
+
import { escapeHtml as escapeText } from './utils.js';
|
|
6
|
+
|
|
7
|
+
function escapeHtml(value) {
|
|
8
|
+
return escapeText(String(value)).replace(/"/g, '"').replace(/'/g, ''');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function safeLinks(session) {
|
|
12
|
+
return (session && Array.isArray(session.githubLinks) ? session.githubLinks : []).filter(function (link) {
|
|
13
|
+
var target = githubLinkTarget(link.url);
|
|
14
|
+
return target && (target.kind === 'issue' || target.kind === 'pull');
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function label(link) {
|
|
18
|
+
return (link.kind === 'pr' ? 'PR #' : 'Issue #') + link.number;
|
|
19
|
+
}
|
|
20
|
+
function anchor(url, title, content, className) {
|
|
21
|
+
return '<a class="' + className + '" href="' + escapeHtml(url) + '" target="_blank" rel="noopener noreferrer" title="' + escapeHtml(title) + '" aria-label="' + escapeHtml(title) + '">' + content + '</a>';
|
|
22
|
+
}
|
|
23
|
+
export function orderedGithubWork(session) {
|
|
24
|
+
var links = safeLinks(session).slice().reverse();
|
|
25
|
+
var latestIssue = links.find(function (link) { return link.kind === 'issue'; });
|
|
26
|
+
if (latestIssue) links = [latestIssue].concat(links.filter(function (link) { return link !== latestIssue; }));
|
|
27
|
+
return links;
|
|
28
|
+
}
|
|
29
|
+
export function githubWorkMarkup(session, detailed, expanded) {
|
|
30
|
+
var links = orderedGithubWork(session);
|
|
31
|
+
if (!links.length) return '';
|
|
32
|
+
return '<span class="session-github-links' + (detailed ? ' github-work-details' : '') + '">' + (detailed || expanded ? links : links.slice(0, 1)).map(function (link) {
|
|
33
|
+
var state = ['open', 'closed', 'merged', 'draft'].indexOf(link.state) >= 0 ? link.state : 'unknown';
|
|
34
|
+
var icon = link.kind === 'pr' ? (state === 'merged' ? 'git-merge' : state === 'closed' ? 'git-pull-request-closed' : state === 'draft' ? 'git-pull-request-draft' : 'git-pull-request') : (state === 'closed' ? 'circle-check' : 'circle-dot');
|
|
35
|
+
var description = link.repository + ' · ' + label(link) + ' · ' + link.title + ' · ' + state + (link.checkedAt ? ' · Checked ' + new Date(link.checkedAt).toLocaleString() : '') + (link.stale ? ' (last known status; refresh unavailable)' : '');
|
|
36
|
+
var html = anchor(link.url, description, iconHtml('github', 'github-work-brand') + iconHtml(icon, 'github-work-status') + '<span><span class="github-work-kind">' + (link.kind === 'pr' ? 'PR ' : 'Issue ') + '</span>#' + escapeHtml(String(link.number)) + '</span>', 'session-github-link github-kind-' + (link.kind === 'pr' ? 'pr' : 'issue') + ' github-state-' + (link.stale ? 'unknown' : state));
|
|
37
|
+
if (expanded) html = '<span class="github-work-entry">' + html + '<span class="github-work-entry-title">' + escapeHtml(link.title || '') + '</span><span class="github-work-entry-meta">' + escapeHtml(link.repository + ' · ' + state + (link.stale ? ' · Last known status' : '')) + '</span></span>';
|
|
38
|
+
if (detailed && link.kind === 'pr') {
|
|
39
|
+
html += anchor(link.url + '/files', 'View PR changes on GitHub', 'Changes', 'github-work-action');
|
|
40
|
+
html += anchor(link.url + '#pullrequestreview', 'View PR reviews on GitHub', escapeHtml((link.review || 'Reviews').replace(/_/g, ' ').toLowerCase()), 'github-work-action');
|
|
41
|
+
(link.checks || []).forEach(function (check) {
|
|
42
|
+
if (/^https:\/\//i.test(check.url)) html += anchor(check.url, check.name + ': ' + check.state, escapeHtml(check.name + ' · ' + check.state.toLowerCase()), 'github-work-action');
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return html;
|
|
46
|
+
}).join('') + (!detailed && !expanded && links.length > 1 ? '<button type="button" class="github-work-more" aria-label="Show all ' + links.length + ' linked GitHub items" aria-haspopup="dialog" aria-expanded="false" data-github-links="' + escapeHtml(JSON.stringify(safeLinks(session))) + '">+' + (links.length - 1) + '</button>' : '') + '</span>';
|
|
47
|
+
}
|
|
48
|
+
export function appendGithubWork(row, session, title) {
|
|
49
|
+
var html = githubWorkMarkup(session, false);
|
|
50
|
+
bindGithubWorkPopover();
|
|
51
|
+
if (!html) return;
|
|
52
|
+
var column = document.createElement('span');
|
|
53
|
+
column.className = 'session-work-column';
|
|
54
|
+
row.insertBefore(column, title);
|
|
55
|
+
column.appendChild(title);
|
|
56
|
+
var links = document.createElement('span');
|
|
57
|
+
links.innerHTML = html;
|
|
58
|
+
links.addEventListener('click', function (event) { if (event.target.closest('a')) event.stopPropagation(); });
|
|
59
|
+
links.addEventListener('contextmenu', function (event) { event.stopPropagation(); });
|
|
60
|
+
column.appendChild(links);
|
|
61
|
+
}
|
|
62
|
+
export function syncGithubSessions(sessions) {
|
|
63
|
+
bindGithubWorkPopover();
|
|
64
|
+
store.set({ githubWorkSessions: sessions || [], githubWorkProject: store.get('currentSlug') });
|
|
65
|
+
var active = (sessions || []).find(function (session) { return session.active; });
|
|
66
|
+
var title = document.getElementById('header-title');
|
|
67
|
+
var old = document.getElementById('header-github-links');
|
|
68
|
+
var html = title && !store.get('dmMode') && !store.get('splitPanes') && active ? githubWorkMarkup(active, false) : '';
|
|
69
|
+
if (!html) { if (old) old.remove(); }
|
|
70
|
+
else if (!old || old.dataset.markup !== html) {
|
|
71
|
+
var element = old || document.createElement('span');
|
|
72
|
+
element.id = 'header-github-links';
|
|
73
|
+
element.dataset.markup = html;
|
|
74
|
+
element.innerHTML = html;
|
|
75
|
+
if (!old) title.insertAdjacentElement('afterend', element);
|
|
76
|
+
}
|
|
77
|
+
refreshIcons();
|
|
78
|
+
refreshGithubWork();
|
|
79
|
+
}
|
|
80
|
+
export function currentGithubWorkMarkup() {
|
|
81
|
+
if (store.get('githubWorkProject') !== store.get('currentSlug')) return '';
|
|
82
|
+
var session = (store.get('githubWorkSessions') || []).find(function (item) { return item.active; });
|
|
83
|
+
return githubWorkMarkup(session, true);
|
|
84
|
+
}
|
|
85
|
+
export function refreshGithubWork() {
|
|
86
|
+
if (document.hidden || store.get('dmMode') || !store.get('connected')) return;
|
|
87
|
+
if (store.get('githubWorkProject') !== store.get('currentSlug')) return;
|
|
88
|
+
var session = (store.get('githubWorkSessions') || []).find(function (item) { return item.active; });
|
|
89
|
+
if (!safeLinks(session).length) return;
|
|
90
|
+
var key = store.get('currentSlug') + ':' + session.id;
|
|
91
|
+
var previous = store.get('githubWorkRefresh') || {};
|
|
92
|
+
if (previous.key === key && Date.now() - previous.at < 60000) return;
|
|
93
|
+
var ws = getWs();
|
|
94
|
+
if (!ws || ws.readyState !== 1) return;
|
|
95
|
+
store.set({ githubWorkRefresh: { key: key, at: Date.now() } });
|
|
96
|
+
ws.send(JSON.stringify({ type: 'session_github_refresh' }));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function resetGithubWork() {
|
|
100
|
+
closeGithubWorkPopover();
|
|
101
|
+
store.set({ githubWorkSessions: [], githubWorkProject: null, githubWorkRefresh: null });
|
|
102
|
+
var header = document.getElementById('header-github-links');
|
|
103
|
+
if (header) header.remove();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function closeGithubWorkPopover() {
|
|
107
|
+
var popup = document.getElementById('github-work-popover');
|
|
108
|
+
if (popup) popup.hidePopover();
|
|
109
|
+
}
|
|
110
|
+
function bindGithubWorkPopover() {
|
|
111
|
+
if (store.get('githubWorkPopoverBound')) return;
|
|
112
|
+
store.set({ githubWorkPopoverBound: true });
|
|
113
|
+
document.addEventListener('click', function (event) {
|
|
114
|
+
var button = event.target.closest('.github-work-more');
|
|
115
|
+
if (!button) return;
|
|
116
|
+
event.preventDefault();
|
|
117
|
+
event.stopPropagation();
|
|
118
|
+
var existing = document.getElementById('github-work-popover');
|
|
119
|
+
var same = existing && button.getAttribute('aria-expanded') === 'true';
|
|
120
|
+
if (existing) { existing.hidePopover(); existing.remove(); }
|
|
121
|
+
if (same) return;
|
|
122
|
+
var links;
|
|
123
|
+
try { links = JSON.parse(button.dataset.githubLinks); } catch (error) { return; }
|
|
124
|
+
var popup = document.createElement('div');
|
|
125
|
+
popup.id = 'github-work-popover';
|
|
126
|
+
popup.className = 'github-work-popover';
|
|
127
|
+
popup.setAttribute('popover', 'auto');
|
|
128
|
+
popup.setAttribute('role', 'dialog');
|
|
129
|
+
popup.setAttribute('aria-label', 'Linked GitHub work');
|
|
130
|
+
popup.innerHTML = '<strong>Linked GitHub work</strong>' + githubWorkMarkup({ githubLinks: links }, false, true);
|
|
131
|
+
document.body.appendChild(popup);
|
|
132
|
+
button.setAttribute('aria-expanded', 'true');
|
|
133
|
+
popup.addEventListener('toggle', function (toggle) {
|
|
134
|
+
if (toggle.newState === 'closed') {
|
|
135
|
+
button.setAttribute('aria-expanded', 'false');
|
|
136
|
+
if (document.activeElement === document.body && button.isConnected) button.focus();
|
|
137
|
+
popup.remove();
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
popup.showPopover();
|
|
141
|
+
var rect = button.getBoundingClientRect();
|
|
142
|
+
var box = popup.getBoundingClientRect();
|
|
143
|
+
popup.style.left = Math.max(8, Math.min(rect.left, window.innerWidth - box.width - 8)) + 'px';
|
|
144
|
+
popup.style.top = Math.max(8, rect.bottom + box.height + 8 <= window.innerHeight ? rect.bottom + 6 : rect.top - box.height - 6) + 'px';
|
|
145
|
+
var first = popup.querySelector('a');
|
|
146
|
+
if (first) first.focus({ preventScroll: true });
|
|
147
|
+
refreshIcons();
|
|
148
|
+
}, true);
|
|
149
|
+
window.addEventListener('resize', closeGithubWorkPopover);
|
|
150
|
+
store.subscribe(function (state, previous) {
|
|
151
|
+
if (state.currentSlug !== previous.currentSlug || state.activeSessionId !== previous.activeSessionId || state.dmMode !== previous.dmMode) closeGithubWorkPopover();
|
|
152
|
+
});
|
|
153
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { appendGithubWork } from './session-github.js';
|
|
1
2
|
// sidebar-mobile.js - Mobile sheet overlays, tab bar, and mobile-specific rendering
|
|
2
3
|
// Extracted from sidebar.js (PR-38)
|
|
3
4
|
|
|
@@ -384,7 +385,7 @@ function renderSheetSessions(listEl) {
|
|
|
384
385
|
// Helper: create a mobile session item element
|
|
385
386
|
function createMobileSessionItem(s, options) {
|
|
386
387
|
var itemOptions = options || {};
|
|
387
|
-
var el = document.createElement("
|
|
388
|
+
var el = document.createElement("div");
|
|
388
389
|
el.className = "mobile-session-item" + (s.active ? " active" : "");
|
|
389
390
|
if (itemOptions.worker) el.classList.add("mobile-worker-item");
|
|
390
391
|
|
|
@@ -404,9 +405,15 @@ function createMobileSessionItem(s, options) {
|
|
|
404
405
|
}
|
|
405
406
|
|
|
406
407
|
var titleSpan = document.createElement("span");
|
|
408
|
+
titleSpan.tabIndex = 0;
|
|
409
|
+
titleSpan.setAttribute("role", "button");
|
|
410
|
+
titleSpan.addEventListener("keydown", function (event) {
|
|
411
|
+
if (event.key === "Enter" || event.key === " ") { event.preventDefault(); el.click(); }
|
|
412
|
+
});
|
|
407
413
|
titleSpan.className = "mobile-session-title";
|
|
408
414
|
titleSpan.appendChild(document.createTextNode(s.title || "New Session"));
|
|
409
415
|
el.appendChild(titleSpan);
|
|
416
|
+
appendGithubWork(el, s, titleSpan);
|
|
410
417
|
|
|
411
418
|
if (itemOptions.worker) {
|
|
412
419
|
var generation = document.createElement("span");
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { appendGithubWork } from './session-github.js';
|
|
1
2
|
// sidebar-sessions.js - Session list, search, presence, countdown, CLI picker
|
|
2
3
|
// Extracted from sidebar.js (PR-35)
|
|
3
4
|
|
|
@@ -1367,6 +1368,7 @@ function renderSessionItem(s, options) {
|
|
|
1367
1368
|
textHtml += highlightMatch(s.title || "New Session", searchQuery);
|
|
1368
1369
|
textSpan.innerHTML = textHtml;
|
|
1369
1370
|
el.appendChild(textSpan);
|
|
1371
|
+
appendGithubWork(el, s, textSpan);
|
|
1370
1372
|
|
|
1371
1373
|
if (itemOptions.worker) {
|
|
1372
1374
|
var workerLabel = document.createElement("span");
|
package/lib/public/style.css
CHANGED
package/lib/sdk-bridge.js
CHANGED
|
@@ -567,6 +567,17 @@ function createSDKBridge(opts) {
|
|
|
567
567
|
return { behavior: "allow", updatedInput: input };
|
|
568
568
|
}
|
|
569
569
|
|
|
570
|
+
// Session search is read-only; its handler enforces project and session access.
|
|
571
|
+
if (toolName === "search_other_driver_sessions"
|
|
572
|
+
|| toolName === "mcp__clay-handoff__search_other_driver_sessions") {
|
|
573
|
+
return { behavior: "allow", updatedInput: input };
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Local work associations verify GitHub data but never mutate GitHub.
|
|
577
|
+
if (["link_session_github", "get_session_github", "unlink_session_github"].some(function (name) {
|
|
578
|
+
return toolName === name || toolName === "mcp__clay-github__" + name;
|
|
579
|
+
})) return { behavior: "allow", updatedInput: input };
|
|
580
|
+
|
|
570
581
|
// Auto-approve safe browser MCP tools.
|
|
571
582
|
// Only watch/unwatch: user explicitly chose which tab to share.
|
|
572
583
|
// Everything else (screenshot, read_page, list_tabs, etc.) can expose
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Verified GitHub references owned by a work session, never inferred from mentions.
|
|
2
|
+
var execFile = require("child_process").execFile;
|
|
3
|
+
var wrapSpawnAsUser = require("./os-users").wrapSpawnAsUser;
|
|
4
|
+
|
|
5
|
+
function parseReference(value) {
|
|
6
|
+
var match = typeof value === "string" && value.match(/^https:\/\/github\.com\/([a-zA-Z0-9][a-zA-Z0-9-]{0,38})\/([a-zA-Z0-9][a-zA-Z0-9._-]{0,99})\/(issues|pull)\/([1-9][0-9]*)(?:[?#].*)?$/);
|
|
7
|
+
if (!match || !Number.isSafeInteger(Number(match[4]))) throw new Error("Use a GitHub issue or pull request URL.");
|
|
8
|
+
return { repository: match[1] + "/" + match[2], kind: match[3] === "pull" ? "pr" : "issue", number: Number(match[4]), url: "https://github.com/" + match[1] + "/" + match[2] + "/" + match[3] + "/" + match[4] };
|
|
9
|
+
}
|
|
10
|
+
function runGh(cwd, args, identity) {
|
|
11
|
+
return new Promise(function (resolve, reject) {
|
|
12
|
+
var env = Object.assign({}, identity ? require("./build-user-env").buildUserEnv(identity) : process.env, { GH_PROMPT_DISABLED: "1", GH_HOST: "github.com" });
|
|
13
|
+
if (identity) {
|
|
14
|
+
// Never let the daemon's GitHub credentials cross an OS-user boundary.
|
|
15
|
+
delete env.GH_TOKEN; delete env.GITHUB_TOKEN; delete env.GH_ENTERPRISE_TOKEN; delete env.GITHUB_ENTERPRISE_TOKEN;
|
|
16
|
+
delete env.GH_CONFIG_DIR; delete env.XDG_CONFIG_HOME;
|
|
17
|
+
env.HOME = identity.home; env.USER = identity.user; env.LOGNAME = identity.user;
|
|
18
|
+
}
|
|
19
|
+
var options = { cwd: cwd, env: env, timeout: 15000, maxBuffer: 512 * 1024, encoding: "utf8" };
|
|
20
|
+
if (identity) { options.uid = identity.uid; options.gid = identity.gid; }
|
|
21
|
+
var wrapped = wrapSpawnAsUser("gh", args, options);
|
|
22
|
+
execFile(wrapped.command, wrapped.args, wrapped.options, function (error, stdout) {
|
|
23
|
+
if (error) return reject(new Error("GitHub is unavailable. Check gh authentication and repository access."));
|
|
24
|
+
try { resolve(JSON.parse(stdout)); } catch (e) { reject(new Error("GitHub returned an invalid response.")); }
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
async function readReference(cwd, url, identity, run) {
|
|
29
|
+
var ref = parseReference(url);
|
|
30
|
+
var fields = "title,state,url" + (ref.kind === "pr" ? ",isDraft,headRefName,reviewDecision,statusCheckRollup" : "");
|
|
31
|
+
var data = await (run || runGh)(cwd, [ref.kind === "pr" ? "pr" : "issue", "view", String(ref.number), "--repo", ref.repository, "--json", fields], identity);
|
|
32
|
+
var verified = parseReference(data.url);
|
|
33
|
+
if (verified.url.toLowerCase() !== ref.url.toLowerCase()) throw new Error("GitHub returned a different reference.");
|
|
34
|
+
ref.title = String(data.title || "").slice(0, 200);
|
|
35
|
+
ref.state = data.state === "MERGED" ? "merged" : data.state === "CLOSED" ? "closed" : data.state === "OPEN" ? (data.isDraft ? "draft" : "open") : "unknown";
|
|
36
|
+
ref.checkedAt = Date.now();
|
|
37
|
+
if (ref.kind === "pr") {
|
|
38
|
+
ref.branch = String(data.headRefName || "").slice(0, 200);
|
|
39
|
+
ref.review = ["APPROVED", "CHANGES_REQUESTED", "REVIEW_REQUIRED"].indexOf(data.reviewDecision) >= 0 ? data.reviewDecision : "";
|
|
40
|
+
ref.checks = (data.statusCheckRollup || []).slice(0, 30).map(function (check) {
|
|
41
|
+
var target = check.detailsUrl || check.targetUrl || "";
|
|
42
|
+
if (!/^https:\/\//i.test(target)) target = "";
|
|
43
|
+
return { name: String(check.name || check.context || "Check").slice(0, 100), state: String(check.conclusion || check.state || check.status || "PENDING").slice(0, 40), url: target };
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return ref;
|
|
47
|
+
}
|
|
48
|
+
module.exports = { parseReference: parseReference, readReference: readReference };
|
package/lib/sessions.js
CHANGED
|
@@ -176,6 +176,7 @@ function createSessionManager(opts) {
|
|
|
176
176
|
titleManuallySet: session.titleManuallySet === true,
|
|
177
177
|
createdAt: session.createdAt,
|
|
178
178
|
};
|
|
179
|
+
if (session.githubLinks) metaObj.githubLinks = session.githubLinks;
|
|
179
180
|
if (session.ownerId) metaObj.ownerId = session.ownerId;
|
|
180
181
|
if (session.vendor) metaObj.vendor = session.vendor;
|
|
181
182
|
if (session.codexDynamicToolCatalogVersion) metaObj.codexDynamicToolCatalogVersion = session.codexDynamicToolCatalogVersion;
|
|
@@ -343,6 +344,7 @@ function createSessionManager(opts) {
|
|
|
343
344
|
title: m.title || "",
|
|
344
345
|
titleProvisional: m.titleProvisional === true,
|
|
345
346
|
titleAutoGenerated: m.titleAutoGenerated === true,
|
|
347
|
+
githubLinks: Array.isArray(m.githubLinks) ? m.githubLinks.slice(0, 8) : [],
|
|
346
348
|
titleManuallySet: m.titleManuallySet === true || legacyTitleProtected,
|
|
347
349
|
createdAt: m.createdAt || Date.now(),
|
|
348
350
|
lastActivity: loaded[i].mtime || m.createdAt || Date.now(),
|
|
@@ -604,6 +606,7 @@ function createSessionManager(opts) {
|
|
|
604
606
|
id: s.localId,
|
|
605
607
|
cliSessionId: s.cliSessionId || null,
|
|
606
608
|
title: s.title || "New Session",
|
|
609
|
+
githubLinks: s.githubLinks || [],
|
|
607
610
|
active: isActive,
|
|
608
611
|
isProcessing: s.isProcessing,
|
|
609
612
|
// Sidebar badge: user work only. Ambient housekeeping is surfaced in the
|
package/package.json
CHANGED