@zq-silk/yui 2.16.0 → 2.17.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/dist/cli/commandCatalog.js +17 -11
- package/dist/cli/interactionCandidates.js +20 -5
- package/dist/cli/interactionPolicy.js +5 -4
- package/dist/cli/roleWizard.js +7 -0
- package/dist/commands/globalRoleCommands.js +13 -2
- package/dist/commands/operatorCommands.js +2 -0
- package/dist/commands/profileCommands.js +19 -2
- package/dist/commands/roleConfiguration.js +3 -0
- package/dist/commands/taskCommands.js +16 -0
- package/dist/commands/taskRoleRuntimeStatus.js +3 -0
- package/dist/controller/agentJobControl.js +9 -1
- package/dist/controller/fileSchedulerStoreAdapter.js +35 -4
- package/dist/controller/globalInputDelivery.js +14 -8
- package/dist/controller/providerRetryAdmission.js +9 -0
- package/dist/controller/providerRetryDelivery.js +16 -0
- package/dist/controller/runtime.js +14 -0
- package/dist/controller/runtimeLaunchCoordinator.js +3 -1
- package/dist/executor/fileRoleLaunchPlanner.js +3 -0
- package/dist/message/messageContinuation.js +4 -0
- package/dist/output/rolePresentation.js +3 -0
- package/dist/profile/agentProfile.js +8 -0
- package/dist/profile/agentProfileRuntime.js +16 -1
- package/dist/review/reviewDecision.js +6 -0
- package/dist/role/availability.js +28 -0
- package/dist/role/role.js +6 -0
- package/dist/runtime/codexNativeAccess.js +21 -4
- package/dist/runtime/tmuxAdapters.js +5 -0
- package/dist/scheduler/activeRoleRunDelivery.js +9 -0
- package/dist/scheduler/leaderWakeupProcessor.js +8 -1
- package/dist/scheduler/roleRunLiveness.js +3 -0
- package/dist/storage/sqliteSchema.js +10 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/web/assets/client/app/main.js +15 -22
- package/dist/web/assets/client/app/taskView.js +7 -11
- package/dist/web/assets/client/i18n/en.js +62 -18
- package/dist/web/assets/client/i18n/zh-CN.js +62 -18
- package/dist/web/assets/client/index.js +2 -0
- package/dist/web/assets/client/layout/shortcuts.js +2 -2
- package/dist/web/assets/client/layout/workspace.js +99 -112
- package/dist/web/assets/client/ui/primitives.js +15 -2
- package/dist/web/assets/client/views/dock/conversation.js +421 -123
- package/dist/web/assets/client/views/dock/session.js +229 -0
- package/dist/web/assets/client/views/dock/terminal.js +48 -39
- package/dist/web/assets/client/views/overview.js +2 -2
- package/dist/web/assets/client/views/task/page.js +2 -12
- package/dist/web/assets/shell/dock.js +9 -27
- package/dist/web/assets/styles/views/conversation.js +96 -17
- package/dist/web/assets/styles/views/dock.js +17 -5
- package/dist/web/assets/styles/views/task.js +1 -2
- package/dist/web/assets/styles/views/terminal.js +12 -8
- package/dist/web/webConversation.js +1 -0
- package/dist/web/webSettings.js +10 -2
- package/docs/managed-turn-and-session-runtime.md +10 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +6 -0
- package/docs/release-workflow.md +24 -0
- package/docs/release-workflow.zh-CN.md +19 -0
- package/docs/roles-and-configuration.md +45 -1
- package/docs/roles-and-configuration.zh-CN.md +35 -0
- package/docs/storage-baseline.md +6 -1
- package/docs/storage-baseline.zh-CN.md +5 -1
- package/docs/testing/review-evaluation.md +35 -1
- package/docs/testing/review-evaluation.zh-CN.md +25 -1
- package/docs/testing/verification-levels.md +2 -2
- package/docs/testing/verification-levels.zh-CN.md +2 -2
- package/package.json +1 -1
- package/skills/yui-leader/VERSION +1 -1
- package/skills/yui-leader/references/execution.md +19 -8
- package/skills/yui-reviewer/SKILL.md +13 -2
- package/skills/yui-reviewer/VERSION +1 -1
|
@@ -2,61 +2,90 @@ export const CONVERSATION_SCRIPT = String.raw `
|
|
|
2
2
|
import { h, clear, icon } from "/assets/js/lib/dom.js";
|
|
3
3
|
import { api, releaseMutation, requestJson, submitMutation } from "/assets/js/lib/api.js";
|
|
4
4
|
import { richText } from "/assets/js/ui/text.js";
|
|
5
|
-
import {
|
|
5
|
+
import { formatDateTime, formatShort, fill } from "/assets/js/lib/format.js";
|
|
6
6
|
import { inputCard } from "/assets/js/domain/work.js";
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
// options.tools: a node placed in the header row (the session page's mode
|
|
9
|
+
// switch). options.onSelection(sessionId, currentSessionId) reports the chosen
|
|
10
|
+
// Session; currentSessionId is undefined until the owner's facts are read.
|
|
11
|
+
export function createConversationController(host, t, locale = function () { return "en"; }, options = {}) {
|
|
9
12
|
let owner = null, selected = null, facts = null, next = null, latestItems = [], olderItems = [];
|
|
10
13
|
let generation = 0, busy = false, timer = null, pending = null, historyOk = false;
|
|
11
14
|
let recoveryUnavailable = false;
|
|
15
|
+
// A background read (the poll) holds busy only to serialize requests. It
|
|
16
|
+
// never disables controls, so an open picker or a typed draft does not grey
|
|
17
|
+
// out every cycle; an action started meanwhile waits for the read to finish
|
|
18
|
+
// and then revalidates instead of being dropped or racing it.
|
|
19
|
+
let reading = false, readWaiters = [];
|
|
12
20
|
let rendered = new Map();
|
|
13
21
|
let materials = [], incomingMaterials = null;
|
|
14
|
-
let requestSignature = "";
|
|
22
|
+
let requestSignature = "", sessionSignature = "", detailSignature = "";
|
|
15
23
|
let modelPending = null, modelCatalog = null, lastModelObservation = null;
|
|
24
|
+
// The selected Session's own native confirmation (or its stated absence).
|
|
25
|
+
let modelObservation = null;
|
|
26
|
+
let pageFacts = null;
|
|
16
27
|
const requests = h("div.cv-requests");
|
|
17
28
|
const questions = h("div.cv-requests");
|
|
18
29
|
let questionSignature = "";
|
|
30
|
+
// Native answer drafts typed in this view (discarded with it on a mode switch).
|
|
31
|
+
const nativeDraftKeys = new Set();
|
|
19
32
|
let confirmedModel = null, modelRowMap = new Map();
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
const
|
|
33
|
+
// Who/which Session: one row. The picker lists current and previous Sessions
|
|
34
|
+
// by readable name and time; exact identities live in the details menu.
|
|
35
|
+
const pickerName = h("span.cv-picker-name", null, t("conversation.sessions"));
|
|
36
|
+
const sessionList = h("div.cv-session-list", { role: "listbox", "aria-label": t("conversation.sessions") });
|
|
37
|
+
const moreSessions = h("button.cv-link.cv-more-sessions", { type: "button", hidden: true }, t("conversation.moreSessions"));
|
|
38
|
+
const picker = h("details.cv-pop.cv-picker", null,
|
|
39
|
+
h("summary.cv-picker-btn", { "aria-label": t("conversation.sessions"), title: t("conversation.sessions") },
|
|
40
|
+
pickerName, icon("down", "cv-picker-chevron")),
|
|
41
|
+
h("div.cv-popbody", null, sessionList, moreSessions));
|
|
42
|
+
const status = h("span.cv-status", { role: "status" });
|
|
43
|
+
const detailFacts = h("dl.cv-facts");
|
|
31
44
|
const identity = h("p.cv-identity");
|
|
45
|
+
const copyIdentity = h("button.cv-link", { type: "button" }, icon("copy"), h("span", null, t("conversation.copyIdentity")));
|
|
46
|
+
const liveNote = h("p.cv-note");
|
|
32
47
|
const feed = h("div.feed", { tabIndex: 0, "aria-label": t("conversation.history") });
|
|
33
48
|
const text = h("textarea.cv-input", { rows: 2, placeholder: t("conversation.placeholder"), "aria-label": t("conversation.input"), maxLength: 12000 });
|
|
34
49
|
const receipt = h("p.cv-note", { role: "status" });
|
|
35
50
|
const draftStatus = h("p.cv-note", { role: "status" });
|
|
36
51
|
const receiptDetail = h("pre.cv-code");
|
|
52
|
+
const receiptBlock = h("details.cv-sub", { hidden: true }, h("summary", null, t("conversation.receiptDetails")), receiptDetail);
|
|
53
|
+
// Two facts only: what the Session last confirmed, and what is selected.
|
|
54
|
+
// Their nodes are built once and updated in place on each poll.
|
|
55
|
+
const modelFactNodes = [0, 1].map(function () {
|
|
56
|
+
const value = h("span.cv-mf-value"), note = h("small.cv-mf-note");
|
|
57
|
+
return { term: h("dt"), value: value, note: note, detail: h("dd", null, value, note) };
|
|
58
|
+
});
|
|
59
|
+
const modelFacts = h("dl.cv-facts.cv-model-facts", null,
|
|
60
|
+
modelFactNodes[0].term, modelFactNodes[0].detail, modelFactNodes[1].term, modelFactNodes[1].detail);
|
|
37
61
|
const modelStatus = h("p.cv-note", { role: "status" });
|
|
38
62
|
const modelResult = h("p.cv-note", { role: "status" });
|
|
39
63
|
const modelSelect = h("select", { "aria-label": t("conversation.model"), hidden: true });
|
|
40
64
|
const modelRows = h("div.cv-options", { role: "listbox", "aria-label": t("conversation.selectModel") });
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
h("
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
65
|
+
// The pill names its state in words (not only the dot's colour) so the
|
|
66
|
+
// source reads without hovering: last confirmed, applying, or unconfirmed.
|
|
67
|
+
const modelState = h("span.cv-model-state");
|
|
68
|
+
const modelLabel = h("span.cv-model-label", null, t("conversation.modelUnconfirmed"));
|
|
69
|
+
const modelSummary = h("summary.cv-pill", { "aria-label": t("conversation.model"), title: t("conversation.model") },
|
|
70
|
+
h("i.cv-model-dot", { "aria-hidden": "true" }), modelState, modelLabel, icon("down"));
|
|
71
|
+
const modelLoad = h("button.cv-link", { type: "button", hidden: true }, t("conversation.modelsLoad"));
|
|
72
|
+
const modelApply = h("button.btn.btn-primary.cv-apply", { type: "button" }, t("conversation.modelApply"));
|
|
73
|
+
const modelMenu = h("details.cv-pop.cv-model", null, modelSummary,
|
|
74
|
+
h("div.cv-popbody", null, modelFacts, modelRows, modelSelect,
|
|
75
|
+
h("div.cv-pop-actions", null, h("div.cv-model-notes", null, modelStatus, modelResult), modelLoad, modelApply)));
|
|
50
76
|
const slash = h("div.conversation-slash", { "aria-label": t("conversation.commands") });
|
|
51
77
|
const literal = h("button.cv-link", { type: "button", hidden: true }, t("conversation.literal"));
|
|
52
|
-
const send = h("button.cv-send", { type: "button", "aria-label": t("conversation.send"), title: t("conversation.
|
|
78
|
+
const send = h("button.cv-send", { type: "button", "aria-label": t("conversation.send"), title: t("conversation.sendHint") }, icon("up"));
|
|
53
79
|
const stop = h("button.cv-stop", { type: "button", hidden: true, "aria-label": t("conversation.stop"), title: t("conversation.stop") }, icon("stop"));
|
|
54
80
|
const check = h("button.cv-link", { type: "button", hidden: true }, t("conversation.receipt"));
|
|
55
|
-
const older = h("button.cv-link", { type: "button", hidden: true }, t("conversation.older"));
|
|
81
|
+
const older = h("button.cv-link.cv-older", { type: "button", hidden: true }, t("conversation.older"));
|
|
56
82
|
const latest = h("button.cv-jump", { type: "button", hidden: true, "aria-label": t("conversation.jump"), title: t("conversation.latest") }, icon("down"));
|
|
57
|
-
const current = h("button.cv-link", { type: "button", hidden: true }, t("conversation.
|
|
58
|
-
|
|
59
|
-
|
|
83
|
+
const current = h("button.cv-link.cv-back", { type: "button", hidden: true, title: t("conversation.backCurrent") }, icon("back"), h("span", null, t("conversation.backCurrentShort")));
|
|
84
|
+
// Why input is unavailable, in one line above the composer, instead of a
|
|
85
|
+
// silently disabled send button and stacked engineering notes.
|
|
86
|
+
const noticeText = h("span");
|
|
87
|
+
const noticeCurrent = h("button.cv-link", { type: "button", hidden: true }, t("conversation.backCurrent"));
|
|
88
|
+
const notice = h("p.cv-notice", { role: "status", hidden: true }, icon("info"), noticeText, noticeCurrent);
|
|
60
89
|
const file = h("input", { type: "file", "aria-label": t("materials.attach") });
|
|
61
90
|
const materialList = h("div.row-stack");
|
|
62
91
|
const materialAlert = h("p.cv-note", { role: "alert" });
|
|
@@ -65,25 +94,51 @@ export function createConversationController(host, t, locale = function () { ret
|
|
|
65
94
|
h("div.cv-popbody", null, h("p.cv-pop-title", null, t("materials.attach")), file,
|
|
66
95
|
h("p.cv-note", null, t("materials.boundary"))));
|
|
67
96
|
const menu = h("details.cv-pop.cv-menu", null,
|
|
68
|
-
h("summary.cv-iconbtn", { "aria-label": t("conversation.
|
|
97
|
+
h("summary.cv-iconbtn", { "aria-label": t("conversation.details"), title: t("conversation.details") }, icon("dots")),
|
|
69
98
|
h("div.cv-popbody", null,
|
|
70
|
-
h("
|
|
71
|
-
|
|
72
|
-
h("details.cv-sub", null, h("summary", null, t("conversation.receiptDetails")), receiptDetail),
|
|
99
|
+
h("p.cv-pop-title", null, t("conversation.details")), detailFacts,
|
|
100
|
+
h("div.cv-identity-row", null, identity, copyIdentity), liveNote, receiptBlock,
|
|
73
101
|
h("p.cv-note", null, t("conversation.boundary"))));
|
|
74
|
-
const pops = [menu, attachMenu, modelMenu];
|
|
102
|
+
const pops = [menu, attachMenu, modelMenu, picker];
|
|
75
103
|
pops.forEach(function (pop) {
|
|
76
104
|
pop.addEventListener("toggle", function () {
|
|
77
105
|
if (!pop.open) return;
|
|
78
106
|
pops.forEach(function (other) { if (other !== pop) other.open = false; });
|
|
79
107
|
if (pop === modelMenu && !modelCatalog && !modelLoad.disabled) loadModels();
|
|
108
|
+
else if (pop === modelMenu) focusOption(modelRowMap.get(modelSelect.value) || modelRows.querySelector("button"));
|
|
109
|
+
if (pop === picker) focusOption(sessionList.querySelector("[aria-selected=true]") || sessionList.querySelector("button"));
|
|
80
110
|
});
|
|
81
111
|
});
|
|
82
112
|
host.addEventListener("click", function (event) {
|
|
83
113
|
pops.forEach(function (pop) { if (pop.open && !pop.contains(event.target)) pop.open = false; });
|
|
84
114
|
});
|
|
115
|
+
// Escape closes only the open popover; the dock-level Escape (hide the
|
|
116
|
+
// dock) must not also fire for the same key press.
|
|
85
117
|
host.addEventListener("keydown", function (event) {
|
|
86
|
-
if (event.key
|
|
118
|
+
if (event.key !== "Escape") return;
|
|
119
|
+
pops.forEach(function (pop) {
|
|
120
|
+
if (!pop.open) return;
|
|
121
|
+
event.stopPropagation();
|
|
122
|
+
pop.open = false;
|
|
123
|
+
pop.querySelector("summary")?.focus?.();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
sessionList.addEventListener("keydown", function (event) {
|
|
127
|
+
const options = [...sessionList.querySelectorAll("button")];
|
|
128
|
+
const index = options.indexOf(document.activeElement);
|
|
129
|
+
const target = event.key === "ArrowDown" ? options[Math.min(options.length - 1, index + 1)]
|
|
130
|
+
: event.key === "ArrowUp" ? options[Math.max(0, index - 1)]
|
|
131
|
+
: event.key === "Home" ? options[0] : event.key === "End" ? options[options.length - 1] : null;
|
|
132
|
+
if (!target) return;
|
|
133
|
+
event.preventDefault(); focusOption(target);
|
|
134
|
+
});
|
|
135
|
+
modelRows.addEventListener("keydown", function (event) {
|
|
136
|
+
const rows = [...modelRowMap.values()];
|
|
137
|
+
const index = rows.indexOf(document.activeElement);
|
|
138
|
+
const target = event.key === "ArrowDown" ? rows[Math.min(rows.length - 1, index + 1)]
|
|
139
|
+
: event.key === "ArrowUp" ? rows[Math.max(0, index - 1)]
|
|
140
|
+
: event.key === "Home" ? rows[0] : event.key === "End" ? rows[rows.length - 1] : null;
|
|
141
|
+
if (target) { event.preventDefault(); focusOption(target); }
|
|
87
142
|
});
|
|
88
143
|
feed.addEventListener("scroll", function () {
|
|
89
144
|
syncJump();
|
|
@@ -91,15 +146,51 @@ export function createConversationController(host, t, locale = function () { ret
|
|
|
91
146
|
if (feed.scrollTop < 120 && next && historyOk && !busy) refresh(true);
|
|
92
147
|
});
|
|
93
148
|
host.append(
|
|
94
|
-
h("div.cv-head", null, h("
|
|
95
|
-
h("div.cv-
|
|
96
|
-
h("div.cv-stage", null, feed, latest),
|
|
149
|
+
h("div.cv-head", null, picker, current, h("span.spacer"), status, options.tools || null, menu),
|
|
150
|
+
h("div.cv-stage", null, older, feed, latest),
|
|
97
151
|
questions, requests,
|
|
98
|
-
h("div.cv-composer", null, slash,
|
|
152
|
+
h("div.cv-composer", null, slash, notice,
|
|
99
153
|
h("div.cv-box", null, materialList, materialAlert, text,
|
|
100
|
-
h("div.cv-tools", null, attachMenu,
|
|
154
|
+
h("div.cv-tools", null, attachMenu, h("span.spacer"), modelMenu, stop, send)),
|
|
101
155
|
h("div.cv-hint", null, draftStatus, literal, check, receipt)));
|
|
102
156
|
|
|
157
|
+
function focusOption(option) { if (option && typeof option.focus === "function") option.focus(); }
|
|
158
|
+
// Write only real changes so an unchanged poll leaves the DOM untouched.
|
|
159
|
+
function setText(node, value) { if (node.textContent !== value) node.textContent = value; }
|
|
160
|
+
function setChildren(node, value) { if (node.textContent !== value) node.replaceChildren(value); }
|
|
161
|
+
function setProp(node, name, value) { if (node[name] !== value) node[name] = value; }
|
|
162
|
+
function setAttr(node, name, value) {
|
|
163
|
+
if ((node.getAttribute ? node.getAttribute(name) : node[name]) !== value) node.setAttribute(name, value);
|
|
164
|
+
}
|
|
165
|
+
async function afterRead() {
|
|
166
|
+
const own = generation;
|
|
167
|
+
while (reading) await new Promise(function (resolve) { readWaiters.push(resolve); });
|
|
168
|
+
return own === generation;
|
|
169
|
+
}
|
|
170
|
+
function endRead() {
|
|
171
|
+
reading = false;
|
|
172
|
+
const waiting = readWaiters; readWaiters = [];
|
|
173
|
+
waiting.forEach(function (resolve) { resolve(); });
|
|
174
|
+
}
|
|
175
|
+
function report() {
|
|
176
|
+
if (options.onSelection) options.onSelection(selected, facts ? facts.currentSessionId || null : undefined);
|
|
177
|
+
}
|
|
178
|
+
function selectedSession() {
|
|
179
|
+
return facts && facts.sessions.find(function (s) { return s.nativeSessionId === selected; }) || null;
|
|
180
|
+
}
|
|
181
|
+
function ownerLabel() {
|
|
182
|
+
if (!owner) return "";
|
|
183
|
+
return owner.scope === "global" ? owner.roleName === "operator" ? t("actions.operator") : owner.roleName
|
|
184
|
+
: owner.roleName + " · " + owner.taskId;
|
|
185
|
+
}
|
|
186
|
+
function sessionWhen(s) { return formatShort(s.createdAt, locale()) || "—"; }
|
|
187
|
+
function sessionLifecycle(s) {
|
|
188
|
+
return t(s.status === "active" ? "conversation.sessionActive"
|
|
189
|
+
: s.endReason === "failed" ? "conversation.sessionFailed" : "conversation.sessionEnded");
|
|
190
|
+
}
|
|
191
|
+
function sessionName(s) {
|
|
192
|
+
return (s.current ? t("conversation.current") : t("conversation.previous")) + " · " + (s.title || sessionWhen(s));
|
|
193
|
+
}
|
|
103
194
|
function key() { return "yui.conversation." + JSON.stringify(owner) + "." + selected; }
|
|
104
195
|
function saved(suffix, value, expectedRequestId) {
|
|
105
196
|
try {
|
|
@@ -135,36 +226,61 @@ export function createConversationController(host, t, locale = function () { ret
|
|
|
135
226
|
return "/api/conversation?" + q;
|
|
136
227
|
}
|
|
137
228
|
function syncJump() {
|
|
138
|
-
latest
|
|
229
|
+
setProp(latest, "hidden", !((selected && !historyOk) || feed.scrollHeight - feed.clientHeight - feed.scrollTop > 80));
|
|
230
|
+
setProp(older, "hidden", !next || !historyOk || feed.scrollTop > 160);
|
|
139
231
|
}
|
|
140
232
|
function controls() {
|
|
141
233
|
if (owner && selected) {
|
|
142
234
|
try { pending = JSON.parse(saved(".pending") || "null"); }
|
|
143
235
|
catch { recoveryUnavailable = true; }
|
|
144
236
|
}
|
|
145
|
-
|
|
146
|
-
|
|
237
|
+
const active = selectedSession();
|
|
238
|
+
// Task Roles other than the Leader keep their assignment controls; here they are read only.
|
|
239
|
+
const roleReadOnly = !!owner && owner.scope === "task" && owner.roleName !== "leader";
|
|
147
240
|
const writable = historyOk && active && active.current && active.status === "active" && active.adapterId === "codex"
|
|
148
|
-
&& facts.authority && facts.authority.owner === "controller" && !facts.terminalWriter;
|
|
241
|
+
&& facts.authority && facts.authority.owner === "controller" && !facts.terminalWriter && !roleReadOnly;
|
|
149
242
|
const turn = facts && facts.turn;
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
literal
|
|
243
|
+
const unsettled = turn && ["submitting", "delivery-unknown"].includes(turn.status);
|
|
244
|
+
const locked = busy && !reading;
|
|
245
|
+
setProp(send, "disabled", !!(locked || recoveryUnavailable || !!pending || !!modelPending || !writable || unsettled));
|
|
246
|
+
setProp(stop, "disabled", !!(locked || recoveryUnavailable || !!pending || !writable || !turn || turn.status !== "accepted" || !turn.nativeTurnId));
|
|
247
|
+
setProp(check, "disabled", !!(locked || !pending));
|
|
248
|
+
setProp(older, "disabled", !!(locked || !next));
|
|
249
|
+
setProp(check, "hidden", !pending);
|
|
250
|
+
setProp(stop, "hidden", !turn || turn.status !== "accepted");
|
|
251
|
+
setProp(current, "hidden", !facts || !selected || selected === facts.currentSessionId);
|
|
252
|
+
setProp(literal, "disabled", !!send.disabled);
|
|
160
253
|
syncJump();
|
|
161
|
-
modelLoad
|
|
162
|
-
modelApply
|
|
254
|
+
setProp(modelLoad, "disabled", !!(locked || !active || active.adapterId !== "codex"));
|
|
255
|
+
setProp(modelApply, "disabled", !!(locked || !!pending || !!modelPending || !writable || !modelSelect.value
|
|
256
|
+
|| modelSelect.value === confirmedModel
|
|
163
257
|
|| !modelCatalog || modelCatalog.source === "fallback" || !!modelCatalog.failure
|
|
164
|
-
|| turn && ["submitting", "accepted", "delivery-unknown"].includes(turn.status);
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
258
|
+
|| turn && ["submitting", "accepted", "delivery-unknown"].includes(turn.status)));
|
|
259
|
+
modelRowMap.forEach(function (row) { setProp(row, "disabled", locked || !!modelPending); });
|
|
260
|
+
const modelReason = modelPending ? (locked ? "" : "conversation.modelUnknown")
|
|
261
|
+
: active && !active.current ? "conversation.readOnlyHistory"
|
|
262
|
+
: facts && facts.terminalWriter ? "conversation.modelTerminalBusy"
|
|
263
|
+
: turn && ["submitting", "accepted", "delivery-unknown"].includes(turn.status) ? "conversation.modelBusy"
|
|
264
|
+
: active && (!writable || roleReadOnly) ? "conversation.inputUnavailable" : "";
|
|
265
|
+
setText(modelStatus, modelReason ? t(modelReason) : "");
|
|
266
|
+
setProp(attachMenu, "hidden", !owner || owner.scope !== "task");
|
|
267
|
+
setProp(file, "disabled", !!(locked || !!pending || !writable || materials.length >= 8));
|
|
268
|
+
materialList.querySelectorAll("button").forEach(function (control) { setProp(control, "disabled", locked || !!pending); });
|
|
269
|
+
// One plain reason for unavailable input; ordered by what the user can act on.
|
|
270
|
+
const reason = !facts || !selected || !historyOk || !active ? ""
|
|
271
|
+
: !active.current ? "conversation.readOnlyHistory"
|
|
272
|
+
: facts.terminalWriter ? "conversation.terminalWriter"
|
|
273
|
+
: roleReadOnly ? "conversation.roleReadOnly"
|
|
274
|
+
: recoveryUnavailable ? "conversation.storageFailed"
|
|
275
|
+
: pending ? "conversation.pendingInput"
|
|
276
|
+
: unsettled ? "conversation.turnUnsettled"
|
|
277
|
+
: modelPending ? "conversation.modelApplying"
|
|
278
|
+
: !writable ? "conversation.inputUnavailable" : "";
|
|
279
|
+
setProp(notice, "hidden", !reason);
|
|
280
|
+
setProp(notice, "className", "cv-notice" + (["conversation.terminalWriter", "conversation.readOnlyHistory", "conversation.roleReadOnly"].includes(reason) ? "" : " is-warn"));
|
|
281
|
+
setText(noticeText, reason ? t(reason) : "");
|
|
282
|
+
setProp(noticeCurrent, "hidden", reason !== "conversation.readOnlyHistory");
|
|
283
|
+
setProp(text, "placeholder", t(reason === "conversation.readOnlyHistory" ? "conversation.placeholderHistory" : "conversation.placeholder"));
|
|
168
284
|
}
|
|
169
285
|
function showReceipt(result) {
|
|
170
286
|
const refused = result.interrupt && result.interrupt.state === "not-interrupted";
|
|
@@ -174,72 +290,156 @@ export function createConversationController(host, t, locale = function () { ret
|
|
|
174
290
|
+ (result.messageId ? " · " + result.messageId : "")
|
|
175
291
|
+ (result.notDelivered ? " · " + (typeof result.notDelivered === "string" ? result.notDelivered : JSON.stringify(result.notDelivered)) : "");
|
|
176
292
|
receiptDetail.textContent = JSON.stringify(result, null, 2);
|
|
293
|
+
receiptBlock.hidden = false;
|
|
294
|
+
}
|
|
295
|
+
// Keep execution evidence and the user's selected target apart. A Codex observation is the
|
|
296
|
+
// most recent native confirmation (start, resume or an adopted switch) with
|
|
297
|
+
// its time, not a live reading of the current Turn or of Desktop changes, so
|
|
298
|
+
// it is labelled that way. A pending switch is shown as pending. An absent, unknown or unreadable confirmation
|
|
299
|
+
// is shown as unconfirmed: never filled from launch, Role or poll time.
|
|
300
|
+
function observedAxis(observation, category) {
|
|
301
|
+
if (!observation || observation.status !== "observed" || !Array.isArray(observation.axes)) return null;
|
|
302
|
+
const axis = observation.axes.find(function (a) { return a.category === category; });
|
|
303
|
+
return axis && axis.current && axis.current.status === "observed" ? axis.current.value : null;
|
|
177
304
|
}
|
|
305
|
+
function modelText(model, effort) { return model + (effort ? " · " + effort : ""); }
|
|
178
306
|
function showModel(observation) {
|
|
179
|
-
const
|
|
180
|
-
&& observation.axes.find(function (a) { return a.key === "model"; });
|
|
181
|
-
const value = axis && axis.current.status === "observed" ? axis.current.value : null;
|
|
182
|
-
const session = facts && facts.sessions.find(function (s) { return s.nativeSessionId === selected; });
|
|
307
|
+
const value = observedAxis(observation, "model");
|
|
183
308
|
confirmedModel = value;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (value === modelPending.model && observation.observedAt !== modelPending.previousObservation) {
|
|
190
|
-
modelPending = null; saved(".modelPending", null);
|
|
191
|
-
releaseMutation(url({ session: selected }).replace("/api/conversation?", "/api/conversation/model?"));
|
|
192
|
-
} else modelStatus.textContent += " · " + t("conversation.modelUnknown");
|
|
309
|
+
modelObservation = observation || null;
|
|
310
|
+
// An unresolved switch keeps its pending record; controls() owns the note.
|
|
311
|
+
if (modelPending && value === modelPending.model && observation.observedAt !== modelPending.previousObservation) {
|
|
312
|
+
modelPending = null; saved(".modelPending", null);
|
|
313
|
+
releaseMutation(url({ session: selected }).replace("/api/conversation?", "/api/conversation/model?"));
|
|
193
314
|
}
|
|
194
315
|
lastModelObservation = value ? observation.observedAt : null;
|
|
316
|
+
// A catalog read before the first confirmation leaves nothing chosen;
|
|
317
|
+
// default that untouched choice to the confirmed model, as a load does.
|
|
318
|
+
if (value && !modelPending && !modelSelect.value && modelRowMap.has(value)) { pickModel(value); return; }
|
|
319
|
+
drawModel();
|
|
195
320
|
}
|
|
196
|
-
function
|
|
197
|
-
|
|
321
|
+
function fact(list, label, value, note) {
|
|
322
|
+
list.append(h("dt", null, label), h("dd", null, h("span", null, value), note ? h("small", null, note) : null));
|
|
323
|
+
}
|
|
324
|
+
function modelName(value) {
|
|
325
|
+
const known = modelCatalog && (modelCatalog.catalog.models || []).find(function (m) { return m.value === value; });
|
|
326
|
+
return known && known.label && known.label !== value ? known.label : value;
|
|
327
|
+
}
|
|
328
|
+
function drawModel() {
|
|
329
|
+
const observation = modelObservation;
|
|
330
|
+
const value = confirmedModel;
|
|
331
|
+
const effort = value ? observedAxis(observation, "effort") : null;
|
|
332
|
+
// Dated, so a confirmation from yesterday is not read as today's.
|
|
333
|
+
const at = value ? formatDateTime(observation.observedAt, locale()) : "";
|
|
334
|
+
let state, tag, label, title;
|
|
335
|
+
if (modelPending) {
|
|
336
|
+
state = "pending"; tag = t("conversation.modelStatePending"); label = modelPending.model; title = t("conversation.modelApplying");
|
|
337
|
+
} else if (value) {
|
|
338
|
+
state = "confirmed"; tag = t("conversation.modelStateConfirmed"); label = modelText(value, effort);
|
|
339
|
+
title = fill(t("conversation.modelConfirmedShort"), { time: at });
|
|
340
|
+
} else {
|
|
341
|
+
state = "unknown"; tag = ""; label = t("conversation.modelUnconfirmed"); title = t("conversation.modelUnconfirmedHint");
|
|
342
|
+
}
|
|
343
|
+
if (modelMenu.dataset.state !== state) modelMenu.dataset.state = state;
|
|
344
|
+
setText(modelState, tag);
|
|
345
|
+
setProp(modelState, "hidden", !tag);
|
|
346
|
+
setText(modelLabel, label);
|
|
347
|
+
setAttr(modelSummary, "title", t("conversation.model") + " · " + title);
|
|
348
|
+
setAttr(modelSummary, "aria-label", t("conversation.model") + " · " + (tag ? tag + " " : "") + label);
|
|
349
|
+
const selection = modelPending ? modelPending.model : modelSelect.value;
|
|
350
|
+
const rows = [
|
|
351
|
+
[t("conversation.confirmedModel"), value ? modelText(modelName(value), effort) : t("conversation.modelUnconfirmed"),
|
|
352
|
+
value ? t("conversation.modelStateConfirmed") : "", value ? title : t("conversation.modelUnconfirmedHint"), !value],
|
|
353
|
+
[t("conversation.selectedModel"), selection ? modelName(selection) : t("conversation.modelNotSelected"),
|
|
354
|
+
modelPending ? t("conversation.modelStatePending") : "", selection || "", !selection]];
|
|
355
|
+
rows.forEach(function (row, index) {
|
|
356
|
+
const node = modelFactNodes[index];
|
|
357
|
+
setChildren(node.term, row[0]);
|
|
358
|
+
setChildren(node.value, row[1]);
|
|
359
|
+
setChildren(node.note, row[2]);
|
|
360
|
+
setProp(node.note, "hidden", !row[2]);
|
|
361
|
+
setAttr(node.detail, "title", row[3]);
|
|
362
|
+
const kind = "cv-mf-value" + (row[4] ? " is-empty" : "");
|
|
363
|
+
if (node.value.className !== kind) node.value.className = kind;
|
|
364
|
+
});
|
|
198
365
|
modelRowMap.forEach(function (row, key) {
|
|
199
|
-
|
|
200
|
-
row.
|
|
366
|
+
const kind = "cv-option" + (key === modelSelect.value ? " is-selected" : "") + (key === value ? " is-current" : "");
|
|
367
|
+
if (row.className !== kind) row.className = kind;
|
|
201
368
|
});
|
|
369
|
+
}
|
|
370
|
+
function pickModel(value) {
|
|
371
|
+
modelSelect.value = value;
|
|
372
|
+
modelRowMap.forEach(function (row, key) { setAttr(row, "aria-selected", key === value ? "true" : "false"); });
|
|
373
|
+
modelResult.textContent = "";
|
|
374
|
+
modelResult.title = "";
|
|
375
|
+
drawModel();
|
|
202
376
|
controls();
|
|
203
377
|
}
|
|
204
378
|
async function loadModels() {
|
|
205
|
-
if (
|
|
379
|
+
if (!await afterRead()) return;
|
|
380
|
+
controls();
|
|
381
|
+
if (modelLoad.disabled || busy) return;
|
|
206
382
|
const own = generation;
|
|
207
|
-
busy = true; controls();
|
|
383
|
+
busy = true; modelResult.textContent = t("conversation.modelsLoading"); controls();
|
|
208
384
|
try {
|
|
209
385
|
const result = await requestJson(url({ session: selected }).replace("/api/conversation?", "/api/conversation/model?"));
|
|
210
386
|
if (own !== generation) return;
|
|
387
|
+
const previousChoice = modelSelect.value;
|
|
211
388
|
modelCatalog = result; clear(modelSelect); clear(modelRows); modelRowMap = new Map();
|
|
212
389
|
(result.catalog.models || []).forEach(function (m) {
|
|
213
390
|
modelSelect.append(h("option", { value: m.value }, m.label + " · " + m.value));
|
|
214
|
-
|
|
215
|
-
|
|
391
|
+
// The native id is shown beside a differing label (it is what the
|
|
392
|
+
// pill and facts name), not in a hover tooltip.
|
|
393
|
+
const row = h("button.cv-option", { type: "button", role: "option", "aria-selected": "false" },
|
|
394
|
+
h("i.cv-option-mark", { "aria-hidden": "true" }), h("span.cv-option-name", null, m.label || m.value),
|
|
395
|
+
m.label && m.label !== m.value ? h("span.cv-option-id", null, m.value) : null, icon("check"));
|
|
216
396
|
row.addEventListener("click", function () { pickModel(m.value); });
|
|
217
397
|
modelRowMap.set(m.value, row); modelRows.append(row);
|
|
218
398
|
});
|
|
219
|
-
pickModel(modelRowMap.has(confirmedModel) ? confirmedModel : "");
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
399
|
+
pickModel(modelRowMap.has(previousChoice) ? previousChoice : modelRowMap.has(confirmedModel) ? confirmedModel : "");
|
|
400
|
+
modelLoad.hidden = !result.failure && result.source !== "fallback" && modelRowMap.size > 0;
|
|
401
|
+
modelResult.textContent = result.failure || result.source === "fallback" ? t("conversation.modelsFailed")
|
|
402
|
+
: !modelRowMap.size ? t("conversation.modelsEmpty") : "";
|
|
403
|
+
modelResult.title = result.failure?.message || "";
|
|
404
|
+
} catch (error) {
|
|
405
|
+
if (own === generation) {
|
|
406
|
+
modelCatalog = null; modelLoad.hidden = false;
|
|
407
|
+
modelResult.textContent = t("conversation.modelsFailed"); modelResult.title = error.message;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
finally {
|
|
411
|
+
finish(own);
|
|
412
|
+
if (own === generation && modelMenu.open) focusOption(modelRowMap.get(modelSelect.value) || modelRows.querySelector("button"));
|
|
413
|
+
}
|
|
224
414
|
}
|
|
225
415
|
async function applyModel() {
|
|
226
|
-
|
|
416
|
+
const choice = modelSelect.value;
|
|
417
|
+
if (!await afterRead() || choice !== modelSelect.value) return;
|
|
418
|
+
controls();
|
|
419
|
+
if (modelApply.disabled || busy) return;
|
|
227
420
|
const own = generation;
|
|
228
421
|
const endpoint = url({ session: selected }).replace("/api/conversation?", "/api/conversation/model?");
|
|
229
422
|
modelPending = { model: modelSelect.value, previousObservation: lastModelObservation };
|
|
230
423
|
saved(".modelPending", JSON.stringify(modelPending));
|
|
424
|
+
// The Host drops its own confirmation before adopting; so does this view.
|
|
425
|
+
confirmedModel = null; drawModel();
|
|
231
426
|
busy = true; controls();
|
|
232
427
|
modelResult.textContent = t("conversation.modelApplying");
|
|
233
428
|
try {
|
|
234
429
|
const result = await submitMutation(endpoint, endpoint, { model: modelPending.model });
|
|
235
430
|
if (own !== generation) return;
|
|
236
|
-
modelPending = null; saved(".modelPending", null);
|
|
237
431
|
showModel(result.runConfiguration);
|
|
238
|
-
modelResult.textContent = t("conversation.modelConfirmed");
|
|
432
|
+
modelResult.textContent = modelPending ? "" : t("conversation.modelConfirmed");
|
|
433
|
+
if (!modelPending && confirmedModel === modelSelect.value) {
|
|
434
|
+
modelResult.textContent = ""; modelMenu.open = false; modelSummary.focus?.();
|
|
435
|
+
}
|
|
239
436
|
} catch (error) {
|
|
240
437
|
if (own !== generation) return;
|
|
241
|
-
modelResult.textContent = error.
|
|
438
|
+
modelResult.textContent = error.disposition !== "not-submitted" ? ""
|
|
439
|
+
: t(error.message.includes("Exit the native TUI") ? "conversation.modelTerminalBusy" : "conversation.modelFailed");
|
|
440
|
+
modelResult.title = error.message;
|
|
242
441
|
if (error.disposition === "not-submitted") { modelPending = null; saved(".modelPending", null); }
|
|
442
|
+
drawModel();
|
|
243
443
|
} finally { finish(own); }
|
|
244
444
|
}
|
|
245
445
|
const commands = ["help", "status", "model", "stop", "latest"];
|
|
@@ -281,10 +481,11 @@ export function createConversationController(host, t, locale = function () { ret
|
|
|
281
481
|
ref.relativePath + " · " + ref.commit + " · sha256:" + ref.digest), remove));
|
|
282
482
|
});
|
|
283
483
|
}
|
|
284
|
-
function renderRequests(rows) {
|
|
285
|
-
const signature = JSON.stringify([selected, rows, !!facts.terminalWriter]);
|
|
484
|
+
function renderRequests(rows, omitted) {
|
|
485
|
+
const signature = JSON.stringify([selected, rows, !!facts.terminalWriter, !!omitted]);
|
|
286
486
|
if (signature === requestSignature) return;
|
|
287
487
|
requestSignature = signature; clear(requests);
|
|
488
|
+
if (omitted) requests.append(h("p.cv-note", null, t("conversation.nativeOmitted")));
|
|
288
489
|
rows.forEach(function (request) {
|
|
289
490
|
const card = h("section.cv-card", null, h("h3.cv-card-title", null, t("conversation.nativeRequest")),
|
|
290
491
|
h("p.cv-note", null, request.method + " · " + request.turnId));
|
|
@@ -294,6 +495,7 @@ export function createConversationController(host, t, locale = function () { ret
|
|
|
294
495
|
(params.questions || []).forEach(function (question) {
|
|
295
496
|
const field = h("input", { type: question.isSecret ? "password" : "text", "aria-label": question.question });
|
|
296
497
|
const draftKey = ".nativeDraft." + request.turnId + "." + request.id + "." + question.id;
|
|
498
|
+
nativeDraftKeys.add(draftKey);
|
|
297
499
|
if (!question.isSecret) {
|
|
298
500
|
field.value = saved(draftKey) || "";
|
|
299
501
|
field.addEventListener("input", function () { saved(draftKey, field.value); });
|
|
@@ -383,7 +585,11 @@ export function createConversationController(host, t, locale = function () { ret
|
|
|
383
585
|
try { materials = JSON.parse(saved(".materials") || "[]"); } catch { materials = []; }
|
|
384
586
|
try { modelPending = JSON.parse(saved(".modelPending") || "null"); } catch { modelPending = null; }
|
|
385
587
|
modelCatalog = null; lastModelObservation = null; clear(modelSelect); clear(modelRows); modelRowMap = new Map(); confirmedModel = null;
|
|
386
|
-
|
|
588
|
+
// A historical Session never borrows another Session's model confirmation.
|
|
589
|
+
modelObservation = null; pageFacts = null;
|
|
590
|
+
modelStatus.textContent = ""; modelResult.textContent = ""; receiptDetail.textContent = ""; receiptBlock.hidden = true;
|
|
591
|
+
status.textContent = ""; detailSignature = ""; sessionSignature = "";
|
|
592
|
+
drawModel();
|
|
387
593
|
drawCommands();
|
|
388
594
|
materialAlert.textContent = "";
|
|
389
595
|
if (incomingMaterials) {
|
|
@@ -400,6 +606,7 @@ export function createConversationController(host, t, locale = function () { ret
|
|
|
400
606
|
clear(requests); requestSignature = "";
|
|
401
607
|
clear(questions); questionSignature = "";
|
|
402
608
|
controls();
|
|
609
|
+
report();
|
|
403
610
|
}
|
|
404
611
|
function finish(own) {
|
|
405
612
|
busy = false; controls();
|
|
@@ -441,6 +648,8 @@ export function createConversationController(host, t, locale = function () { ret
|
|
|
441
648
|
function messageNode(item) {
|
|
442
649
|
const meta = itemMeta(item);
|
|
443
650
|
if (item.kind === "user") {
|
|
651
|
+
// User text is shown exactly as sent: tags in it are the user's words,
|
|
652
|
+
// never guessed to be attached context.
|
|
444
653
|
return h("article.cv-msg.cv-user", { title: meta },
|
|
445
654
|
item.source === "yui-input" ? h("span.cv-tag", null, t("conversation.linkedInput") + " " + item.messageId) : null,
|
|
446
655
|
h("div.cv-bubble", null, richText(null, item.text, t, { threshold: 1800 })),
|
|
@@ -511,20 +720,74 @@ export function createConversationController(host, t, locale = function () { ret
|
|
|
511
720
|
feed.replaceChildren(...nodes);
|
|
512
721
|
}
|
|
513
722
|
}
|
|
514
|
-
function
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
723
|
+
function chooseSession(id) {
|
|
724
|
+
picker.open = false;
|
|
725
|
+
picker.querySelector("summary")?.focus?.();
|
|
726
|
+
if (id === selected) return;
|
|
727
|
+
choose(id); refresh();
|
|
728
|
+
}
|
|
729
|
+
function drawSessions() {
|
|
730
|
+
const session = selectedSession();
|
|
731
|
+
pickerName.textContent = session ? sessionName(session)
|
|
732
|
+
: selected ? t(selected === (facts && facts.currentSessionId) ? "conversation.current" : "conversation.previous") : t("conversation.noSessionShort");
|
|
733
|
+
picker.querySelector("summary").title = pickerName.textContent;
|
|
734
|
+
const rows = facts.sessions.slice().sort(function (a, b) {
|
|
735
|
+
return Number(b.current) - Number(a.current) || String(b.createdAt).localeCompare(String(a.createdAt));
|
|
520
736
|
});
|
|
521
|
-
|
|
737
|
+
const signature = JSON.stringify([selected, facts.nextOffset, locale(), rows.map(function (s) {
|
|
738
|
+
return [s.nativeSessionId, s.current, s.status, s.endReason, s.title, s.createdAt];
|
|
739
|
+
})]);
|
|
522
740
|
moreSessions.hidden = !facts.nextOffset;
|
|
741
|
+
if (signature === sessionSignature) return;
|
|
742
|
+
sessionSignature = signature;
|
|
743
|
+
clear(sessionList);
|
|
744
|
+
let historyHeading = false;
|
|
745
|
+
rows.forEach(function (s) {
|
|
746
|
+
if (!s.current && !historyHeading) {
|
|
747
|
+
historyHeading = true;
|
|
748
|
+
sessionList.append(h("p.cv-pop-title", { role: "presentation" }, t("conversation.previousSessions")));
|
|
749
|
+
}
|
|
750
|
+
const chosen = s.nativeSessionId === selected;
|
|
751
|
+
const option = h("button.cv-session-opt", { type: "button", role: "option", "aria-selected": String(chosen),
|
|
752
|
+
title: s.title || sessionWhen(s), dataset: { session: s.nativeSessionId } },
|
|
753
|
+
h("span.cv-so-dot.is-" + (s.status === "active" ? "active" : s.endReason === "failed" ? "failed" : "ended"), { "aria-hidden": "true" }),
|
|
754
|
+
h("span.cv-so-text", null,
|
|
755
|
+
h("span.cv-so-main", null, s.title || (s.current ? t("conversation.current") : sessionWhen(s))),
|
|
756
|
+
h("span.cv-so-meta", null, (s.title || s.current ? sessionWhen(s) + " · " : "") + sessionLifecycle(s))),
|
|
757
|
+
chosen ? icon("check", "cv-so-check") : null);
|
|
758
|
+
option.addEventListener("click", function () { chooseSession(s.nativeSessionId); });
|
|
759
|
+
sessionList.append(option);
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
// Low-frequency identity and diagnostics: readable first, exact values copyable.
|
|
763
|
+
function drawDetails() {
|
|
764
|
+
const session = selectedSession();
|
|
765
|
+
const turn = facts && selected === facts.currentSessionId ? facts.turn : null;
|
|
766
|
+
const exact = (owner.taskId || "Global") + " / " + owner.roleName + " / " + selected
|
|
767
|
+
+ (turn ? " / " + (turn.nativeTurnId || turn.attemptId) + (turn.runId ? " / " + turn.runId : "") : "");
|
|
768
|
+
const live = pageFacts && pageFacts.live === "unavailable";
|
|
769
|
+
const signature = JSON.stringify([exact, session, pageFacts && [pageFacts.live, pageFacts.linkedInputsOmitted],
|
|
770
|
+
!!(facts && facts.terminalWriter), locale()]);
|
|
771
|
+
if (signature === detailSignature) return;
|
|
772
|
+
detailSignature = signature;
|
|
773
|
+
clear(detailFacts);
|
|
774
|
+
fact(detailFacts, t("dock.target"), ownerLabel());
|
|
775
|
+
if (session) {
|
|
776
|
+
fact(detailFacts, t("conversation.sessionLabel"), sessionName(session), sessionLifecycle(session));
|
|
777
|
+
fact(detailFacts, t("conversation.sessionId"), session.nativeSessionId);
|
|
778
|
+
if (session.executionAuthority) fact(detailFacts, t("conversation.authority"), session.executionAuthority);
|
|
779
|
+
if (session.agentId) fact(detailFacts, t("conversation.agent"), session.agentId + (session.adapterId ? " · " + session.adapterId : ""));
|
|
780
|
+
}
|
|
781
|
+
if (turn) fact(detailFacts, t("conversation.turn"), String(turn.nativeTurnId || turn.attemptId), turn.status);
|
|
782
|
+
fact(detailFacts, t("conversation.writer"), t(facts && facts.terminalWriter ? "conversation.terminalWriter" : "conversation.structuredWriter"));
|
|
783
|
+
identity.textContent = exact;
|
|
784
|
+
liveNote.textContent = [live ? t("conversation.liveUnavailable") : "",
|
|
785
|
+
pageFacts && pageFacts.linkedInputsOmitted ? t("conversation.linkedOmitted") : ""].filter(Boolean).join(" · ");
|
|
523
786
|
}
|
|
524
787
|
async function refresh(older) {
|
|
525
788
|
if (!owner || busy || host.hidden || host.closest("[hidden]") || document.hidden) return;
|
|
526
789
|
if (older && (!next || !selected || !facts || !historyOk)) return;
|
|
527
|
-
busy = true; controls();
|
|
790
|
+
busy = true; reading = true; controls();
|
|
528
791
|
let own = generation;
|
|
529
792
|
try {
|
|
530
793
|
if (older) {
|
|
@@ -553,13 +816,17 @@ export function createConversationController(host, t, locale = function () { ret
|
|
|
553
816
|
choose(facts.currentSessionId);
|
|
554
817
|
own = generation;
|
|
555
818
|
}
|
|
556
|
-
|
|
819
|
+
report();
|
|
820
|
+
if (!selected) {
|
|
821
|
+
status.textContent = t("conversation.noSession"); status.className = "cv-status is-unknown";
|
|
822
|
+
pickerName.textContent = t("conversation.noSessionShort"); return;
|
|
823
|
+
}
|
|
557
824
|
// Preserve a historical choice even if it is outside the newest page.
|
|
558
|
-
drawSessions(
|
|
825
|
+
drawSessions(); drawDetails();
|
|
559
826
|
renderQuestions();
|
|
560
827
|
const page = await requestJson(url({ session: selected }));
|
|
561
828
|
if (own !== generation) return;
|
|
562
|
-
historyOk = true; latestItems = page.items;
|
|
829
|
+
historyOk = true; latestItems = page.items; pageFacts = page;
|
|
563
830
|
if (!olderItems.length) next = page.nextCursor;
|
|
564
831
|
const turn = selected === facts.currentSessionId ? facts.turn : null;
|
|
565
832
|
let state = page.status === "waiting-user" ? "waiting" : page.status === "active" ? "active"
|
|
@@ -567,26 +834,28 @@ export function createConversationController(host, t, locale = function () { ret
|
|
|
567
834
|
const session = facts.sessions.find(function (s) { return s.nativeSessionId === selected; });
|
|
568
835
|
if (session && session.status === "ended") state = session.endReason === "failed" ? "failed" : "ended";
|
|
569
836
|
if (turn && turn.status === "delivery-unknown") state = "unknown";
|
|
570
|
-
status.textContent = t("conversation." + state);
|
|
837
|
+
status.textContent = t(state === "unknown" ? "conversation.stateUnknown" : "conversation." + state);
|
|
571
838
|
status.title = page.observedAt + " · " + t(facts.terminalWriter ? "conversation.terminalWriter" : "conversation.structuredWriter");
|
|
572
839
|
status.className = "cv-status is-" + state;
|
|
573
|
-
|
|
574
|
-
if (page.linkedInputsOmitted) status.textContent += " · " + t("conversation.linkedOmitted");
|
|
575
|
-
if (page.nativeRequestsOmitted) status.textContent += " · " + t("conversation.nativeOmitted");
|
|
576
|
-
renderRequests(page.nativeRequests || []);
|
|
840
|
+
renderRequests(page.nativeRequests || [], !!page.nativeRequestsOmitted);
|
|
577
841
|
showModel(page.runConfiguration);
|
|
578
|
-
|
|
579
|
-
+ (turn ? " / " + (turn.nativeTurnId || turn.attemptId) + (turn.runId ? " / " + turn.runId : "") : "");
|
|
842
|
+
drawDetails();
|
|
580
843
|
const scroll = feed.scrollTop, atEnd = feed.scrollHeight - feed.clientHeight - scroll < 40;
|
|
581
844
|
renderItems(allItems());
|
|
582
845
|
feed.scrollTop = atEnd ? feed.scrollHeight : scroll;
|
|
583
846
|
syncJump();
|
|
584
847
|
} catch (error) {
|
|
585
|
-
if (own === generation) {
|
|
586
|
-
|
|
848
|
+
if (own === generation) {
|
|
849
|
+
historyOk = false; status.textContent = t("conversation.disconnectedShort");
|
|
850
|
+
status.title = t("conversation.disconnected") + " · " + error.message; status.className = "cv-status is-failed";
|
|
851
|
+
// An unreadable confirmation is unconfirmed, not the earlier value.
|
|
852
|
+
if (confirmedModel) { confirmedModel = null; modelObservation = null; drawModel(); }
|
|
853
|
+
}
|
|
854
|
+
} finally { endRead(); finish(own); }
|
|
587
855
|
if (own === generation && pending && historyOk) await inspectReceipt();
|
|
588
856
|
}
|
|
589
857
|
async function inspectReceipt() {
|
|
858
|
+
if (!await afterRead()) return;
|
|
590
859
|
if (!pending || busy) return;
|
|
591
860
|
const own = generation;
|
|
592
861
|
const inspected = pending;
|
|
@@ -613,6 +882,10 @@ export function createConversationController(host, t, locale = function () { ret
|
|
|
613
882
|
finally { finish(own); }
|
|
614
883
|
}
|
|
615
884
|
async function submit(interrupt, asLiteral) {
|
|
885
|
+
const body = text.value;
|
|
886
|
+
const target = facts && facts.turn && (facts.turn.nativeTurnId || facts.turn.attemptId);
|
|
887
|
+
if (!await afterRead() || body !== text.value
|
|
888
|
+
|| target !== (facts && facts.turn && (facts.turn.nativeTurnId || facts.turn.attemptId))) return;
|
|
616
889
|
controls(); // Another tab may have acquired an unresolved input since refresh.
|
|
617
890
|
if (busy || pending || (interrupt ? stop.disabled : send.disabled)) return;
|
|
618
891
|
if (!interrupt && !text.value.trim()) return;
|
|
@@ -650,7 +923,7 @@ export function createConversationController(host, t, locale = function () { ret
|
|
|
650
923
|
if (own === generation) await inspectReceipt();
|
|
651
924
|
}
|
|
652
925
|
text.addEventListener("input", function () {
|
|
653
|
-
if (selected)
|
|
926
|
+
if (selected) saved(".draft", text.value);
|
|
654
927
|
drawCommands();
|
|
655
928
|
});
|
|
656
929
|
text.addEventListener("keydown", function (event) {
|
|
@@ -664,12 +937,14 @@ export function createConversationController(host, t, locale = function () { ret
|
|
|
664
937
|
}
|
|
665
938
|
});
|
|
666
939
|
modelLoad.addEventListener("click", loadModels);
|
|
667
|
-
modelSelect.addEventListener("change",
|
|
940
|
+
modelSelect.addEventListener("change", function () { pickModel(modelSelect.value); });
|
|
668
941
|
modelApply.addEventListener("click", applyModel);
|
|
669
942
|
literal.addEventListener("click", function () { submit(false, true); });
|
|
670
943
|
file.addEventListener("change", async function () {
|
|
944
|
+
if (!await afterRead()) return;
|
|
945
|
+
controls();
|
|
671
946
|
const chosen = file.files[0];
|
|
672
|
-
if (!chosen || file.disabled) return;
|
|
947
|
+
if (!chosen || file.disabled || busy) return;
|
|
673
948
|
const own = generation;
|
|
674
949
|
const endpoint = url({ session: selected }).replace("/api/conversation?", "/api/conversation/material?");
|
|
675
950
|
const requestId = crypto.randomUUID();
|
|
@@ -688,9 +963,17 @@ export function createConversationController(host, t, locale = function () { ret
|
|
|
688
963
|
+ " materials/" + requestId + "/" + chosen.name;
|
|
689
964
|
} finally { file.value = ""; finish(own); drawMaterials(); }
|
|
690
965
|
});
|
|
691
|
-
|
|
692
|
-
current.addEventListener("click",
|
|
693
|
-
|
|
966
|
+
function backToCurrent() { if (facts && facts.currentSessionId) chooseSession(facts.currentSessionId); }
|
|
967
|
+
current.addEventListener("click", backToCurrent);
|
|
968
|
+
noticeCurrent.addEventListener("click", backToCurrent);
|
|
969
|
+
copyIdentity.addEventListener("click", function () {
|
|
970
|
+
navigator.clipboard.writeText(identity.textContent).then(function () {
|
|
971
|
+
copyIdentity.title = t("conversation.copied");
|
|
972
|
+
}, function () { copyIdentity.title = t("conversation.copyFailed"); });
|
|
973
|
+
});
|
|
974
|
+
older.addEventListener("click", function () {
|
|
975
|
+
if (reading) afterRead().then(function (same) { if (same) refresh(true); }); else refresh(true);
|
|
976
|
+
});
|
|
694
977
|
latest.addEventListener("click", function () {
|
|
695
978
|
refresh().then(function () { feed.scrollTop = feed.scrollHeight; syncJump(); });
|
|
696
979
|
});
|
|
@@ -705,7 +988,7 @@ export function createConversationController(host, t, locale = function () { ret
|
|
|
705
988
|
const page = await requestJson(url({ offset: facts.nextOffset }));
|
|
706
989
|
if (own !== generation) return;
|
|
707
990
|
facts.sessions = facts.sessions.concat(page.sessions); facts.nextOffset = page.nextOffset;
|
|
708
|
-
drawSessions(
|
|
991
|
+
drawSessions();
|
|
709
992
|
} catch (error) { status.textContent = error.message; }
|
|
710
993
|
finally { finish(own); }
|
|
711
994
|
});
|
|
@@ -742,11 +1025,14 @@ export function createConversationController(host, t, locale = function () { ret
|
|
|
742
1025
|
// from the shared endpoint, not retain a historical selection or stale ID.
|
|
743
1026
|
if (JSON.stringify(target) !== JSON.stringify(owner) || options && options.current) {
|
|
744
1027
|
if (selected) saved(".draft", text.value);
|
|
745
|
-
generation++; owner = target; selected = null; facts = null; pending = null; clear(
|
|
1028
|
+
generation++; owner = target; selected = null; facts = null; pending = null; clear(feed);
|
|
1029
|
+
clear(sessionList); sessionSignature = ""; detailSignature = ""; clear(detailFacts); pageFacts = null;
|
|
1030
|
+
pickerName.textContent = t("conversation.sessions"); status.textContent = ""; status.className = "cv-status";
|
|
746
1031
|
next = null; latestItems = []; olderItems = []; historyOk = false; rendered.clear();
|
|
747
|
-
text.value = ""; receipt.textContent = ""; identity.textContent = "";
|
|
1032
|
+
text.value = ""; receipt.textContent = ""; identity.textContent = ""; liveNote.textContent = "";
|
|
1033
|
+
receiptBlock.hidden = true; controls();
|
|
748
1034
|
modelPending = null; modelCatalog = null; clear(modelSelect); clear(modelRows); modelRowMap = new Map(); confirmedModel = null;
|
|
749
|
-
|
|
1035
|
+
modelObservation = null; drawModel(); modelStatus.textContent = "";
|
|
750
1036
|
modelResult.textContent = "";
|
|
751
1037
|
receiptDetail.textContent = ""; draftStatus.textContent = ""; drawCommands();
|
|
752
1038
|
materials = []; incomingMaterials = null; clear(materialList); materialAlert.textContent = "";
|
|
@@ -754,10 +1040,22 @@ export function createConversationController(host, t, locale = function () { ret
|
|
|
754
1040
|
clear(questions); questionSignature = "";
|
|
755
1041
|
}
|
|
756
1042
|
if (options && options.materials) incomingMaterials = options.materials;
|
|
1043
|
+
// A named Session (carried from another mode) is kept even when it is no
|
|
1044
|
+
// longer current: never silently exchanged for today's current Session.
|
|
1045
|
+
if (options && options.session && options.session !== selected) choose(options.session);
|
|
757
1046
|
if (!timer) timer = window.setInterval(function () { refresh(); }, 2000);
|
|
758
1047
|
refresh();
|
|
759
1048
|
},
|
|
760
|
-
close: function () { if (timer) window.clearInterval(timer); timer = null; generation++; }
|
|
1049
|
+
close: function () { if (timer) window.clearInterval(timer); timer = null; generation++; },
|
|
1050
|
+
// The session page left this view: forget its unsent text. A draft that
|
|
1051
|
+
// is the body of an unresolved submission stays with that receipt.
|
|
1052
|
+
discardDraft: function () {
|
|
1053
|
+
if (!owner || !selected) return;
|
|
1054
|
+
let held = null;
|
|
1055
|
+
try { held = JSON.parse(saved(".pending") || "null"); } catch { held = null; }
|
|
1056
|
+
if (!held) saved(".draft", null);
|
|
1057
|
+
nativeDraftKeys.forEach(function (draftKey) { saved(draftKey, null); });
|
|
1058
|
+
}
|
|
761
1059
|
};
|
|
762
1060
|
}
|
|
763
1061
|
`;
|