claude-code-kanban 7.18.0 → 7.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/ask.js +215 -0
- package/package.json +1 -1
- package/plugin/plugins/claude-code-kanban/.claude-plugin/plugin.json +1 -1
- package/plugin/plugins/claude-code-kanban/hooks/ask.ts +163 -0
- package/plugin/plugins/claude-code-kanban/hooks/index.ts +2 -0
- package/plugin/plugins/claude-code-kanban/skills/ask/SKILL.md +44 -0
- package/plugin/plugins/claude-code-kanban/skills/ask/references/playground.md +25 -0
- package/plugin/plugins/claude-code-kanban/tests/ask.test.ts +100 -0
- package/plugin/plugins/claude-code-kanban/tests/show.test.ts +1 -1
- package/public/app.js +286 -56
- package/public/index.html +24 -3
- package/public/style.css +176 -1
- package/public/vendor/claude-hub-sdk.js +18 -3
- package/server.js +11 -0
package/public/app.js
CHANGED
|
@@ -2808,7 +2808,7 @@ function isAnyModalOpen() {
|
|
|
2808
2808
|
|
|
2809
2809
|
// An open modal owns the zoom keys even when it has nothing to scale.
|
|
2810
2810
|
function isZoomableOnScreen() {
|
|
2811
|
-
return isAnyModalOpen() ? isZoomableModalOpen() : isPaneDocOnScreen() || isShowCardFocused();
|
|
2811
|
+
return isAnyModalOpen() ? isZoomableModalOpen() : isAskOnScreen() || isPaneDocOnScreen() || isShowCardFocused();
|
|
2812
2812
|
}
|
|
2813
2813
|
|
|
2814
2814
|
const ZOOM_KEYS = { '+': 0.1, '=': 0.1, NumpadAdd: 0.1, '-': -0.1, _: -0.1, NumpadSubtract: -0.1, 0: 0, Numpad0: 0 };
|
|
@@ -3433,24 +3433,34 @@ function agentIdentHtml(a) {
|
|
|
3433
3433
|
|
|
3434
3434
|
// The count names every running agent; the rows stop at ZEN_AGENTS_MAX, and the Agents Log
|
|
3435
3435
|
// below the board lists the rest.
|
|
3436
|
-
function zenAgentsHtml(
|
|
3437
|
-
const active = agents.filter((a) => a.status === 'active');
|
|
3436
|
+
function zenAgentsHtml(active) {
|
|
3438
3437
|
if (!active.length) return '';
|
|
3439
|
-
const now = Date.now();
|
|
3440
3438
|
const rows = active.slice(0, ZEN_AGENTS_MAX).map((a) => {
|
|
3441
3439
|
const task = truncateAtWord(agentTaskText(a), 200);
|
|
3442
3440
|
return `<button type="button" class="zen-agent" title="${escapeHtml(task)}" onclick="showAgentModal('${escAttrJs(a.agentId)}')">
|
|
3443
3441
|
<span class="agent-dot active"></span>
|
|
3444
3442
|
<span class="zen-agent-text"><span class="zen-agent-task">${escapeHtml(task)}</span><span class="agent-ident-row">${agentIdentHtml(a)}</span></span>
|
|
3445
|
-
<span class="zen-agent-time"
|
|
3443
|
+
<span class="zen-agent-time"></span>
|
|
3446
3444
|
</button>`;
|
|
3447
3445
|
});
|
|
3448
3446
|
return `<div class="panel-section-header"><span>Agents</span><span class="panel-section-count">${active.length}</span></div>${rows.join('')}`;
|
|
3449
3447
|
}
|
|
3450
3448
|
|
|
3449
|
+
// The 1 s duration tick calls this. Rewriting the rows each time would drop the focus of a row,
|
|
3450
|
+
// so the rows are rewritten only when they change and the times are set in place.
|
|
3451
3451
|
function renderZenAgents(agents) {
|
|
3452
3452
|
const el = zenMode && sessionsList.querySelector('.zen-agents');
|
|
3453
|
-
if (el)
|
|
3453
|
+
if (!el) return;
|
|
3454
|
+
const active = agents.filter((a) => a.status === 'active');
|
|
3455
|
+
const html = zenAgentsHtml(active);
|
|
3456
|
+
if (html !== el.dataset.shape) {
|
|
3457
|
+
el.dataset.shape = html;
|
|
3458
|
+
el.innerHTML = html;
|
|
3459
|
+
}
|
|
3460
|
+
const now = Date.now();
|
|
3461
|
+
el.querySelectorAll('.zen-agent-time').forEach((t, i) => {
|
|
3462
|
+
t.textContent = formatDuration(agentElapsed(active[i], now));
|
|
3463
|
+
});
|
|
3454
3464
|
}
|
|
3455
3465
|
|
|
3456
3466
|
// Filter shutdown ghosts: for same-type agents, keep if they overlapped (parallel)
|
|
@@ -4111,11 +4121,13 @@ function renderSessions() {
|
|
|
4111
4121
|
${renderZenBookmarksHtml(zenSession.id)}
|
|
4112
4122
|
</div>
|
|
4113
4123
|
${zenSession.hasWorkflow ? renderWorkflowLiveHtml(zenSession.id) : ''}
|
|
4114
|
-
<div class="zen-agents"
|
|
4124
|
+
<div class="zen-agents"></div>`;
|
|
4125
|
+
renderZenAgents(visibleAgents());
|
|
4115
4126
|
bindLinkedDocsHandlers(sessionsList.querySelector('.linked-docs-section'), zenSession.id);
|
|
4116
4127
|
artifactsSection.load(zenSession.id);
|
|
4117
4128
|
scratchFilesSection.load(zenSession.id);
|
|
4118
4129
|
if (zenSession.hasWorkflow) pollWorkflowLive(zenSession.id);
|
|
4130
|
+
restoreSessionCursor();
|
|
4119
4131
|
return;
|
|
4120
4132
|
}
|
|
4121
4133
|
|
|
@@ -4364,7 +4376,10 @@ function renderSessions() {
|
|
|
4364
4376
|
const tail = sectioned ? sectionHtml(SECTION_SESSIONS, 'Sessions', true, restHtml, sgRest.length === 0) : restHtml;
|
|
4365
4377
|
sessionsList.innerHTML = html + sgSectionHtml(true) + tail + outsideHtml;
|
|
4366
4378
|
}
|
|
4379
|
+
restoreSessionCursor();
|
|
4380
|
+
}
|
|
4367
4381
|
|
|
4382
|
+
function restoreSessionCursor() {
|
|
4368
4383
|
const navItems = getNavigableItems();
|
|
4369
4384
|
const allSessions = getSessionItems();
|
|
4370
4385
|
const activeIdx = allSessions.findIndex((el) => el.classList.contains('active'));
|
|
@@ -7949,6 +7964,21 @@ document.addEventListener('keydown', (e) => {
|
|
|
7949
7964
|
toggleZenMode();
|
|
7950
7965
|
return;
|
|
7951
7966
|
}
|
|
7967
|
+
if (e.key === '$' && plainShift) {
|
|
7968
|
+
e.preventDefault();
|
|
7969
|
+
openCost(contextSessionId());
|
|
7970
|
+
return;
|
|
7971
|
+
}
|
|
7972
|
+
if (matchKey(e, 'KeyM')) {
|
|
7973
|
+
e.preventDefault();
|
|
7974
|
+
openMarketplace(contextSessionId());
|
|
7975
|
+
return;
|
|
7976
|
+
}
|
|
7977
|
+
if (ClaudeHub.comboOf(e) === 'ctrl+m') {
|
|
7978
|
+
e.preventDefault();
|
|
7979
|
+
openMemory(contextSessionId());
|
|
7980
|
+
return;
|
|
7981
|
+
}
|
|
7952
7982
|
if (e.key === '.' || e.key === '>') {
|
|
7953
7983
|
const sid = sessionsList.querySelector('.kb-selected')?.dataset.sessionId || currentSessionId;
|
|
7954
7984
|
if (sid) {
|
|
@@ -8089,23 +8119,6 @@ document.addEventListener('keydown', (e) => {
|
|
|
8089
8119
|
else toggleScratchpad();
|
|
8090
8120
|
return;
|
|
8091
8121
|
}
|
|
8092
|
-
if (e.key === '$' && !e.ctrlKey && !e.altKey && !e.metaKey) {
|
|
8093
|
-
e.preventDefault();
|
|
8094
|
-
openCost(contextSid);
|
|
8095
|
-
return;
|
|
8096
|
-
}
|
|
8097
|
-
if (matchKey(e, 'KeyM')) {
|
|
8098
|
-
e.preventDefault();
|
|
8099
|
-
const mSession = contextSid ? sessions.find((s) => s.id === contextSid) : null;
|
|
8100
|
-
openMarketplace(mSession?.project);
|
|
8101
|
-
return;
|
|
8102
|
-
}
|
|
8103
|
-
if (ClaudeHub.comboOf(e) === 'ctrl+m') {
|
|
8104
|
-
e.preventDefault();
|
|
8105
|
-
const mSession = contextSid ? sessions.find((s) => s.id === contextSid) : null;
|
|
8106
|
-
openMemory(mSession?.project);
|
|
8107
|
-
return;
|
|
8108
|
-
}
|
|
8109
8122
|
if (ClaudeHub.comboOf(e) === 'ctrl+d') {
|
|
8110
8123
|
e.preventDefault();
|
|
8111
8124
|
dismissContextSession();
|
|
@@ -8709,7 +8722,9 @@ const ZOOM_CLAIMS = ['ctrlKey', 'metaKey'].flatMap((mod) =>
|
|
|
8709
8722
|
// first keeps it, and text fields keep every key.
|
|
8710
8723
|
const PANE_CLAIMS = [
|
|
8711
8724
|
...['\\', '[', ']', 'ContextMenu', 'Escape'].map((key) => ({ key, code: '' })),
|
|
8712
|
-
...['|', '{', '}', 'F10'].map((key) => ({ shiftKey: true, key, code: '' })),
|
|
8725
|
+
...['|', '{', '}', 'F10', '$'].map((key) => ({ shiftKey: true, key, code: '' })),
|
|
8726
|
+
{ key: 'Unidentified', code: 'KeyM' },
|
|
8727
|
+
{ ctrlKey: true, key: 'm', code: '' },
|
|
8713
8728
|
...['BracketLeft', 'BracketRight', 'KeyW'].map((code) => ({ altKey: true, key: 'Unidentified', code })),
|
|
8714
8729
|
...['KeyL', 'KeyM', 'KeyP', 'KeyS', 'KeyZ'].map((code) => ({ shiftKey: true, key: 'Unidentified', code })),
|
|
8715
8730
|
...'123456789'.split('').map((n) => ({ ctrlKey: true, altKey: true, key: 'Unidentified', code: `Digit${n}` })),
|
|
@@ -9058,6 +9073,7 @@ function reviewOptsOfFrame(frame) {
|
|
|
9058
9073
|
|
|
9059
9074
|
async function sendPreviewAction(frame, msg) {
|
|
9060
9075
|
if (!navigator.userActivation?.isActive) return;
|
|
9076
|
+
if (frame === askState.frame) return answerAsk({ action: msg.action, label: msg.label, fields: msg.data });
|
|
9061
9077
|
const opts = reviewOptsOfFrame(frame);
|
|
9062
9078
|
if (!opts?.sessionId) return;
|
|
9063
9079
|
const key = `${msg.action}\n${JSON.stringify(msg.data)}`;
|
|
@@ -9197,7 +9213,7 @@ function createPreviewFrame(className, srcdoc, sandbox = 'allow-scripts allow-po
|
|
|
9197
9213
|
frame.setAttribute('referrerpolicy', 'no-referrer');
|
|
9198
9214
|
frame.srcdoc = srcdoc;
|
|
9199
9215
|
bridgedFrames.add(frame);
|
|
9200
|
-
frame.addEventListener('load', () => sendBridgeClaimsAll([frame], keyClaims()));
|
|
9216
|
+
frame.addEventListener('load', () => sendBridgeClaimsAll([frame], bridgeClaims(keyClaims())));
|
|
9201
9217
|
return frame;
|
|
9202
9218
|
}
|
|
9203
9219
|
|
|
@@ -10235,6 +10251,8 @@ function setupEventSource() {
|
|
|
10235
10251
|
|
|
10236
10252
|
if (data.type === 'show:cleared') onShowCleared(data);
|
|
10237
10253
|
if (data.type === 'show:removed') onShowRemoved(data);
|
|
10254
|
+
if (data.type === 'ask:opened') onAskOpened(data);
|
|
10255
|
+
if (data.type === 'ask:closed') onAskClosed(data);
|
|
10238
10256
|
|
|
10239
10257
|
if (data.type === 'agent-update') {
|
|
10240
10258
|
if (!data.unreadOnly) pendingAgentSessionIds.add(data.sessionId);
|
|
@@ -10275,7 +10293,7 @@ function setupEventSource() {
|
|
|
10275
10293
|
// Alone, the page would open a tab without a user gesture, which the browser blocks; the
|
|
10276
10294
|
// CLI prints the link instead.
|
|
10277
10295
|
if (data.type === 'session:cost' && hub.inHub) {
|
|
10278
|
-
openCost(data.id, data.agent);
|
|
10296
|
+
openCost(data.id, { agent: data.agent, pane: false });
|
|
10279
10297
|
}
|
|
10280
10298
|
|
|
10281
10299
|
if (data.type === 'session:pin') {
|
|
@@ -13148,31 +13166,33 @@ function openFolderInEditor(folder, file) {
|
|
|
13148
13166
|
postAndToast('/api/open-folder', body, 'folder');
|
|
13149
13167
|
}
|
|
13150
13168
|
|
|
13151
|
-
|
|
13152
|
-
|
|
13153
|
-
|
|
13154
|
-
hub.invoke(
|
|
13169
|
+
// Opens the app in a pane of `sid` and brings the pane into view. With no session, or `pane: false`
|
|
13170
|
+
// (a Shift-click), the hub switches to the app's tab instead.
|
|
13171
|
+
async function openAppAction(action, params, sid, { pane = true, label }) {
|
|
13172
|
+
if (!sid || !pane) return hub.invoke(action, params);
|
|
13173
|
+
const { url } = await hub.invoke(action, params, { open: 'pane' });
|
|
13174
|
+
if (!url) return;
|
|
13175
|
+
const onScreen = await postPane(sid, { target: url, title: label });
|
|
13176
|
+
if (onScreen !== null) revealPane(sid, onScreen);
|
|
13155
13177
|
}
|
|
13156
13178
|
|
|
13157
|
-
function
|
|
13158
|
-
|
|
13179
|
+
function openCost(sid, { agent, pane } = {}) {
|
|
13180
|
+
const opts = { pane, label: 'Cost' };
|
|
13181
|
+
if (sid && agent && hub.can('agent.cost')) return openAppAction('agent.cost', { session: sid, agent }, sid, opts);
|
|
13182
|
+
return openAppAction('session.cost', sid ? { session: sid } : {}, sid, opts);
|
|
13159
13183
|
}
|
|
13160
13184
|
|
|
13161
|
-
function
|
|
13162
|
-
|
|
13185
|
+
function openProjectApp(action, label, sid, { pane } = {}) {
|
|
13186
|
+
const project = sessions.find((s) => s.id === sid)?.project;
|
|
13187
|
+
return openAppAction(action, project ? { project } : {}, sid, { pane, label });
|
|
13163
13188
|
}
|
|
13189
|
+
const openMarketplace = (sid, opts) => openProjectApp('project.plugins', 'Marketplace', sid, opts);
|
|
13190
|
+
const openMemory = (sid, opts) => openProjectApp('project.memory', 'Memory', sid, opts);
|
|
13164
13191
|
|
|
13165
|
-
function openForInfoModalProject(open) {
|
|
13166
|
-
const s = sessions.find((x) => x.id === _infoModalSessionId);
|
|
13167
|
-
if (s?.project) open(s.project);
|
|
13168
|
-
}
|
|
13169
13192
|
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
13170
|
-
function
|
|
13171
|
-
|
|
13172
|
-
}
|
|
13173
|
-
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
13174
|
-
function openMemoryForInfoModal() {
|
|
13175
|
-
openForInfoModalProject(openMemory);
|
|
13193
|
+
function openAppForInfoModal(open, e) {
|
|
13194
|
+
closeTeamModal();
|
|
13195
|
+
open(_infoModalSessionId, { pane: !e.shiftKey });
|
|
13176
13196
|
}
|
|
13177
13197
|
|
|
13178
13198
|
//#endregion
|
|
@@ -13883,6 +13903,11 @@ async function loadPaneView(sid, pane, view, scroll) {
|
|
|
13883
13903
|
frame.setAttribute('referrerpolicy', 'no-referrer');
|
|
13884
13904
|
frame.title = pane.title;
|
|
13885
13905
|
frame.src = pane.target;
|
|
13906
|
+
frame.addEventListener('load', () => {
|
|
13907
|
+
view.classList.remove('keys-bridged');
|
|
13908
|
+
frame.hostKeysSig = null;
|
|
13909
|
+
pushTerminalClaims();
|
|
13910
|
+
});
|
|
13886
13911
|
view.appendChild(frame);
|
|
13887
13912
|
view.insertAdjacentHTML('beforeend', paneUrlBarHtml());
|
|
13888
13913
|
// Framed at once; a refusal found by the probe swaps the frame for the card.
|
|
@@ -15192,9 +15217,10 @@ function inPageField(e) {
|
|
|
15192
15217
|
function terminalShortcut(e, probe = false) {
|
|
15193
15218
|
const ctrlAlt = hubModDown(e);
|
|
15194
15219
|
const ctrlShift = e.ctrlKey && e.shiftKey && !e.altKey && !e.metaKey;
|
|
15195
|
-
// Ctrl+_ is Claude Code's undo, so the zoom keys leave it to the terminal.
|
|
15220
|
+
// Ctrl+_ is Claude Code's undo, so the zoom keys leave it to the terminal. An open ask card,
|
|
15221
|
+
// which sits in the terminal pane, takes them for its own text zoom.
|
|
15196
15222
|
const zoom = e.key === '_' ? undefined : zoomDelta(e);
|
|
15197
|
-
if (zoom !== undefined && termFrame.inited && (probe || terminalPaneFocused())) {
|
|
15223
|
+
if (zoom !== undefined && termFrame.inited && (probe || (terminalPaneFocused() && !isAskOnScreen()))) {
|
|
15198
15224
|
return () => adjustTerminalFontSize(Math.sign(zoom));
|
|
15199
15225
|
}
|
|
15200
15226
|
if (ctrlAlt && e.code === 'KeyS') return swapToPreviousSession;
|
|
@@ -15574,23 +15600,49 @@ function bridgeClaims(claims) {
|
|
|
15574
15600
|
return { ...claims, keys: [...claims.keys, ...ZOOM_CLAIMS, ...PANE_CLAIMS, ...review] };
|
|
15575
15601
|
}
|
|
15576
15602
|
|
|
15577
|
-
function sendBridgeClaimsAll(frames,
|
|
15578
|
-
const base = bridgeClaims(claims);
|
|
15603
|
+
function sendBridgeClaimsAll(frames, base) {
|
|
15579
15604
|
const rest = frames.filter((f) => f !== showState.frame);
|
|
15580
15605
|
sendBridgeClaims(rest, base);
|
|
15581
15606
|
if (rest.length < frames.length)
|
|
15582
15607
|
sendBridgeClaims([showState.frame], { ...base, keys: [...base.keys, ...SHOW_CLAIMS] });
|
|
15583
15608
|
}
|
|
15584
15609
|
|
|
15610
|
+
// A hub app in a URL pane runs the SDK, which hands back these keys (hub protocol §5.2). Only keys with
|
|
15611
|
+
// Ctrl, Alt or Meta: the page keeps Escape, brackets and its own letter keys.
|
|
15612
|
+
function sendHostKeys(frames, bridged) {
|
|
15613
|
+
const keys = bridged.keys.filter((k) => k.ctrlKey || k.altKey || k.metaKey);
|
|
15614
|
+
const sig = JSON.stringify(keys);
|
|
15615
|
+
for (const f of frames) {
|
|
15616
|
+
const origin = URL.parse(f.src)?.origin;
|
|
15617
|
+
if (!origin || origin === 'null' || f.hostKeysSig === sig) continue;
|
|
15618
|
+
f.hostKeysSig = sig;
|
|
15619
|
+
f.contentWindow?.postMessage({ type: 'hub:hostKeys', keys }, origin);
|
|
15620
|
+
}
|
|
15621
|
+
}
|
|
15622
|
+
|
|
15623
|
+
const urlPaneFrames = () => [...paneViews.querySelectorAll('.pane-url-frame')];
|
|
15624
|
+
|
|
15625
|
+
window.addEventListener('message', (e) => {
|
|
15626
|
+
const type = e.data?.type;
|
|
15627
|
+
if (type !== 'hub:hostKey' && type !== 'hub:hostReady') return;
|
|
15628
|
+
const frame = urlPaneFrames().find((f) => f.contentWindow === e.source);
|
|
15629
|
+
if (!frame || URL.parse(frame.src)?.origin !== e.origin) return;
|
|
15630
|
+
if (type === 'hub:hostReady') frame.parentElement.classList.add('keys-bridged');
|
|
15631
|
+
else replayKey(e.data, frame);
|
|
15632
|
+
});
|
|
15633
|
+
|
|
15585
15634
|
function pushTerminalClaims() {
|
|
15586
15635
|
if (terminalClaimsQueued) return;
|
|
15587
15636
|
terminalClaimsQueued = true;
|
|
15588
15637
|
queueMicrotask(() => {
|
|
15589
15638
|
terminalClaimsQueued = false;
|
|
15590
15639
|
const bridged = bridgedFrameEls();
|
|
15591
|
-
|
|
15640
|
+
const urlFrames = urlPaneFrames();
|
|
15641
|
+
if (!termFrame.inited && !kanbotState.inited && !bridged.length && !urlFrames.length) return;
|
|
15592
15642
|
const claims = keyClaims();
|
|
15593
|
-
|
|
15643
|
+
const base = bridgeClaims(claims);
|
|
15644
|
+
sendBridgeClaimsAll(bridged, base);
|
|
15645
|
+
sendHostKeys(urlFrames, base);
|
|
15594
15646
|
const sig = JSON.stringify(claims);
|
|
15595
15647
|
sendFrameClaims(kanbotState, claims, sig, postToKanbotFrame);
|
|
15596
15648
|
sendFrameClaims(termFrame, claims, sig, terminalFrameSend);
|
|
@@ -16369,6 +16421,177 @@ window.addEventListener('message', (e) => {
|
|
|
16369
16421
|
|
|
16370
16422
|
//#endregion
|
|
16371
16423
|
|
|
16424
|
+
//#region ASK
|
|
16425
|
+
// The question the session in the embedded terminal asks with the ask tool; the tool call waits for
|
|
16426
|
+
// the answer. Hiding keeps the frame, so a half-filled card comes back as it was. Only an answer, a
|
|
16427
|
+
// skip or the agent's interrupt ends the question.
|
|
16428
|
+
const ASK_MSG = 'cck-ask:';
|
|
16429
|
+
const askState = { terminalId: null, ask: null, frame: null, frameH: 0, hidden: false, busy: false, seq: 0 };
|
|
16430
|
+
|
|
16431
|
+
// Runs inside the card as source text, ahead of the card's own scripts. The JSON round trip makes
|
|
16432
|
+
// the answer cloneable, and a value that is not JSON throws in the card.
|
|
16433
|
+
function askBridge() {
|
|
16434
|
+
window.cck = {
|
|
16435
|
+
send: (data) => parent.postMessage({ type: 'cck-ask:send', data: JSON.parse(JSON.stringify(data ?? null)) }, '*'),
|
|
16436
|
+
};
|
|
16437
|
+
}
|
|
16438
|
+
|
|
16439
|
+
const ASK_BRIDGE_TAG = `<script>(${askBridge})();</script>`;
|
|
16440
|
+
|
|
16441
|
+
function askEl() {
|
|
16442
|
+
return document.getElementById('ask-modal');
|
|
16443
|
+
}
|
|
16444
|
+
|
|
16445
|
+
function isAskOnScreen() {
|
|
16446
|
+
return !!askState.ask && !askEl().hidden;
|
|
16447
|
+
}
|
|
16448
|
+
|
|
16449
|
+
function syncAsk(tid) {
|
|
16450
|
+
if (tid === askState.terminalId) return;
|
|
16451
|
+
askState.terminalId = tid;
|
|
16452
|
+
setAsk(null);
|
|
16453
|
+
if (tid) loadAsk(tid);
|
|
16454
|
+
}
|
|
16455
|
+
|
|
16456
|
+
async function loadAsk(tid) {
|
|
16457
|
+
const seq = ++askState.seq;
|
|
16458
|
+
const data = await getJson(apiPath`/api/terminals/${tid}/ask`, null, { cache: 'no-store' });
|
|
16459
|
+
if (seq !== askState.seq || tid !== askState.terminalId) return;
|
|
16460
|
+
const a = data?.ask ?? null;
|
|
16461
|
+
if (a?.id !== askState.ask?.id) setAsk(a);
|
|
16462
|
+
}
|
|
16463
|
+
|
|
16464
|
+
function setAsk(a) {
|
|
16465
|
+
const modal = askEl();
|
|
16466
|
+
const body = modal.querySelector('.ask-body');
|
|
16467
|
+
Object.assign(askState, { ask: a, frame: null, frameH: 0, hidden: false, busy: false });
|
|
16468
|
+
body.replaceChildren();
|
|
16469
|
+
if (a) {
|
|
16470
|
+
modal.querySelector('.ask-title').textContent = a.title;
|
|
16471
|
+
modal.querySelector('.ask-box').dataset.size = a.size;
|
|
16472
|
+
if (a.content == null) {
|
|
16473
|
+
body.innerHTML = '<p class="ask-missing">The card file is gone. Skip, so the agent stops waiting.</p>';
|
|
16474
|
+
} else {
|
|
16475
|
+
const frame = createPreviewFrame('ask-frame', showSrcdoc(a.content, 0, ASK_BRIDGE_TAG), SHOW_SANDBOX);
|
|
16476
|
+
frame.addEventListener('load', () => {
|
|
16477
|
+
if (askState.frame === frame && !askState.hidden) frame.focus();
|
|
16478
|
+
});
|
|
16479
|
+
askState.frame = frame;
|
|
16480
|
+
body.append(frame);
|
|
16481
|
+
}
|
|
16482
|
+
}
|
|
16483
|
+
renderAsk();
|
|
16484
|
+
}
|
|
16485
|
+
|
|
16486
|
+
function renderAsk() {
|
|
16487
|
+
const a = askState.ask;
|
|
16488
|
+
askEl().hidden = !a || askState.hidden;
|
|
16489
|
+
const chip = document.getElementById('ask-reopen');
|
|
16490
|
+
chip.hidden = !a || !askState.hidden;
|
|
16491
|
+
if (a) {
|
|
16492
|
+
chip.querySelector('.ask-reopen-n').textContent = a.title;
|
|
16493
|
+
chip.title = `Open the question: ${a.title}`;
|
|
16494
|
+
}
|
|
16495
|
+
layoutAsk();
|
|
16496
|
+
}
|
|
16497
|
+
|
|
16498
|
+
// A full card fills the modal; a smaller one takes its content's height, up to the terminal's.
|
|
16499
|
+
function layoutAsk() {
|
|
16500
|
+
const frame = askState.frame;
|
|
16501
|
+
const box = askEl().querySelector('.ask-box');
|
|
16502
|
+
if (!frame || askEl().hidden || box.dataset.size === 'full') return;
|
|
16503
|
+
const host = document.getElementById('terminal-host');
|
|
16504
|
+
const chrome = box.querySelector('.ask-head').offsetHeight + box.querySelector('.ask-foot').offsetHeight + 2;
|
|
16505
|
+
frame.style.height = `${Math.max(SHOW_MIN_H, Math.min(askState.frameH || SHOW_MIN_H, host.clientHeight - 32 - chrome))}px`;
|
|
16506
|
+
}
|
|
16507
|
+
|
|
16508
|
+
function hideAsk() {
|
|
16509
|
+
askState.hidden = true;
|
|
16510
|
+
renderAsk();
|
|
16511
|
+
focusTerminalPane();
|
|
16512
|
+
}
|
|
16513
|
+
|
|
16514
|
+
function reopenAsk() {
|
|
16515
|
+
askState.hidden = false;
|
|
16516
|
+
renderAsk();
|
|
16517
|
+
askState.frame?.focus();
|
|
16518
|
+
}
|
|
16519
|
+
|
|
16520
|
+
async function endAsk(body) {
|
|
16521
|
+
const a = askState.ask;
|
|
16522
|
+
const tid = askState.terminalId;
|
|
16523
|
+
if (!a || !tid || askState.busy) return;
|
|
16524
|
+
askState.busy = true;
|
|
16525
|
+
try {
|
|
16526
|
+
const res = await terminalFetch(apiPath`/api/terminals/${tid}/ask/${a.id}`, 'POST', body);
|
|
16527
|
+
if (res.status === 404) showToast('The question is no longer open');
|
|
16528
|
+
else if (!res.ok) {
|
|
16529
|
+
const out = await res.json().catch(() => ({}));
|
|
16530
|
+
return showToast(out.error || `Send failed (HTTP ${res.status})`, 'error');
|
|
16531
|
+
}
|
|
16532
|
+
if (askState.ask?.id !== a.id) return;
|
|
16533
|
+
setAsk(null);
|
|
16534
|
+
focusTerminalPane();
|
|
16535
|
+
} catch {
|
|
16536
|
+
showToast('Failed to send the answer', 'error');
|
|
16537
|
+
} finally {
|
|
16538
|
+
if (askState.ask?.id === a.id) askState.busy = false;
|
|
16539
|
+
}
|
|
16540
|
+
}
|
|
16541
|
+
|
|
16542
|
+
function answerAsk(data) {
|
|
16543
|
+
return endAsk({ status: 'answered', data });
|
|
16544
|
+
}
|
|
16545
|
+
|
|
16546
|
+
function onAskOpened(d) {
|
|
16547
|
+
if (typeof d.terminalId !== 'string') return;
|
|
16548
|
+
if (d.terminalId === askState.terminalId) return loadAsk(d.terminalId);
|
|
16549
|
+
if (askState.terminalId || !wantsTerminal()) return;
|
|
16550
|
+
if (!terminalIds.has(currentSessionId)) showState.lookup = null;
|
|
16551
|
+
syncShowCard();
|
|
16552
|
+
}
|
|
16553
|
+
|
|
16554
|
+
function onAskClosed(d) {
|
|
16555
|
+
if (d.terminalId !== askState.terminalId || askState.ask?.id !== d.id) return;
|
|
16556
|
+
if (d.status === 'cancelled') showToast('The agent stopped waiting for the answer');
|
|
16557
|
+
setAsk(null);
|
|
16558
|
+
}
|
|
16559
|
+
|
|
16560
|
+
// A script can send without a click, so a send needs transient user activation, as a card action does.
|
|
16561
|
+
window.addEventListener('message', (e) => {
|
|
16562
|
+
const frame = askState.frame;
|
|
16563
|
+
if (!frame || e.source !== frame.contentWindow || typeof e.data?.type !== 'string') return;
|
|
16564
|
+
if (e.data.type === `${ASK_MSG}send`) {
|
|
16565
|
+
if (navigator.userActivation?.isActive) answerAsk(e.data.data);
|
|
16566
|
+
} else if (e.data.type === `${SHOW_MSG}height`) {
|
|
16567
|
+
askState.frameH = Number(e.data.h) || askState.frameH;
|
|
16568
|
+
layoutAsk();
|
|
16569
|
+
}
|
|
16570
|
+
});
|
|
16571
|
+
|
|
16572
|
+
function initAsk() {
|
|
16573
|
+
const modal = askEl();
|
|
16574
|
+
modal.addEventListener('click', (e) => {
|
|
16575
|
+
const cmd = e.target === modal ? 'hide' : e.target.closest('[data-ask]')?.dataset.ask;
|
|
16576
|
+
if (cmd === 'hide') hideAsk();
|
|
16577
|
+
else if (cmd === 'skip') endAsk({ status: 'dismissed' });
|
|
16578
|
+
});
|
|
16579
|
+
document.getElementById('ask-reopen').addEventListener('click', reopenAsk);
|
|
16580
|
+
// Capture, ahead of the board's own Escape. The card's frame hands Escape back as a key on the frame.
|
|
16581
|
+
window.addEventListener(
|
|
16582
|
+
'keydown',
|
|
16583
|
+
(e) => {
|
|
16584
|
+
if (e.key !== 'Escape' || modal.hidden || !modal.contains(e.target)) return;
|
|
16585
|
+
e.preventDefault();
|
|
16586
|
+
e.stopImmediatePropagation();
|
|
16587
|
+
hideAsk();
|
|
16588
|
+
},
|
|
16589
|
+
true,
|
|
16590
|
+
);
|
|
16591
|
+
window.addEventListener('resize', layoutAsk);
|
|
16592
|
+
}
|
|
16593
|
+
//#endregion
|
|
16594
|
+
|
|
16372
16595
|
//#region SHOW
|
|
16373
16596
|
// The card the session in the embedded terminal posts to with the show tool. It belongs to the
|
|
16374
16597
|
// terminal, not the session: /clear and the --resume picker change the session id, the terminal id
|
|
@@ -16581,6 +16804,7 @@ function syncShowCard() {
|
|
|
16581
16804
|
loadTerminals().catch(() => {});
|
|
16582
16805
|
}
|
|
16583
16806
|
const tid = pty ? (terminalIds.get(pty) ?? null) : null;
|
|
16807
|
+
syncAsk(tid);
|
|
16584
16808
|
if (tid === showState.terminalId) return;
|
|
16585
16809
|
const view = (tid && showViews[tid]) || { collapsed: true };
|
|
16586
16810
|
Object.assign(showState, { terminalId: tid, sessionId: null, idx: 0 });
|
|
@@ -16834,8 +17058,8 @@ function withHeadFirst(content, head) {
|
|
|
16834
17058
|
const FRAME_SCROLLBAR_CSS = ':where(html){scrollbar-width:thin;scrollbar-color:rgba(128,128,128,.45) transparent}';
|
|
16835
17059
|
|
|
16836
17060
|
// A whole document gets the tokens and base styles first in its head, so its own styles win.
|
|
16837
|
-
function showSrcdoc(content, restoreY) {
|
|
16838
|
-
const head = `<meta http-equiv="Content-Security-Policy" content="${SHOW_CSP}"><style>:root{${showTokenCss()}}${SHOW_FRAME_BASE}${FRAME_SCROLLBAR_CSS}</style
|
|
17061
|
+
function showSrcdoc(content, restoreY, extraHead = '') {
|
|
17062
|
+
const head = `<meta http-equiv="Content-Security-Policy" content="${SHOW_CSP}"><style>:root{${showTokenCss()}}${SHOW_FRAME_BASE}${FRAME_SCROLLBAR_CSS}</style>${extraHead}`;
|
|
16839
17063
|
const bridge = `<script>(${showBridge})(${Math.max(0, Math.round(restoreY) || 0)}, ${modalZoom});</script>${REVIEW_BRIDGE_TAG}`;
|
|
16840
17064
|
if (!/^\s*(<!doctype|<html)/i.test(content)) {
|
|
16841
17065
|
return `<!doctype html><html><head><meta charset="utf-8">${head}</head><body>${content}${bridge}</body></html>`;
|
|
@@ -16941,7 +17165,7 @@ new MutationObserver(() => {
|
|
|
16941
17165
|
|
|
16942
17166
|
// Builds the message only when a frame is there, because showTokens forces a style recalc.
|
|
16943
17167
|
function postToShowFrames(makeMsg) {
|
|
16944
|
-
const frames = [showState.frame, ...document.querySelectorAll('.show-pane-frame, .themed-frame')].filter(
|
|
17168
|
+
const frames = [showState.frame, ...document.querySelectorAll('.show-pane-frame, .themed-frame, .ask-frame')].filter(
|
|
16945
17169
|
(f) => f?.contentWindow,
|
|
16946
17170
|
);
|
|
16947
17171
|
if (!frames.length) return;
|
|
@@ -16999,10 +17223,15 @@ async function openShowInPane() {
|
|
|
16999
17223
|
}
|
|
17000
17224
|
|
|
17001
17225
|
// `onScreen` is postPane's result: null failed, true is already in view unless the terminal covers it.
|
|
17226
|
+
function revealPane(sid, onScreen) {
|
|
17227
|
+
if (onScreen && !wantsTerminal()) syncPanes();
|
|
17228
|
+
else if (sid === currentSessionId && wantsTerminal()) toggleTerminal();
|
|
17229
|
+
else openSession(sid);
|
|
17230
|
+
}
|
|
17231
|
+
|
|
17002
17232
|
function toastPaneAdded(sid, onScreen, label) {
|
|
17003
17233
|
if (onScreen === null || (onScreen && !wantsTerminal())) return;
|
|
17004
|
-
|
|
17005
|
-
showToast(`${label} added as a pane`, 'success', { label: 'Open', onClick: open });
|
|
17234
|
+
showToast(`${label} added as a pane`, 'success', { label: 'Open', onClick: () => revealPane(sid, false) });
|
|
17006
17235
|
}
|
|
17007
17236
|
|
|
17008
17237
|
function showCommand(cmd) {
|
|
@@ -17876,6 +18105,7 @@ initSessionGroupsDnd();
|
|
|
17876
18105
|
initSessionPicker();
|
|
17877
18106
|
initProjectPicker();
|
|
17878
18107
|
initShowCard();
|
|
18108
|
+
initAsk();
|
|
17879
18109
|
for (const k of readStoredList(ACTIVITY_FILTER_KEY)) activityFilter.add(k);
|
|
17880
18110
|
initSidebarResize();
|
|
17881
18111
|
applyModalZoom();
|
package/public/index.html
CHANGED
|
@@ -362,6 +362,10 @@
|
|
|
362
362
|
<div id="terminal-pane" class="terminal-pane">
|
|
363
363
|
<div class="terminal-bar">
|
|
364
364
|
<span id="terminal-status" class="terminal-status"></span>
|
|
365
|
+
<button id="ask-reopen" class="show-reopen ask-reopen" type="button" hidden>
|
|
366
|
+
<span class="ask-dot" aria-hidden="true">?</span>
|
|
367
|
+
<span class="ask-reopen-n"></span>
|
|
368
|
+
</button>
|
|
365
369
|
<button id="show-reopen" class="show-reopen" type="button" title="Show the overlay" hidden>
|
|
366
370
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M13 4v7h8"/></svg>
|
|
367
371
|
<span class="show-reopen-n">show</span>
|
|
@@ -375,6 +379,23 @@
|
|
|
375
379
|
</div>
|
|
376
380
|
<div id="terminal-host" class="terminal-host">
|
|
377
381
|
<section id="show-card" class="show-card" aria-label="Show overlay"></section>
|
|
382
|
+
<div id="ask-modal" class="ask-modal" hidden>
|
|
383
|
+
<section class="ask-box" role="dialog" aria-modal="true" aria-labelledby="ask-title">
|
|
384
|
+
<header class="ask-head">
|
|
385
|
+
<span class="ask-dot" aria-hidden="true">?</span>
|
|
386
|
+
<span id="ask-title" class="ask-title"></span>
|
|
387
|
+
<span class="ask-hint">Esc hides</span>
|
|
388
|
+
<button type="button" class="ask-x" data-ask="hide" title="Hide. The question stays open." aria-label="Hide">
|
|
389
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
|
390
|
+
</button>
|
|
391
|
+
</header>
|
|
392
|
+
<div class="ask-body"></div>
|
|
393
|
+
<footer class="ask-foot">
|
|
394
|
+
<span>The agent waits for your answer.</span>
|
|
395
|
+
<button type="button" data-ask="skip">Skip</button>
|
|
396
|
+
</footer>
|
|
397
|
+
</section>
|
|
398
|
+
</div>
|
|
378
399
|
</div>
|
|
379
400
|
<textarea id="terminal-key-proxy" class="terminal-key-proxy" tabindex="-1" aria-hidden="true"></textarea>
|
|
380
401
|
</div>
|
|
@@ -557,13 +578,13 @@
|
|
|
557
578
|
<button id="session-info-sticky-btn" class="icon-btn" style="display:none" title="Sticky pin — always show in the list" onclick="toggleSessionSticky(_infoModalSessionId); updateStickyBtnState()">
|
|
558
579
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2 L15 9 L22 9 L17 14 L19 22 L12 18 L5 22 L7 14 L2 9 L9 9 Z"/></svg>
|
|
559
580
|
</button>
|
|
560
|
-
<button id="session-info-cost-btn" class="icon-btn" style="display:none" title="Open in Cost" onclick="openCost
|
|
581
|
+
<button id="session-info-cost-btn" class="icon-btn" style="display:none" title="Open in Cost (Shift: Cost tab)" onclick="openAppForInfoModal(openCost, event)">
|
|
561
582
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"/><path d="M12 18V6"/></svg>
|
|
562
583
|
</button>
|
|
563
|
-
<button id="session-info-marketplace-btn" class="icon-btn" style="display:none" title="Open in Marketplace" onclick="
|
|
584
|
+
<button id="session-info-marketplace-btn" class="icon-btn" style="display:none" title="Open in Marketplace (Shift: Marketplace tab)" onclick="openAppForInfoModal(openMarketplace, event)">
|
|
564
585
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 01-8 0"/></svg>
|
|
565
586
|
</button>
|
|
566
|
-
<button id="session-info-memory-btn" class="icon-btn" style="display:none" title="Open in Memory" onclick="
|
|
587
|
+
<button id="session-info-memory-btn" class="icon-btn" style="display:none" title="Open in Memory (Shift: Memory tab)" onclick="openAppForInfoModal(openMemory, event)">
|
|
567
588
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>
|
|
568
589
|
</button>
|
|
569
590
|
<button class="modal-close" aria-label="Close dialog" onclick="closeTeamModal()">
|