clay-server 4.0.0-beta.15 → 4.0.0-beta.17
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/capsule-frame-server.js +26 -0
- package/lib/capsule-mate-turn.js +105 -0
- package/lib/capsule-pig-logic.js +33 -1
- package/lib/capsule-server-runtimes.js +0 -2
- package/lib/capsules/pig/display.js +8 -0
- package/lib/capsules/pig/manifest.json +1 -1
- package/lib/capsules/pig/ui.json +52 -14
- package/lib/daemon-projects.js +10 -5
- package/lib/daemon.js +69 -10
- package/lib/migrate-single-user.js +2 -0
- package/lib/project-capsule-turn.js +96 -0
- package/lib/project-log-context.js +171 -0
- package/lib/project-log-feedback-delivery.js +47 -5
- package/lib/project-logs-context-state.js +42 -0
- package/lib/project-logs-mcp-server.js +16 -10
- package/lib/project-logs-query.js +18 -0
- package/lib/project-logs-root.js +8 -0
- package/lib/project-logs-service.js +92 -15
- package/lib/project-logs-snapshot.js +3 -0
- package/lib/project-logs-store.js +29 -32
- package/lib/project-logs-versioning.js +6 -2
- package/lib/project-logs.js +56 -12
- package/lib/project-message-delivery.js +70 -0
- package/lib/project-session-pair.js +15 -5
- package/lib/project-user-message.js +11 -0
- package/lib/project-worker-proposal.js +8 -8
- package/lib/project.js +18 -1
- package/lib/public/app.js +11 -15
- package/lib/public/css/capsule-ui.css +21 -1
- package/lib/public/css/home-hub.css +4 -0
- package/lib/public/css/input.css +84 -3
- package/lib/public/css/project-logs.css +20 -2
- package/lib/public/css/session-actions.css +16 -101
- package/lib/public/css/sticky-notes.css +3 -7
- package/lib/public/index.html +31 -9
- package/lib/public/modules/app-connection.js +22 -3
- package/lib/public/modules/app-message-cards.js +2 -1
- package/lib/public/modules/app-messages.js +39 -3
- package/lib/public/modules/app-misc.js +2 -14
- package/lib/public/modules/app-panels.js +3 -2
- package/lib/public/modules/app-projects.js +6 -3
- package/lib/public/modules/app-rendering.js +1 -0
- package/lib/public/modules/capsule-preference.js +44 -0
- package/lib/public/modules/context-sources.js +0 -3
- package/lib/public/modules/home-chat-empty-state.js +39 -0
- package/lib/public/modules/home-chat-identity.js +9 -0
- package/lib/public/modules/home-mate-chat.js +15 -35
- package/lib/public/modules/home-tool-frame.js +17 -0
- package/lib/public/modules/home-tools.js +9 -0
- package/lib/public/modules/input.js +38 -7
- package/lib/public/modules/message-delivery.js +97 -0
- package/lib/public/modules/paste-modal.js +84 -0
- package/lib/public/modules/project-logs-render.js +48 -2
- package/lib/public/modules/project-logs.js +133 -11
- package/lib/public/modules/project-removal-target.js +26 -0
- package/lib/public/modules/session-actions.js +29 -105
- package/lib/public/modules/user-settings.js +4 -0
- package/lib/public/modules/worker-pane-lock.js +2 -0
- package/lib/sdk-bridge.js +14 -7
- package/lib/server-experimental-settings.js +49 -0
- package/lib/server-home-chat-events.js +28 -1
- package/lib/server-home-chat.js +1 -1
- package/lib/server-settings.js +5 -0
- package/lib/server-tools.js +30 -0
- package/lib/server.js +41 -2
- package/lib/session-driver-orchestration.js +31 -0
- package/lib/session-pair-prompts.js +24 -5
- package/lib/tools-registry.js +46 -1
- package/lib/users-experimental-preferences.js +30 -0
- package/lib/users.js +6 -0
- package/lib/ws-schema.js +8 -2
- package/lib/yoke/adapters/codex.js +24 -14
- package/lib/yoke/vendor-registry.js +1 -1
- package/package.json +2 -2
- package/lib/capsule-tictactoe-logic.js +0 -260
- package/lib/capsules/tictactoe/manifest.json +0 -9
- package/lib/capsules/tictactoe/ui.json +0 -203
|
@@ -132,8 +132,34 @@ function createCapsuleFrameServer(opts) {
|
|
|
132
132
|
" if (data.type === \"state\" && typeof api.onState === \"function\") api.onState(data.state || {});",
|
|
133
133
|
" else if (data.type === \"event\" && typeof api.onEvent === \"function\") api.onEvent(data.event || null);",
|
|
134
134
|
" });",
|
|
135
|
+
" // The host owns the iframe box, so the frame reports its natural",
|
|
136
|
+
" // content height and the host resizes the box to fit. Without this",
|
|
137
|
+
" // the box stays fixed and a widening layout clips the element.",
|
|
138
|
+
" var reportedHeight = 0;",
|
|
139
|
+
" var reportQueued = false;",
|
|
140
|
+
" function reportSize() {",
|
|
141
|
+
" reportQueued = false;",
|
|
142
|
+
" var root = document.documentElement;",
|
|
143
|
+
" var body = document.body;",
|
|
144
|
+
" var height = Math.ceil(Math.max(root ? root.scrollHeight : 0, body ? body.scrollHeight : 0));",
|
|
145
|
+
" if (!height || height === reportedHeight) return;",
|
|
146
|
+
" reportedHeight = height;",
|
|
147
|
+
" window.parent.postMessage({ clayCapsuleFrame: 1, type: \"size\", height: height }, \"*\");",
|
|
148
|
+
" }",
|
|
149
|
+
" function queueReport() {",
|
|
150
|
+
" if (reportQueued) return;",
|
|
151
|
+
" reportQueued = true;",
|
|
152
|
+
" if (typeof requestAnimationFrame === \"function\") requestAnimationFrame(reportSize);",
|
|
153
|
+
" else setTimeout(reportSize, 50);",
|
|
154
|
+
" }",
|
|
155
|
+
" if (typeof ResizeObserver === \"function\") {",
|
|
156
|
+
" new ResizeObserver(queueReport).observe(document.documentElement);",
|
|
157
|
+
" if (document.body) new ResizeObserver(queueReport).observe(document.body);",
|
|
158
|
+
" }",
|
|
159
|
+
" window.addEventListener(\"resize\", queueReport);",
|
|
135
160
|
" window.addEventListener(\"load\", function () {",
|
|
136
161
|
" window.parent.postMessage({ clayCapsuleFrame: 1, type: \"ready\" }, \"*\");",
|
|
162
|
+
" queueReport();",
|
|
137
163
|
" });",
|
|
138
164
|
"})();",
|
|
139
165
|
].join("\n");
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Bridges Capsule engagement events to the opponent Mate's session.
|
|
2
|
+
//
|
|
3
|
+
// This module knows no game. A Capsule's Logic declares on its causal event
|
|
4
|
+
// when the Mate should be engaged (`event.engage = {kind}`): "turn" asks the
|
|
5
|
+
// Mate to read the state and act, "start" asks it to acknowledge a fresh
|
|
6
|
+
// game without acting. The bridge only carries that declaration: it finds the
|
|
7
|
+
// opponent Mate's project and delivers a prompt through it, so the Capsule
|
|
8
|
+
// round-trips for every game alike: the human plays on the Display, the Mate
|
|
9
|
+
// wakes up, reads the state over clay_tool_snapshot, and acts through the
|
|
10
|
+
// same clay_tool_act pipeline. The prompt carries words only; it grants no
|
|
11
|
+
// authority the Mate does not already have, and a lost prompt costs a
|
|
12
|
+
// reminder, never the game. Only human-caused events engage, whatever Logic
|
|
13
|
+
// declares, so a Mate can never wake itself.
|
|
14
|
+
//
|
|
15
|
+
// The opponent is the Mate that last acted on this Capsule for this user.
|
|
16
|
+
// Before any Mate has acted, the built-in host Mate (Clay) takes the seat, so
|
|
17
|
+
// a fresh game is playable with zero setup.
|
|
18
|
+
|
|
19
|
+
function attachCapsuleMateTurn(deps) {
|
|
20
|
+
var users = deps.users;
|
|
21
|
+
var findMateProject = deps.findMateProject || null;
|
|
22
|
+
var broadcastToUser = deps.broadcastToUser || null;
|
|
23
|
+
var opponents = Object.create(null);
|
|
24
|
+
var nudgedSeqs = Object.create(null);
|
|
25
|
+
|
|
26
|
+
function keyFor(userId, toolId) {
|
|
27
|
+
return String(userId) + "\u0000" + String(toolId);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Called when a Mate acts on a Capsule: that Mate holds the opponent seat
|
|
31
|
+
// for this user's game from then on.
|
|
32
|
+
function rememberOpponent(userId, toolId, mateId) {
|
|
33
|
+
if (typeof mateId === "string" && mateId) opponents[keyFor(userId, toolId)] = mateId;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function turnPrompt(manifest) {
|
|
37
|
+
return "Capsule turn: it is your seat's turn in the \"" + manifest.name + "\" Capsule (toolId \"" + manifest.id + "\"). "
|
|
38
|
+
+ "Read the game with clay_tool_snapshot, decide your move, and take your turn with clay_tool_act. "
|
|
39
|
+
+ "Keep playing until the turn passes away from your seat, and say your reasoning in one or two short sentences as you play. "
|
|
40
|
+
+ "If an act is refused, read the snapshot again instead of retrying blindly.";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function startPrompt(manifest) {
|
|
44
|
+
return "Capsule game: the user just started a new game of \"" + manifest.name + "\" (toolId \"" + manifest.id + "\") against you. "
|
|
45
|
+
+ "The user moves first. Reply with one short line of table talk, and do not call clay_tool_act now: "
|
|
46
|
+
+ "you will be woken in this session whenever it is your turn.";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Fire-and-forget from the act pipeline: delivers a Logic-declared
|
|
50
|
+
// engagement exactly once per event, and only for human-caused events.
|
|
51
|
+
function maybeNudge(userId, manifest, actor, event) {
|
|
52
|
+
if (!findMateProject || !manifest || !event) return Promise.resolve(null);
|
|
53
|
+
if (actor !== "human") return Promise.resolve(null);
|
|
54
|
+
var kind = event.engage && (event.engage.kind === "turn" || event.engage.kind === "start") ? event.engage.kind : null;
|
|
55
|
+
if (!kind) return Promise.resolve(null);
|
|
56
|
+
var key = keyFor(userId, manifest.id);
|
|
57
|
+
if (nudgedSeqs[key] !== undefined && event.seq <= nudgedSeqs[key]) return Promise.resolve(null);
|
|
58
|
+
nudgedSeqs[key] = event.seq;
|
|
59
|
+
return Promise.resolve().then(function () {
|
|
60
|
+
var mateId = opponents[key] || null;
|
|
61
|
+
// The mates registry keys single-user data off a null userId, exactly
|
|
62
|
+
// like the home chat surface does.
|
|
63
|
+
var mateUserId = users.isMultiUser() ? userId : null;
|
|
64
|
+
var found = findMateProject(mateUserId, mateId, true);
|
|
65
|
+
if (!found && mateId) found = findMateProject(mateUserId, null, true);
|
|
66
|
+
if (!found || !found.ctx || typeof found.ctx.deliverCapsuleTurn !== "function") {
|
|
67
|
+
console.error("[capsules] No Mate project could take the '" + manifest.id + "' turn; is a Mate installed?");
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
var principal = { userId: users.isMultiUser() ? userId : null };
|
|
71
|
+
var mateId = found.mate && found.mate.id ? found.mate.id : null;
|
|
72
|
+
return found.ctx.deliverCapsuleTurn(principal, {
|
|
73
|
+
toolId: manifest.id,
|
|
74
|
+
toolName: manifest.name,
|
|
75
|
+
kind: kind,
|
|
76
|
+
text: kind === "turn" ? turnPrompt(manifest) : startPrompt(manifest),
|
|
77
|
+
}).then(function (delivered) {
|
|
78
|
+
// An explicit game start navigates the user's home board into the
|
|
79
|
+
// Mate's game session, so starting a game visibly opens the table.
|
|
80
|
+
if (broadcastToUser && delivered && delivered.reference && mateId) {
|
|
81
|
+
broadcastToUser(userId, {
|
|
82
|
+
type: "capsule_game_session",
|
|
83
|
+
toolId: manifest.id,
|
|
84
|
+
toolName: manifest.name,
|
|
85
|
+
mateId: mateId,
|
|
86
|
+
sessionId: delivered.reference,
|
|
87
|
+
kind: kind,
|
|
88
|
+
created: !!delivered.created,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return delivered;
|
|
92
|
+
});
|
|
93
|
+
}).catch(function (error) {
|
|
94
|
+
console.error("[capsules] Could not deliver the Mate's turn:", error && error.message ? error.message : error);
|
|
95
|
+
return null;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
rememberOpponent: rememberOpponent,
|
|
101
|
+
maybeNudge: maybeNudge,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { attachCapsuleMateTurn: attachCapsuleMateTurn };
|
package/lib/capsule-pig-logic.js
CHANGED
|
@@ -159,6 +159,18 @@ function reset(state, seat) {
|
|
|
159
159
|
return newGame();
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
function statusTextFor(state, complete, userTurn) {
|
|
163
|
+
if (complete) {
|
|
164
|
+
return state.winner === USER ? "You win this game!" : "Your Mate wins this game.";
|
|
165
|
+
}
|
|
166
|
+
if (userTurn) {
|
|
167
|
+
return state.turnTotal > 0
|
|
168
|
+
? "Your turn: roll again or hold to bank " + state.turnTotal + " point(s)."
|
|
169
|
+
: "Your turn: roll the die.";
|
|
170
|
+
}
|
|
171
|
+
return "Your Mate's turn. Moves appear here as they happen.";
|
|
172
|
+
}
|
|
173
|
+
|
|
162
174
|
// The projection a Display renders and a Mate reads. It adds no meaning that is
|
|
163
175
|
// absent from state: every derived field is a restatement of the fields above.
|
|
164
176
|
// The score series exist because a progress chart binds to a collection.
|
|
@@ -172,7 +184,12 @@ function project(state) {
|
|
|
172
184
|
userScoreSeries: [{ seat: SEAT_LABELS.user, value: state.scores.user }],
|
|
173
185
|
mateScoreSeries: [{ seat: SEAT_LABELS.mate, value: state.scores.mate }],
|
|
174
186
|
turnTotal: state.turnTotal,
|
|
187
|
+
turnTotalSeries: [{ label: "Turn total", value: state.turnTotal }],
|
|
175
188
|
turnTotalText: "Turn total " + state.turnTotal + " of " + state.target + ", " + SEAT_LABELS[state.turn] + " to play.",
|
|
189
|
+
statusText: statusTextFor(state, complete, userTurn),
|
|
190
|
+
lastRollText: state.lastRoll === null
|
|
191
|
+
? "No rolls yet."
|
|
192
|
+
: "Last roll: " + state.lastRoll + " by " + SEAT_LABELS[state.lastActor || state.turn] + ".",
|
|
176
193
|
lastRoll: state.lastRoll,
|
|
177
194
|
lastActor: state.lastActor,
|
|
178
195
|
winner: state.winner,
|
|
@@ -184,17 +201,32 @@ function project(state) {
|
|
|
184
201
|
};
|
|
185
202
|
}
|
|
186
203
|
|
|
204
|
+
// When the Mate should be engaged is a game rule, so Logic declares it on the
|
|
205
|
+
// event rather than the host inferring it from Pig's fields. The host bridge
|
|
206
|
+
// delivers any declared engagement generically for every Capsule: "turn"
|
|
207
|
+
// means play your seat now, "start" means the human opened a fresh game and
|
|
208
|
+
// you should acknowledge it without acting.
|
|
209
|
+
function engagementFor(seat, actionId, next) {
|
|
210
|
+
if (seat !== USER) return null;
|
|
211
|
+
if (next.status === "playing" && next.turn === MATE) return { kind: "turn" };
|
|
212
|
+
if (actionId === "reset" && next.status === "playing") return { kind: "start" };
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
187
216
|
// Causality for the live Display: {actor, action, previous, next} plus a
|
|
188
217
|
// monotonic seq. Sending only the new state would make a Mate's move teleport
|
|
189
218
|
// onto the human's screen; the event lets the Display replay and attribute it.
|
|
190
219
|
function buildEvent(seat, actionId, previous, next) {
|
|
191
|
-
|
|
220
|
+
var event = {
|
|
192
221
|
seq: next.eventSeq,
|
|
193
222
|
actor: seat,
|
|
194
223
|
action: actionId,
|
|
195
224
|
previous: previous,
|
|
196
225
|
next: next,
|
|
197
226
|
};
|
|
227
|
+
var engage = engagementFor(seat, actionId, next);
|
|
228
|
+
if (engage) event.engage = engage;
|
|
229
|
+
return event;
|
|
198
230
|
}
|
|
199
231
|
|
|
200
232
|
function seatFor(context) {
|
|
@@ -13,11 +13,9 @@ var path = require("path");
|
|
|
13
13
|
var toolStorage = require("./tool-storage");
|
|
14
14
|
var toolsRegistry = require("./tools-registry");
|
|
15
15
|
var pigLogic = require("./capsule-pig-logic");
|
|
16
|
-
var tictactoeLogic = require("./capsule-tictactoe-logic");
|
|
17
16
|
|
|
18
17
|
var RUNTIME_FACTORIES = {
|
|
19
18
|
pig: pigLogic.createRuntime,
|
|
20
|
-
tictactoe: tictactoeLogic.createRuntime,
|
|
21
19
|
};
|
|
22
20
|
|
|
23
21
|
function hasRuntime(toolId) {
|
|
@@ -42,6 +42,14 @@
|
|
|
42
42
|
controls.style.marginTop = "10px";
|
|
43
43
|
root.appendChild(controls);
|
|
44
44
|
|
|
45
|
+
var rulesEl = document.createElement("p");
|
|
46
|
+
rulesEl.textContent = "Pig is a push-your-luck dice game: roll to build this turn's total, but a 1 loses it. Hold to bank your points; the first player to 100 wins.";
|
|
47
|
+
rulesEl.style.margin = "12px 0 0";
|
|
48
|
+
rulesEl.style.color = "#b8b8b8";
|
|
49
|
+
rulesEl.style.fontSize = "12px";
|
|
50
|
+
rulesEl.style.lineHeight = "1.45";
|
|
51
|
+
root.appendChild(rulesEl);
|
|
52
|
+
|
|
45
53
|
function makeButton(label, action, accent) {
|
|
46
54
|
var button = document.createElement("button");
|
|
47
55
|
button.type = "button";
|
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
"lucideIcon": "sparkles",
|
|
6
6
|
"description": "Two-player push-your-luck dice game shared by the user and one Mate.",
|
|
7
7
|
"useWhen": "Use when the user wants to play Pig, take a turn, or hear how the current game stands.",
|
|
8
|
-
"skills": "# Pig\n\nA two-seat push-your-luck dice game. The seats are `user` and `mate`; the person holds the user seat and you hold the mate seat. All randomness, turn order, and scoring are enforced server-side by the Capsule's Logic; you cannot roll a die yourself, and neither can the user.\n\n## Rules\n\n- Seats alternate turns. On your turn you may roll as often as you like.\n- Each roll adds its face value to the turn total. Rolling a 1 loses the entire turn total and passes play to the other seat.\n- Holding banks the turn total into your score and passes play.\n- The first seat to bank at least 100 points wins. A finished game is frozen until it is reset.\n- Only the user may reset a game that is still in progress. You may reset once the game is complete.\n\n## How to play\n\n1. `clay_tool_snapshot` with toolId `pig` to read the current state: `turn`, `scores.user`, `scores.mate`, `turnTotal`, `status`, `winner`, and `recentRolls`, which lists recent rolls only and never holds or wins.\n2. Act only when `turn` is `mate`. `clay_tool_act` with actionId `roll`, `hold`, or `reset`. Arguments are not used.\n3. An out-of-turn or post-game call is refused by Logic with an explanatory error. Read the snapshot again rather than retrying blindly.\n4. Every act returns the complete new state, so a snapshot immediately after an act is redundant.\n\n## Playing well\n\nThe interesting part of this Capsule is the judgement call, not the mechanics: decide whether to roll again or hold, and say why in your own words before you act. Typical play holds at a turn total near 20, but the score gap matters more than the rule of thumb, so explain the trade-off you are taking. Never claim a roll result before the act returns it."
|
|
8
|
+
"skills": "# Pig\n\nA two-seat push-your-luck dice game. The seats are `user` and `mate`; the person holds the user seat and you hold the mate seat. All randomness, turn order, and scoring are enforced server-side by the Capsule's Logic; you cannot roll a die yourself, and neither can the user.\n\n## Rules\n\n- Seats alternate turns. On your turn you may roll as often as you like.\n- Each roll adds its face value to the turn total. Rolling a 1 loses the entire turn total and passes play to the other seat.\n- Holding banks the turn total into your score and passes play.\n- The first seat to bank at least 100 points wins. A finished game is frozen until it is reset.\n- Only the user may reset a game that is still in progress. You may reset once the game is complete.\n\n## How to play\n\n1. `clay_tool_snapshot` with toolId `pig` to read the current state: `turn`, `scores.user`, `scores.mate`, `turnTotal`, `status`, `winner`, and `recentRolls`, which lists recent rolls only and never holds or wins.\n2. Act only when `turn` is `mate`. `clay_tool_act` with actionId `roll`, `hold`, or `reset`. Arguments are not used.\n3. An out-of-turn or post-game call is refused by Logic with an explanatory error. Read the snapshot again rather than retrying blindly.\n4. Every act returns the complete new state, so a snapshot immediately after an act is redundant.\n\n## Playing well\n\nThe interesting part of this Capsule is the judgement call, not the mechanics: decide whether to roll again or hold, and say why in your own words before you act. Typical play holds at a turn total near 20, but the score gap matters more than the rule of thumb, so explain the trade-off you are taking. Never claim a roll result before the act returns it.\n\n## Turn flow\n\nWhen the user's move hands the turn to your seat, Clay wakes you in a dedicated game session with a \"Capsule turn\" message. Take your whole turn there: roll as often as you judge right, then hold (or bust), and stop once the turn passes away from your seat. The user's Display updates live as you act, so never narrate moves you have not made. If nothing wakes you, you are not on turn."
|
|
9
9
|
}
|
package/lib/capsules/pig/ui.json
CHANGED
|
@@ -3,16 +3,42 @@
|
|
|
3
3
|
"props": { "gap": "md" },
|
|
4
4
|
"children": [
|
|
5
5
|
{
|
|
6
|
-
"type": "
|
|
7
|
-
"
|
|
8
|
-
"props": { "
|
|
6
|
+
"type": "callout",
|
|
7
|
+
"when": { "equals": { "path": "$state.complete", "value": true } },
|
|
8
|
+
"props": { "title": "Game over", "text": { "$bind": "$state.statusText", "fallback": "Game over." }, "icon": "circle-check", "variant": "soft" },
|
|
9
|
+
"else": {
|
|
10
|
+
"type": "callout",
|
|
11
|
+
"props": { "title": "Pig", "text": { "$bind": "$state.statusText", "fallback": "Your turn: roll the die." }, "icon": "sparkles", "variant": "soft" }
|
|
12
|
+
}
|
|
9
13
|
},
|
|
10
14
|
{
|
|
11
|
-
"type": "
|
|
12
|
-
"
|
|
13
|
-
"
|
|
15
|
+
"type": "row",
|
|
16
|
+
"props": { "gap": "md", "wrap": true },
|
|
17
|
+
"children": [
|
|
18
|
+
{
|
|
19
|
+
"type": "chart",
|
|
20
|
+
"bind": "userScoreSeries",
|
|
21
|
+
"props": { "label": "Your score", "kind": "progress", "categoryKey": "seat", "valueKey": "value", "max": 100 }
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"type": "chart",
|
|
25
|
+
"bind": "mateScoreSeries",
|
|
26
|
+
"props": { "label": "Your Mate's score", "kind": "progress", "categoryKey": "seat", "valueKey": "value", "max": 100 }
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"type": "row",
|
|
32
|
+
"props": { "gap": "md", "align": "center", "wrap": true },
|
|
33
|
+
"children": [
|
|
34
|
+
{
|
|
35
|
+
"type": "chart",
|
|
36
|
+
"bind": "turnTotalSeries",
|
|
37
|
+
"props": { "label": "Turn total", "kind": "metric", "valueKey": "value" }
|
|
38
|
+
},
|
|
39
|
+
{ "type": "text", "props": { "text": { "$bind": "$state.lastRollText", "fallback": "No rolls yet." }, "role": "muted" } }
|
|
40
|
+
]
|
|
14
41
|
},
|
|
15
|
-
{ "type": "text", "props": { "text": { "$bind": "$state.turnTotalText", "fallback": "Turn total 0 of 100." }, "role": "output" } },
|
|
16
42
|
{
|
|
17
43
|
"type": "row",
|
|
18
44
|
"props": { "gap": "sm", "wrap": true },
|
|
@@ -20,26 +46,38 @@
|
|
|
20
46
|
{
|
|
21
47
|
"type": "button",
|
|
22
48
|
"action": "roll",
|
|
23
|
-
"when": { "
|
|
49
|
+
"when": { "equals": { "path": "$state.userTurn", "value": true } },
|
|
24
50
|
"props": { "label": "Roll", "variant": "primary" },
|
|
25
51
|
"else": { "type": "button", "action": "roll", "props": { "label": "Roll", "variant": "primary", "disabled": true } }
|
|
26
52
|
},
|
|
27
53
|
{
|
|
28
54
|
"type": "button",
|
|
29
55
|
"action": "hold",
|
|
30
|
-
"when": { "
|
|
56
|
+
"when": { "equals": { "path": "$state.userTurn", "value": true } },
|
|
31
57
|
"props": { "label": "Hold", "variant": "secondary" },
|
|
32
58
|
"else": { "type": "button", "action": "hold", "props": { "label": "Hold", "variant": "secondary", "disabled": true } }
|
|
33
59
|
},
|
|
34
|
-
{
|
|
60
|
+
{
|
|
61
|
+
"type": "button",
|
|
62
|
+
"action": "reset",
|
|
63
|
+
"when": { "equals": { "path": "$state.complete", "value": true } },
|
|
64
|
+
"props": { "label": "New game", "variant": "primary" },
|
|
65
|
+
"else": { "type": "button", "action": "reset", "props": { "label": "Start over", "variant": "ghost" } }
|
|
66
|
+
}
|
|
35
67
|
]
|
|
36
68
|
},
|
|
37
69
|
{
|
|
38
|
-
"type": "
|
|
39
|
-
"
|
|
40
|
-
"props": { "variant": "divided", "gap": "xs" },
|
|
70
|
+
"type": "section",
|
|
71
|
+
"props": { "label": "Recent rolls", "variant": "inset", "gap": "xs" },
|
|
41
72
|
"children": [
|
|
42
|
-
{
|
|
73
|
+
{
|
|
74
|
+
"type": "list",
|
|
75
|
+
"bind": "recentRolls",
|
|
76
|
+
"props": { "variant": "divided", "gap": "xs" },
|
|
77
|
+
"children": [
|
|
78
|
+
{ "type": "text", "props": { "text": "$item.text", "role": "body" } }
|
|
79
|
+
]
|
|
80
|
+
}
|
|
43
81
|
]
|
|
44
82
|
}
|
|
45
83
|
]
|
package/lib/daemon-projects.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
var fs = require("fs");
|
|
8
8
|
var { scanWorktrees, isWorktree } = require("./worktree");
|
|
9
9
|
var { daemonSync } = require("./daemon-sync");
|
|
10
|
+
var logContext = require("./project-log-context");
|
|
10
11
|
|
|
11
12
|
// --- Worktree tracking state ---
|
|
12
13
|
var worktreeRegistry = {}; // parentSlug -> [wtSlug, ...]
|
|
@@ -28,7 +29,7 @@ function isWorktreeSlug(slug) {
|
|
|
28
29
|
* @param {string} parentIcon - icon of parent project
|
|
29
30
|
* @param {string} parentOwnerId - owner user ID
|
|
30
31
|
*/
|
|
31
|
-
function scanAndRegisterWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerId) {
|
|
32
|
+
function scanAndRegisterWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerId, projectKnowledgeId) {
|
|
32
33
|
if (isWorktree(parentPath)) return;
|
|
33
34
|
var worktrees = scanWorktrees(parentPath);
|
|
34
35
|
if (worktrees.length === 0) return;
|
|
@@ -41,13 +42,14 @@ function scanAndRegisterWorktrees(relay, parentPath, parentSlug, parentIcon, par
|
|
|
41
42
|
if (worktreeRegistry[parentSlug][j] === wtSlug) { alreadyRegistered = true; break; }
|
|
42
43
|
}
|
|
43
44
|
if (alreadyRegistered) continue;
|
|
44
|
-
var
|
|
45
|
+
var context = logContext.resolveWorktreeContext(wt.path, projectKnowledgeId, { branch: wt.branch || wt.dirName });
|
|
46
|
+
var wtMeta = Object.assign({ parentSlug: parentSlug, external: wt.external, projectKnowledgeId: projectKnowledgeId }, context);
|
|
45
47
|
relay.addProject(wt.path, wtSlug, wt.branch || wt.dirName, parentIcon, parentOwnerId, wtMeta);
|
|
46
48
|
worktreeRegistry[parentSlug].push(wtSlug);
|
|
47
49
|
console.log("[daemon] Registered worktree:", wtSlug, "->", wt.path, wt.external ? "(external)" : "(inside project folder)");
|
|
48
50
|
}
|
|
49
51
|
daemonSync.register(getWorktreeSyncKey(parentSlug), function () {
|
|
50
|
-
rescanWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerId);
|
|
52
|
+
rescanWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerId, null, projectKnowledgeId);
|
|
51
53
|
});
|
|
52
54
|
}
|
|
53
55
|
|
|
@@ -60,7 +62,7 @@ function scanAndRegisterWorktrees(relay, parentPath, parentSlug, parentIcon, par
|
|
|
60
62
|
* @param {string} parentOwnerId - owner user ID
|
|
61
63
|
* @param {object} [config] - daemon config (optional, for broadcasting project count)
|
|
62
64
|
*/
|
|
63
|
-
function rescanWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerId, config) {
|
|
65
|
+
function rescanWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerId, config, projectKnowledgeId) {
|
|
64
66
|
if (worktreeScanning[parentSlug]) return;
|
|
65
67
|
worktreeScanning[parentSlug] = true;
|
|
66
68
|
try {
|
|
@@ -79,7 +81,8 @@ function rescanWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerI
|
|
|
79
81
|
if (existingSlugs[ei] === wtSlug) { found = true; break; }
|
|
80
82
|
}
|
|
81
83
|
if (!found) {
|
|
82
|
-
var
|
|
84
|
+
var context = logContext.resolveWorktreeContext(wt.path, projectKnowledgeId, { branch: wt.branch || wt.dirName });
|
|
85
|
+
var wtMeta = Object.assign({ parentSlug: parentSlug, external: wt.external, projectKnowledgeId: projectKnowledgeId }, context);
|
|
83
86
|
relay.addProject(wt.path, wtSlug, wt.branch || wt.dirName, parentIcon, parentOwnerId, wtMeta);
|
|
84
87
|
if (!worktreeRegistry[parentSlug]) worktreeRegistry[parentSlug] = [];
|
|
85
88
|
worktreeRegistry[parentSlug].push(wtSlug);
|
|
@@ -91,6 +94,7 @@ function rescanWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerI
|
|
|
91
94
|
var sSlug = existingSlugs[si];
|
|
92
95
|
var dirName = sSlug.split("--").slice(1).join("--");
|
|
93
96
|
if (!discoveredNames[dirName]) {
|
|
97
|
+
if (relay.setProjectLogContextState) relay.setProjectLogContextState(sSlug, "archived");
|
|
94
98
|
relay.removeProject(sSlug);
|
|
95
99
|
existingSlugs.splice(si, 1);
|
|
96
100
|
console.log("[daemon] Rescan: removed stale worktree:", sSlug);
|
|
@@ -112,6 +116,7 @@ function rescanWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerI
|
|
|
112
116
|
function cleanupWorktreesForParent(relay, parentSlug) {
|
|
113
117
|
var wtSlugs = worktreeRegistry[parentSlug] || [];
|
|
114
118
|
for (var i = 0; i < wtSlugs.length; i++) {
|
|
119
|
+
if (relay.setProjectLogContextState) relay.setProjectLogContextState(wtSlugs[i], "archived");
|
|
115
120
|
relay.removeProject(wtSlugs[i]);
|
|
116
121
|
console.log("[daemon] Cascade removed worktree:", wtSlugs[i]);
|
|
117
122
|
}
|
package/lib/daemon.js
CHANGED
|
@@ -36,6 +36,7 @@ var { createWorktree, removeWorktree, isWorktree } = require("./worktree");
|
|
|
36
36
|
var mates = require("./mates");
|
|
37
37
|
var mateKnowledgeMigration = require("./mate-knowledge-migration");
|
|
38
38
|
var { isWorktreeSlug, scanAndRegisterWorktrees, rescanWorktrees, cleanupWorktreesForParent, getFilteredRemovedProjects, registerWorktreeSlug, unregisterWorktreeSlug } = require("./daemon-projects");
|
|
39
|
+
var projectLogContext = require("./project-log-context");
|
|
39
40
|
|
|
40
41
|
var daemonVersion = require("../package.json").version;
|
|
41
42
|
var configFile = process.env.CLAY_CONFIG || process.env.CLAUDE_RELAY_CONFIG || require("./config").configPath();
|
|
@@ -65,6 +66,11 @@ try {
|
|
|
65
66
|
console.error("[daemon] ensureMultiUser threw (continuing):", e && e.message);
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
if (projectLogContext.initializeProjectKnowledge(config)) {
|
|
70
|
+
saveConfig(config);
|
|
71
|
+
console.log("[daemon] Assigned stable knowledge identities to existing projects");
|
|
72
|
+
}
|
|
73
|
+
|
|
68
74
|
// --- OS users mode: check required system dependencies ---
|
|
69
75
|
// Supplementary-group inheritance (issue #367): default on so sessions behave
|
|
70
76
|
// like a normal login; operators can disable via config / the settings toggle.
|
|
@@ -153,6 +159,20 @@ var lanIp = (function () {
|
|
|
153
159
|
// --- Create multi-project server ---
|
|
154
160
|
var listenHost = config.host || "0.0.0.0";
|
|
155
161
|
|
|
162
|
+
function rememberedProjectKnowledgeId(projectPath) {
|
|
163
|
+
var removed = config.removedProjects || [];
|
|
164
|
+
for (var i = removed.length - 1; i >= 0; i--) {
|
|
165
|
+
if (removed[i].path === projectPath && projectLogContext.validId(removed[i].projectKnowledgeId)) {
|
|
166
|
+
return removed[i].projectKnowledgeId;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return projectLogContext.createProjectKnowledgeId();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function projectRuntimeOptions(project) {
|
|
173
|
+
return { projectKnowledgeId: project.projectKnowledgeId };
|
|
174
|
+
}
|
|
175
|
+
|
|
156
176
|
var relay = createServer({
|
|
157
177
|
tlsOptions: tlsOptions,
|
|
158
178
|
caPath: caRoot,
|
|
@@ -173,13 +193,14 @@ var relay = createServer({
|
|
|
173
193
|
}
|
|
174
194
|
var slugs = config.projects.map(function (p) { return p.slug; });
|
|
175
195
|
var slug = generateSlug(absPath, slugs);
|
|
176
|
-
relay.addProject(absPath, slug, null, null, wsUser ? wsUser.id : null);
|
|
177
196
|
var projectEntry = { path: absPath, slug: slug, addedAt: Date.now(), visibility: "private" };
|
|
197
|
+
projectEntry.projectKnowledgeId = rememberedProjectKnowledgeId(absPath);
|
|
178
198
|
// The user who adds a project always becomes the owner
|
|
179
199
|
if (wsUser && wsUser.id) {
|
|
180
200
|
projectEntry.ownerId = wsUser.id;
|
|
181
201
|
}
|
|
182
202
|
config.projects.push(projectEntry);
|
|
203
|
+
relay.addProject(absPath, slug, null, null, wsUser ? wsUser.id : null, null, projectRuntimeOptions(projectEntry));
|
|
183
204
|
// Remove from removedProjects if present
|
|
184
205
|
if (config.removedProjects) {
|
|
185
206
|
config.removedProjects = config.removedProjects.filter(function (rp) { return rp.path !== absPath; });
|
|
@@ -198,7 +219,7 @@ var relay = createServer({
|
|
|
198
219
|
}
|
|
199
220
|
}
|
|
200
221
|
// Discover and register worktrees for the new project
|
|
201
|
-
scanAndRegisterWorktrees(relay, absPath, slug, null, wsUser && wsUser.id && wsUser.role !== "admin" ? wsUser.id : null);
|
|
222
|
+
scanAndRegisterWorktrees(relay, absPath, slug, null, wsUser && wsUser.id && wsUser.role !== "admin" ? wsUser.id : null, projectEntry.projectKnowledgeId);
|
|
202
223
|
// Broadcast updated project list to all clients
|
|
203
224
|
relay.broadcastAll({
|
|
204
225
|
type: "projects_updated",
|
|
@@ -255,10 +276,11 @@ var relay = createServer({
|
|
|
255
276
|
}
|
|
256
277
|
// Register project - creator always becomes owner, default private
|
|
257
278
|
var projectEntry = { path: targetDir, slug: slug, addedAt: Date.now(), visibility: "private" };
|
|
279
|
+
projectEntry.projectKnowledgeId = projectLogContext.createProjectKnowledgeId();
|
|
258
280
|
if (wsUser && wsUser.id) {
|
|
259
281
|
projectEntry.ownerId = wsUser.id;
|
|
260
282
|
}
|
|
261
|
-
relay.addProject(targetDir, slug, null, null, wsUser ? wsUser.id : null);
|
|
283
|
+
relay.addProject(targetDir, slug, null, null, wsUser ? wsUser.id : null, null, projectRuntimeOptions(projectEntry));
|
|
262
284
|
config.projects.push(projectEntry);
|
|
263
285
|
saveConfig(config);
|
|
264
286
|
try { syncClayrc(config.projects); } catch (e) {}
|
|
@@ -345,10 +367,11 @@ var relay = createServer({
|
|
|
345
367
|
// Register project - creator always becomes owner
|
|
346
368
|
// Creator always becomes owner, default private
|
|
347
369
|
var projectEntry = { path: targetDir, slug: slug, addedAt: Date.now(), visibility: "private" };
|
|
370
|
+
projectEntry.projectKnowledgeId = projectLogContext.createProjectKnowledgeId();
|
|
348
371
|
if (wsUser && wsUser.id) {
|
|
349
372
|
projectEntry.ownerId = wsUser.id;
|
|
350
373
|
}
|
|
351
|
-
relay.addProject(targetDir, slug);
|
|
374
|
+
relay.addProject(targetDir, slug, null, null, wsUser ? wsUser.id : null, null, projectRuntimeOptions(projectEntry));
|
|
352
375
|
config.projects.push(projectEntry);
|
|
353
376
|
saveConfig(config);
|
|
354
377
|
try { syncClayrc(config.projects); } catch (e) {}
|
|
@@ -380,11 +403,14 @@ var relay = createServer({
|
|
|
380
403
|
if (config.projects[pi].slug === wtParent) { parentProject = config.projects[pi]; break; }
|
|
381
404
|
}
|
|
382
405
|
if (parentProject) {
|
|
406
|
+
var wtPath = path.join(parentProject.path, wtDirName);
|
|
407
|
+
var contextStatus = projectLogContext.isMerged(parentProject.path, wtPath) ? "merged" : "archived";
|
|
383
408
|
var rmResult = removeWorktree(parentProject.path, wtDirName);
|
|
384
409
|
if (!rmResult.ok) {
|
|
385
410
|
console.log("[daemon] Failed to remove worktree:", slug, rmResult.error);
|
|
386
411
|
return { ok: false, error: rmResult.error };
|
|
387
412
|
}
|
|
413
|
+
relay.setProjectLogContextState(slug, contextStatus);
|
|
388
414
|
}
|
|
389
415
|
relay.removeProject(slug);
|
|
390
416
|
unregisterWorktreeSlug(wtParent, slug);
|
|
@@ -410,6 +436,7 @@ var relay = createServer({
|
|
|
410
436
|
title: found.title || null,
|
|
411
437
|
icon: found.icon || null,
|
|
412
438
|
userId: userId || null,
|
|
439
|
+
projectKnowledgeId: found.projectKnowledgeId || null,
|
|
413
440
|
removedAt: Date.now(),
|
|
414
441
|
});
|
|
415
442
|
// Cap at 20 entries (oldest first)
|
|
@@ -740,6 +767,7 @@ var relay = createServer({
|
|
|
740
767
|
inheritGroups: config.inheritGroups !== false,
|
|
741
768
|
chatLayout: config.chatLayout || "channel",
|
|
742
769
|
matesEnabled: config.matesEnabled === true,
|
|
770
|
+
capsulesEnabled: config.capsulesEnabled === true,
|
|
743
771
|
pinEnabled: !!config.pinHash,
|
|
744
772
|
platform: process.platform,
|
|
745
773
|
hostname: os2.hostname(),
|
|
@@ -818,6 +846,13 @@ var relay = createServer({
|
|
|
818
846
|
console.log("[daemon] Project Mate DMs:", want ? "shown" : "hidden", "(web)");
|
|
819
847
|
return { ok: true, matesEnabled: want };
|
|
820
848
|
},
|
|
849
|
+
onSetCapsulesEnabled: function (value) {
|
|
850
|
+
var want = value === true;
|
|
851
|
+
config.capsulesEnabled = want;
|
|
852
|
+
saveConfig(config);
|
|
853
|
+
console.log("[daemon] Experimental Capsules:", want ? "enabled" : "disabled", "(web)");
|
|
854
|
+
return { ok: true, capsulesEnabled: want };
|
|
855
|
+
},
|
|
821
856
|
onGetToolPalettes: function () {
|
|
822
857
|
return config.toolPalettes || {};
|
|
823
858
|
},
|
|
@@ -976,11 +1011,16 @@ var relay = createServer({
|
|
|
976
1011
|
}
|
|
977
1012
|
if (!parent) return { ok: false, error: "Parent project not found" };
|
|
978
1013
|
if (isWorktree(parent.path)) return { ok: false, error: "Cannot create worktrees from a worktree project" };
|
|
1014
|
+
var baseCommit = projectLogContext.commit(parent.path, baseBranch || "HEAD");
|
|
979
1015
|
var result = createWorktree(parent.path, branchName, dirName, baseBranch);
|
|
980
1016
|
if (!result.ok) return result;
|
|
981
1017
|
// Register the new worktree as ephemeral project
|
|
982
1018
|
var wtSlug = parentSlug + "--" + dirName;
|
|
983
|
-
var
|
|
1019
|
+
var context = projectLogContext.resolveWorktreeContext(result.path, parent.projectKnowledgeId, {
|
|
1020
|
+
branch: branchName,
|
|
1021
|
+
baseCommit: baseCommit,
|
|
1022
|
+
});
|
|
1023
|
+
var wtMeta = Object.assign({ parentSlug: parentSlug, external: false, projectKnowledgeId: parent.projectKnowledgeId }, context);
|
|
984
1024
|
relay.addProject(result.path, wtSlug, branchName, parent.icon, parent.ownerId, wtMeta);
|
|
985
1025
|
registerWorktreeSlug(parentSlug, wtSlug);
|
|
986
1026
|
console.log("[daemon] Created worktree:", wtSlug, "->", result.path);
|
|
@@ -1066,13 +1106,15 @@ var ipc = createIPCServer(socketPath(), function (msg) {
|
|
|
1066
1106
|
}
|
|
1067
1107
|
var slugs = config.projects.map(function (p) { return p.slug; });
|
|
1068
1108
|
var slug = generateSlug(absPath, slugs);
|
|
1069
|
-
|
|
1070
|
-
|
|
1109
|
+
var projectEntry = { path: absPath, slug: slug, addedAt: Date.now(), visibility: "private" };
|
|
1110
|
+
projectEntry.projectKnowledgeId = rememberedProjectKnowledgeId(absPath);
|
|
1111
|
+
relay.addProject(absPath, slug, null, null, null, null, projectRuntimeOptions(projectEntry));
|
|
1112
|
+
config.projects.push(projectEntry);
|
|
1071
1113
|
saveConfig(config);
|
|
1072
1114
|
try { syncClayrc(config.projects); } catch (e) {}
|
|
1073
1115
|
console.log("[daemon] Added project:", slug, "→", absPath);
|
|
1074
1116
|
// Discover and register worktrees for the new project
|
|
1075
|
-
scanAndRegisterWorktrees(relay, absPath, slug, null, null);
|
|
1117
|
+
scanAndRegisterWorktrees(relay, absPath, slug, null, null, projectEntry.projectKnowledgeId);
|
|
1076
1118
|
relay.broadcastAll({
|
|
1077
1119
|
type: "projects_updated",
|
|
1078
1120
|
projects: relay.getProjects(),
|
|
@@ -1094,6 +1136,23 @@ var ipc = createIPCServer(socketPath(), function (msg) {
|
|
|
1094
1136
|
}
|
|
1095
1137
|
}
|
|
1096
1138
|
if (!target) return { ok: false, error: "project not found" };
|
|
1139
|
+
var removedProject = null;
|
|
1140
|
+
for (var ri = 0; ri < config.projects.length; ri++) {
|
|
1141
|
+
if (config.projects[ri].slug === target) { removedProject = config.projects[ri]; break; }
|
|
1142
|
+
}
|
|
1143
|
+
if (removedProject) {
|
|
1144
|
+
if (!config.removedProjects) config.removedProjects = [];
|
|
1145
|
+
config.removedProjects.push({
|
|
1146
|
+
path: removedProject.path,
|
|
1147
|
+
title: removedProject.title || null,
|
|
1148
|
+
icon: removedProject.icon || null,
|
|
1149
|
+
projectKnowledgeId: removedProject.projectKnowledgeId || null,
|
|
1150
|
+
removedAt: Date.now(),
|
|
1151
|
+
});
|
|
1152
|
+
if (config.removedProjects.length > 20) {
|
|
1153
|
+
config.removedProjects = config.removedProjects.slice(config.removedProjects.length - 20);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1097
1156
|
relay.removeProject(target);
|
|
1098
1157
|
config.projects = config.projects.filter(function (p) { return p.slug !== target; });
|
|
1099
1158
|
saveConfig(config);
|
|
@@ -1284,9 +1343,9 @@ for (var i = 0; i < projects.length; i++) {
|
|
|
1284
1343
|
var p = projects[i];
|
|
1285
1344
|
if (fs.existsSync(p.path)) {
|
|
1286
1345
|
console.log("[daemon] Adding project:", p.slug, "→", p.path);
|
|
1287
|
-
relay.addProject(p.path, p.slug, p.title, p.icon, p.ownerId);
|
|
1346
|
+
relay.addProject(p.path, p.slug, p.title, p.icon, p.ownerId, null, projectRuntimeOptions(p));
|
|
1288
1347
|
// Discover and register worktrees for this project
|
|
1289
|
-
scanAndRegisterWorktrees(relay, p.path, p.slug, p.icon, p.ownerId);
|
|
1348
|
+
scanAndRegisterWorktrees(relay, p.path, p.slug, p.icon, p.ownerId, p.projectKnowledgeId);
|
|
1290
1349
|
} else {
|
|
1291
1350
|
console.log("[daemon] Skipping missing project:", p.path);
|
|
1292
1351
|
}
|
|
@@ -23,6 +23,7 @@ var SETTING_KEYS = [
|
|
|
23
23
|
"chatLayout",
|
|
24
24
|
"autoContinueOnRateLimit",
|
|
25
25
|
"matesEnabled",
|
|
26
|
+
"capsulesEnabled",
|
|
26
27
|
"terminalFont",
|
|
27
28
|
"deletedBuiltinKeys",
|
|
28
29
|
"mateOnboardingShown",
|
|
@@ -189,6 +190,7 @@ function migrateSingleUserToMulti() {
|
|
|
189
190
|
if (cfg.chatLayout !== undefined && users.setChatLayout) users.setChatLayout(userId, cfg.chatLayout);
|
|
190
191
|
if (cfg.autoContinueOnRateLimit !== undefined && users.setAutoContinue) users.setAutoContinue(userId, cfg.autoContinueOnRateLimit);
|
|
191
192
|
if (cfg.matesEnabled !== undefined && users.setMatesEnabled) users.setMatesEnabled(userId, cfg.matesEnabled);
|
|
193
|
+
if (cfg.capsulesEnabled !== undefined && users.setCapsulesEnabled) users.setCapsulesEnabled(userId, cfg.capsulesEnabled);
|
|
192
194
|
if (cfg.terminalFont && users.setTerminalFont) {
|
|
193
195
|
users.setTerminalFont(userId, cfg.terminalFont.family, cfg.terminalFont.size);
|
|
194
196
|
}
|