pilotswarm 0.5.2 → 0.5.4
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/mcp/dist/src/tools/sessions.d.ts.map +1 -1
- package/mcp/dist/src/tools/sessions.js +4 -1
- package/mcp/dist/src/tools/sessions.js.map +1 -1
- package/package.json +2 -2
- package/tui/src/app.js +12 -1
- package/tui/src/node-sdk-transport.js +5 -3
- package/ui/core/src/controller.js +89 -5
- package/ui/core/src/history.js +34 -0
- package/ui/core/src/selectors.js +176 -51
- package/ui/react/src/components.js +83 -8
- package/ui/react/src/web-app.js +38 -24
- package/web/dist/assets/index-BRbv6TsU.css +1 -0
- package/web/dist/assets/{index-BA1Gd5ZC.js → index-OSQLEf2A.js} +3 -3
- package/web/dist/assets/pilotswarm-DXc-jqRU.js +90 -0
- package/web/dist/assets/{react-CqzaLZob.js → react-DMAIpp0t.js} +1 -1
- package/web/dist/index.html +4 -4
- package/web/runtime.js +2 -0
- package/web/dist/assets/index-Jr_Wn0fg.css +0 -1
- package/web/dist/assets/pilotswarm-C6iSDexX.js +0 -90
package/ui/core/src/selectors.js
CHANGED
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
decorateArtifactLinksForChat,
|
|
11
11
|
extractArtifactLinks,
|
|
12
12
|
extractHttpLinks,
|
|
13
|
-
formatDisplayDateTime,
|
|
14
13
|
formatHumanDurationSeconds,
|
|
15
14
|
formatTimestamp,
|
|
16
15
|
padRunsToDisplayWidth,
|
|
@@ -21,9 +20,9 @@ import {
|
|
|
21
20
|
wrapRunsToDisplayWidth,
|
|
22
21
|
} from "./formatting.js";
|
|
23
22
|
import {
|
|
23
|
+
computeContextPercent,
|
|
24
24
|
getContextCompactionBadge,
|
|
25
25
|
getContextHeaderBadge,
|
|
26
|
-
getContextListBadge,
|
|
27
26
|
} from "./context-usage.js";
|
|
28
27
|
import { canonicalSystemTitle } from "./system-titles.js";
|
|
29
28
|
import { normalizeArtifactEntries } from "./state.js";
|
|
@@ -217,19 +216,22 @@ function ownerInitials(owner) {
|
|
|
217
216
|
}
|
|
218
217
|
|
|
219
218
|
function shouldDecorateSessionOwners(state) {
|
|
220
|
-
|
|
219
|
+
// Decorate owners only when the list actually surfaces more than one
|
|
220
|
+
// distinct human owner — otherwise the owner chip is pure noise on every
|
|
221
|
+
// row (you are always "you"). A narrowed owner filter is an explicit
|
|
222
|
+
// multi-user context, so honor that too.
|
|
221
223
|
if (state.sessions?.ownerFilter && state.sessions.ownerFilter.all !== true) return true;
|
|
222
|
-
|
|
224
|
+
const owners = new Set();
|
|
225
|
+
for (const session of Object.values(state.sessions?.byId || {})) {
|
|
226
|
+
if (!session || session.isSystem || session.isGroup) continue;
|
|
227
|
+
const key = ownerKeyForOwner(session.owner);
|
|
228
|
+
if (!key) continue;
|
|
229
|
+
owners.add(key);
|
|
230
|
+
if (owners.size > 1) return true;
|
|
231
|
+
}
|
|
223
232
|
return false;
|
|
224
233
|
}
|
|
225
234
|
|
|
226
|
-
function buildSessionListTitle(session, brandingTitle, decorateOwners = false) {
|
|
227
|
-
const title = buildSessionTitle(session, brandingTitle);
|
|
228
|
-
if (!decorateOwners || session?.isSystem) return title;
|
|
229
|
-
const prefix = session?.owner ? ownerInitials(session.owner) : "?";
|
|
230
|
-
return `(${prefix}) ${title}`;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
235
|
function groupMemberSessions(group, byId = {}) {
|
|
234
236
|
if (!group?.isGroup || !group?.groupId) return [];
|
|
235
237
|
return Object.values(byId || {}).filter((session) => (
|
|
@@ -575,68 +577,118 @@ function buildSessionRowView(entry, session, state, totalDescendantCounts, visib
|
|
|
575
577
|
|
|
576
578
|
const mainColor = session?.isGroup ? "cyan" : session?.isSystem ? "yellow" : sessionStatusColor(session, mode);
|
|
577
579
|
const effectiveOwner = effectiveSessionOwner(session, state.sessions?.byId || {});
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
// Show the session's last-updated time at the end of the row. For groups,
|
|
584
|
-
// keep the member-count badge but append the last-updated timestamp so the
|
|
585
|
-
// row reflects the same value the list is sorted by. `formatDisplayDateTime`
|
|
586
|
-
// uses the system locale/timezone, so the timestamp renders in the
|
|
587
|
-
// browser/client local zone.
|
|
580
|
+
|
|
581
|
+
const shortId = shortSessionId(session?.sessionId);
|
|
582
|
+
|
|
583
|
+
// Row age — last-updated (the value the list is sorted by). Coarse buckets
|
|
584
|
+
// so it doesn't tick every second: <1min · Nmin · NhMMm · NdHHh · Nw.
|
|
588
585
|
const rowTimestampMs = session?.updatedAt
|
|
589
586
|
|| session?.summaryUpdatedAt
|
|
590
587
|
|| session?.latestSummaryUpdatedAt
|
|
591
588
|
|| session?.createdAt
|
|
592
589
|
|| 0;
|
|
593
|
-
const
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
590
|
+
const relTime = rowTimestampMs ? formatSessionAge(rowTimestampMs) : "";
|
|
591
|
+
const modelLabel = shortModelReasoningLabel(session?.model, session?.reasoningEffort);
|
|
592
|
+
|
|
593
|
+
// A regular session with no human title otherwise renders as a bare
|
|
594
|
+
// "(guid)" — an empty, ugly line. For those, pull the meta (id · age ·
|
|
595
|
+
// model) UP onto the main line so it carries information. Titled rows keep
|
|
596
|
+
// the title on the line and expand id·age·model·ctx only when selected.
|
|
597
|
+
const rawTitle = session?.isSystem
|
|
598
|
+
? canonicalSystemTitle(session, state.branding?.title || "PilotSwarm")
|
|
599
|
+
: buildSessionDisplayTitle(session);
|
|
600
|
+
const hasRealTitle = Boolean(session?.isSystem || session?.isGroup || (rawTitle && rawTitle.trim()));
|
|
601
|
+
|
|
602
|
+
const titleRuns = [...prefixRuns];
|
|
603
|
+
// Owner chip — only when the list actually surfaces more than one human
|
|
604
|
+
// owner (shouldDecorateSessionOwners). Otherwise it's noise on every row.
|
|
605
|
+
if (shouldDecorateSessionOwners(state) && !session?.isSystem && !session?.isGroup) {
|
|
606
|
+
const initials = effectiveOwner ? ownerInitials(effectiveOwner) : "?";
|
|
607
|
+
titleRuns.push({ text: `${initials} · `, color: "cyan" });
|
|
608
|
+
}
|
|
609
|
+
if (hasRealTitle) {
|
|
610
|
+
titleRuns.push({ text: rawTitle, color: mainColor, bold: Boolean(session?.isSystem || session?.isGroup) });
|
|
611
|
+
} else {
|
|
612
|
+
// Untitled → id · age · model on one line (no wasted title line).
|
|
613
|
+
titleRuns.push({ text: shortId, color: mainColor });
|
|
614
|
+
if (relTime) { titleRuns.push({ text: " · ", color: "gray" }); titleRuns.push({ text: relTime, color: "gray" }); }
|
|
615
|
+
if (modelLabel) { titleRuns.push({ text: " · ", color: "gray" }); titleRuns.push({ text: modelLabel, color: "green" }); }
|
|
616
|
+
}
|
|
617
|
+
|
|
602
618
|
const collapseBadge = getCollapseBadge(session?.sessionId, entry, totalDescendantCounts, visibleDescendantCounts);
|
|
603
619
|
if (collapseBadge) {
|
|
604
620
|
titleRuns.push({ text: ` ${collapseBadge.text}`, color: collapseBadge.color, bold: collapseBadge.bold });
|
|
605
621
|
}
|
|
622
|
+
// Scheduled sessions keep a compact clock glyph on the title; the full
|
|
623
|
+
// cron cadence rides in the detail line.
|
|
624
|
+
const cronBadge = getCronBadge(session);
|
|
625
|
+
if (cronBadge) {
|
|
626
|
+
titleRuns.push({ text: " ⏱", color: "magenta" });
|
|
627
|
+
}
|
|
606
628
|
|
|
629
|
+
// Right-column context %: on every row that has usage. Compaction in
|
|
630
|
+
// flight takes precedence; else green normally, amber ≥70, red ≥85.
|
|
631
|
+
const ctxRuns = [];
|
|
632
|
+
const compactionState = session?.contextUsage?.compaction?.state;
|
|
633
|
+
const ctxPercent = computeContextPercent(session?.contextUsage);
|
|
634
|
+
if (session?.isGroup) {
|
|
635
|
+
if (session?.memberCount != null) ctxRuns.push({ text: `${session.memberCount}`, color: "gray" });
|
|
636
|
+
} else if (compactionState === "running") {
|
|
637
|
+
ctxRuns.push({ text: "⇊", color: "magenta" });
|
|
638
|
+
} else if (compactionState === "failed") {
|
|
639
|
+
ctxRuns.push({ text: "!", color: "red" });
|
|
640
|
+
} else if (ctxPercent != null) {
|
|
641
|
+
ctxRuns.push({ text: `${ctxPercent}%`, color: ctxPercent >= 85 ? "red" : ctxPercent >= 70 ? "yellow" : "green" });
|
|
642
|
+
} else {
|
|
643
|
+
ctxRuns.push({ text: "—", color: "gray" });
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Kept for backward-compat consumers; groups carry a member/time meta.
|
|
607
647
|
const metaRuns = [];
|
|
608
648
|
if (session?.isGroup && session?.memberCount != null) {
|
|
609
649
|
metaRuns.push({ text: `${session.memberCount} member${session.memberCount === 1 ? "" : "s"}`, color: "gray" });
|
|
650
|
+
if (relTime) { metaRuns.push({ text: " · ", color: "gray" }); metaRuns.push({ text: relTime, color: "gray" }); }
|
|
610
651
|
}
|
|
611
|
-
if (formattedRowTimestamp) {
|
|
612
|
-
if (metaRuns.length > 0) metaRuns.push({ text: " · ", color: "gray" });
|
|
613
|
-
metaRuns.push({ text: formattedRowTimestamp, color: "gray" });
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
const badgeRuns = [];
|
|
617
652
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
]
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
653
|
+
// Detail — expanded under the SELECTED row. Titled rows repeat
|
|
654
|
+
// id · age · model here; untitled rows already carry those on the main
|
|
655
|
+
// line, so their detail starts at the ctx breakdown to avoid repetition.
|
|
656
|
+
const detailRuns = [];
|
|
657
|
+
const pushSep = () => { if (detailRuns.length) detailRuns.push({ text: " · ", color: "gray" }); };
|
|
658
|
+
if (session?.isGroup) {
|
|
659
|
+
detailRuns.push(...metaRuns);
|
|
660
|
+
} else {
|
|
661
|
+
if (hasRealTitle) {
|
|
662
|
+
detailRuns.push({ text: shortId, color: "cyan" });
|
|
663
|
+
if (relTime) { pushSep(); detailRuns.push({ text: relTime, color: "gray" }); }
|
|
664
|
+
if (modelLabel) { pushSep(); detailRuns.push({ text: modelLabel, color: "green" }); }
|
|
665
|
+
}
|
|
666
|
+
const cu = session?.contextUsage;
|
|
667
|
+
if (cu && Number.isFinite(cu.currentTokens) && Number.isFinite(cu.tokenLimit) && cu.tokenLimit > 0) {
|
|
668
|
+
pushSep();
|
|
669
|
+
detailRuns.push({ text: `ctx ${formatCompactNumber(cu.currentTokens)}/${formatCompactNumber(cu.tokenLimit)}`, color: "gray" });
|
|
670
|
+
}
|
|
671
|
+
const childCount = totalDescendantCounts?.[session?.sessionId];
|
|
672
|
+
if (childCount) { pushSep(); detailRuns.push({ text: `${childCount} child${childCount === 1 ? "" : "ren"}`, color: "gray" }); }
|
|
673
|
+
if (cronBadge) { pushSep(); detailRuns.push({ text: cronBadge.text, color: cronBadge.color }); }
|
|
625
674
|
}
|
|
626
675
|
|
|
627
|
-
|
|
676
|
+
// Flat runs for the TUI: title + (for titled rows) dim age + context.
|
|
628
677
|
const runs = [
|
|
629
678
|
...titleRuns,
|
|
630
|
-
...(
|
|
631
|
-
...(
|
|
679
|
+
...(hasRealTitle && relTime && !session?.isGroup ? [{ text: " ", color: "gray" }, { text: relTime, color: "gray" }] : []),
|
|
680
|
+
...(ctxRuns.length && !session?.isGroup ? [{ text: " · ", color: "gray" }, ...ctxRuns] : []),
|
|
681
|
+
...(session?.isGroup && metaRuns.length ? [{ text: " ", color: "gray" }, ...metaRuns] : []),
|
|
632
682
|
];
|
|
633
683
|
|
|
634
684
|
return {
|
|
635
685
|
runs,
|
|
636
686
|
titleRuns,
|
|
687
|
+
ctxRuns,
|
|
637
688
|
metaRuns,
|
|
638
|
-
badgeRuns,
|
|
639
|
-
selectedMetaRuns,
|
|
689
|
+
badgeRuns: [],
|
|
690
|
+
selectedMetaRuns: detailRuns,
|
|
691
|
+
detailRuns,
|
|
640
692
|
};
|
|
641
693
|
}
|
|
642
694
|
|
|
@@ -1245,9 +1297,16 @@ function buildChatMessagePrefix(message) {
|
|
|
1245
1297
|
glyph = "x";
|
|
1246
1298
|
glyphColor = "red";
|
|
1247
1299
|
} else if (message?.role === "user" && !message?.optimistic && !message?.pendingPhase) {
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1300
|
+
if (message?.stopped) {
|
|
1301
|
+
// Delivered, but its turn was user-stopped mid-flight — the model
|
|
1302
|
+
// may not have acted on it. Amber prohibition ("no parking") sign.
|
|
1303
|
+
glyph = "⊘";
|
|
1304
|
+
glyphColor = "yellow";
|
|
1305
|
+
} else {
|
|
1306
|
+
// Real durable user.message in transcript — show the "sent" double-check.
|
|
1307
|
+
glyph = "✓✓";
|
|
1308
|
+
glyphColor = "green";
|
|
1309
|
+
}
|
|
1251
1310
|
}
|
|
1252
1311
|
|
|
1253
1312
|
const roleColor = message?.pendingPhase === "pending"
|
|
@@ -4072,6 +4131,55 @@ export function selectReasoningEffortPickerModal(state, maxWidth = 64) {
|
|
|
4072
4131
|
};
|
|
4073
4132
|
}
|
|
4074
4133
|
|
|
4134
|
+
export function selectContextTierPickerModal(state, maxWidth = 64) {
|
|
4135
|
+
const modal = state.ui.modal;
|
|
4136
|
+
if (!modal || modal.type !== "contextTierPicker") return null;
|
|
4137
|
+
|
|
4138
|
+
const items = Array.isArray(modal.items) ? modal.items : [];
|
|
4139
|
+
const selectedIndex = Math.max(0, Number(modal.selectedIndex) || 0);
|
|
4140
|
+
const contentWidth = Math.max(24, maxWidth - 4);
|
|
4141
|
+
|
|
4142
|
+
const rows = items.map((item, index) => {
|
|
4143
|
+
const isSelected = index === selectedIndex;
|
|
4144
|
+
const labelRuns = fitRuns([
|
|
4145
|
+
{ text: "· ", color: "gray" },
|
|
4146
|
+
{ text: String(item?.label || item?.id || "tier"), color: "white", bold: true },
|
|
4147
|
+
...(item?.isDefault ? [{ text: " ← model default", color: "gray" }] : []),
|
|
4148
|
+
], contentWidth);
|
|
4149
|
+
return isSelected
|
|
4150
|
+
? buildActiveHighlightLine(labelRuns.map((run) => run.text).join("").padEnd(contentWidth, " "))
|
|
4151
|
+
: labelRuns;
|
|
4152
|
+
});
|
|
4153
|
+
|
|
4154
|
+
const selectedItem = items[selectedIndex] || null;
|
|
4155
|
+
const modelItem = modal.modelItem || null;
|
|
4156
|
+
const detailsLines = [
|
|
4157
|
+
[{ text: modelItem?.modelName || modelItem?.qualifiedName || "Selected model", color: "white", bold: true }],
|
|
4158
|
+
[{ text: `${modelItem?.providerId || "provider"} (${modelItem?.providerType || "provider"})`, color: "gray" }],
|
|
4159
|
+
[{ text: "", color: "gray" }],
|
|
4160
|
+
[{ text: selectedItem?.id ? `Using context window: ${selectedItem.id}` : "Choose a context window.", color: "white" }],
|
|
4161
|
+
[{ text: "Long context costs more per token.", color: "gray" }],
|
|
4162
|
+
];
|
|
4163
|
+
|
|
4164
|
+
return {
|
|
4165
|
+
title: modal.title || "Select context window",
|
|
4166
|
+
rows,
|
|
4167
|
+
selectedRowIndex: selectedIndex,
|
|
4168
|
+
detailsTitle: "Context Window",
|
|
4169
|
+
detailsLines,
|
|
4170
|
+
idealWidth: Math.min(
|
|
4171
|
+
Math.max(
|
|
4172
|
+
46,
|
|
4173
|
+
rows.reduce((max, row) => {
|
|
4174
|
+
if (Array.isArray(row)) return Math.max(max, flattenRunsLength(row));
|
|
4175
|
+
return Math.max(max, String(row?.text || "").length);
|
|
4176
|
+
}, 0) + 4,
|
|
4177
|
+
),
|
|
4178
|
+
maxWidth,
|
|
4179
|
+
),
|
|
4180
|
+
};
|
|
4181
|
+
}
|
|
4182
|
+
|
|
4075
4183
|
export function selectSessionGroupPickerModal(state, maxWidth = 72) {
|
|
4076
4184
|
const modal = state.ui.modal;
|
|
4077
4185
|
if (!modal || modal.type !== "sessionGroupPicker") return null;
|
|
@@ -4638,6 +4746,23 @@ function formatRelativeTime(ts) {
|
|
|
4638
4746
|
return `${Math.floor(ms / 86_400_000)}d ago`;
|
|
4639
4747
|
}
|
|
4640
4748
|
|
|
4749
|
+
// Coarse session-age buckets for the list. Seconds tick too fast and just
|
|
4750
|
+
// distract, so the smallest bucket is "<1min"; then whole minutes to an hour,
|
|
4751
|
+
// then hours+minutes, then days+hours, then weeks. No "ago" suffix — the age
|
|
4752
|
+
// column context makes it clear.
|
|
4753
|
+
function formatSessionAge(ts) {
|
|
4754
|
+
if (!ts) return "—";
|
|
4755
|
+
const ms = Date.now() - ts;
|
|
4756
|
+
if (ms < 60_000) return "<1min";
|
|
4757
|
+
const totalMin = Math.floor(ms / 60_000);
|
|
4758
|
+
if (totalMin < 60) return `${totalMin}min`;
|
|
4759
|
+
const totalHours = Math.floor(totalMin / 60);
|
|
4760
|
+
if (totalHours < 24) return `${totalHours}h${String(totalMin % 60).padStart(2, "0")}m`;
|
|
4761
|
+
const days = Math.floor(totalHours / 24);
|
|
4762
|
+
if (days < 14) return `${days}d${String(totalHours % 24).padStart(2, "0")}h`;
|
|
4763
|
+
return `${Math.floor(days / 7)}w`;
|
|
4764
|
+
}
|
|
4765
|
+
|
|
4641
4766
|
function formatLocalTimestamp(ts) {
|
|
4642
4767
|
if (!ts) return "—";
|
|
4643
4768
|
const date = ts instanceof Date ? ts : new Date(ts);
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
selectChatPaneChrome,
|
|
11
11
|
selectLiveActivityLines,
|
|
12
12
|
selectChatLines,
|
|
13
|
+
selectOutboxOverlayLines,
|
|
13
14
|
selectActivityPane,
|
|
14
15
|
selectArtifactUploadModal,
|
|
15
16
|
selectArtifactPickerModal,
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
selectLogFilterModal,
|
|
22
23
|
selectModelPickerModal,
|
|
23
24
|
selectReasoningEffortPickerModal,
|
|
25
|
+
selectContextTierPickerModal,
|
|
24
26
|
selectRenameSessionModal,
|
|
25
27
|
selectSessionAgentPickerModal,
|
|
26
28
|
selectSessionGroupNameModal,
|
|
@@ -317,6 +319,7 @@ const ChatPane = React.memo(function ChatPane({ controller, width, height, frame
|
|
|
317
319
|
activeSessionId,
|
|
318
320
|
activeSession: activeSessionId ? state.sessions.byId[activeSessionId] || null : null,
|
|
319
321
|
activeHistory: activeSessionId ? state.history.bySessionId.get(activeSessionId) || null : null,
|
|
322
|
+
activeOutbox: activeSessionId ? state.outbox?.bySessionId?.[activeSessionId] || null : null,
|
|
320
323
|
branding: state.branding,
|
|
321
324
|
connectionError: state.connection.error,
|
|
322
325
|
connectionMode: state.connection.mode,
|
|
@@ -347,11 +350,21 @@ const ChatPane = React.memo(function ChatPane({ controller, width, height, frame
|
|
|
347
350
|
history: {
|
|
348
351
|
bySessionId: historyMap,
|
|
349
352
|
},
|
|
353
|
+
// The queued-prompt overlay reads state.outbox; without it here the
|
|
354
|
+
// TUI ChatPane's synthetic selectorState always saw an empty outbox
|
|
355
|
+
// and rendered no "queued prompts: N" even though the item was
|
|
356
|
+
// durably queued in the real store.
|
|
357
|
+
outbox: {
|
|
358
|
+
bySessionId: chatView.activeSessionId && chatView.activeOutbox
|
|
359
|
+
? { [chatView.activeSessionId]: chatView.activeOutbox }
|
|
360
|
+
: {},
|
|
361
|
+
},
|
|
350
362
|
};
|
|
351
363
|
}, [
|
|
352
364
|
chatView.activeHistory,
|
|
353
365
|
chatView.activeSessionId,
|
|
354
366
|
chatView.activeSession,
|
|
367
|
+
chatView.activeOutbox,
|
|
355
368
|
chatView.branding,
|
|
356
369
|
chatView.chatViewMode,
|
|
357
370
|
chatView.connectionError,
|
|
@@ -383,13 +396,19 @@ const ChatPane = React.memo(function ChatPane({ controller, width, height, frame
|
|
|
383
396
|
() => (startupError ? [] : selectLiveActivityLines(selectorState, { spinnerFrame, maxWidth: contentWidth })),
|
|
384
397
|
[selectorState, startupError, spinnerFrame, contentWidth],
|
|
385
398
|
);
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
399
|
+
const outboxLines = React.useMemo(
|
|
400
|
+
() => (startupError ? [] : selectOutboxOverlayLines(selectorState, contentWidth)),
|
|
401
|
+
[selectorState, startupError, contentWidth],
|
|
402
|
+
);
|
|
403
|
+
// The queued-prompt overlay and the live "Working" strip are pinned in the
|
|
404
|
+
// bottom-sticky region (outbox above, strip at the very bottom), so they
|
|
405
|
+
// stay put at the foot of the pane instead of scrolling inline with the
|
|
406
|
+
// transcript — matching the portal. The transcript itself stays crisp.
|
|
407
|
+
const bottomStickyLines = React.useMemo(
|
|
408
|
+
() => (outboxLines.length > 0 || liveActivityLines.length > 0
|
|
409
|
+
? [...outboxLines, ...liveActivityLines]
|
|
410
|
+
: []),
|
|
411
|
+
[outboxLines, liveActivityLines],
|
|
393
412
|
);
|
|
394
413
|
|
|
395
414
|
return React.createElement(platform.Panel, {
|
|
@@ -399,7 +418,8 @@ const ChatPane = React.memo(function ChatPane({ controller, width, height, frame
|
|
|
399
418
|
focused: chatView.focused,
|
|
400
419
|
width,
|
|
401
420
|
height,
|
|
402
|
-
lines:
|
|
421
|
+
lines: elements,
|
|
422
|
+
bottomStickyLines,
|
|
403
423
|
scrollOffset: chatView.chatScroll,
|
|
404
424
|
scrollMode: "bottom",
|
|
405
425
|
paneId: "chat",
|
|
@@ -1059,6 +1079,60 @@ function ReasoningEffortPickerModalContainer({ controller }) {
|
|
|
1059
1079
|
return React.createElement(ReasoningEffortPickerModal, { state });
|
|
1060
1080
|
}
|
|
1061
1081
|
|
|
1082
|
+
function ContextTierPickerModal({ state }) {
|
|
1083
|
+
const platform = useUiPlatform();
|
|
1084
|
+
const modal = selectContextTierPickerModal(state);
|
|
1085
|
+
if (!modal) return null;
|
|
1086
|
+
|
|
1087
|
+
const viewport = typeof platform.getViewport === "function"
|
|
1088
|
+
? platform.getViewport()
|
|
1089
|
+
: { width: 120, height: 40 };
|
|
1090
|
+
const width = Math.max(46, Math.min(modal.idealWidth || 64, (viewport.width || 120) - 16));
|
|
1091
|
+
const listHeight = Math.max(7, Math.min(modal.rows.length + 2, 10, (viewport.height || 40) - 16));
|
|
1092
|
+
const detailsHeight = Math.max(6, Math.min(8, (viewport.height || 40) - listHeight - 10));
|
|
1093
|
+
const lines = modal.rows.length > 0
|
|
1094
|
+
? modal.rows
|
|
1095
|
+
: [{ text: "No context windows available.", color: "gray" }];
|
|
1096
|
+
const contentRows = Math.max(1, listHeight - 2);
|
|
1097
|
+
const scrollOffset = Math.max(0, modal.selectedRowIndex - Math.floor(contentRows / 2));
|
|
1098
|
+
|
|
1099
|
+
return React.createElement(platform.Overlay, null,
|
|
1100
|
+
React.createElement(platform.Column, { width },
|
|
1101
|
+
React.createElement(platform.Panel, {
|
|
1102
|
+
title: modal.title,
|
|
1103
|
+
color: "cyan",
|
|
1104
|
+
focused: false,
|
|
1105
|
+
width,
|
|
1106
|
+
height: listHeight,
|
|
1107
|
+
lines,
|
|
1108
|
+
scrollOffset,
|
|
1109
|
+
scrollMode: "top",
|
|
1110
|
+
marginBottom: 1,
|
|
1111
|
+
fillColor: "surface",
|
|
1112
|
+
}),
|
|
1113
|
+
React.createElement(platform.Panel, {
|
|
1114
|
+
title: modal.detailsTitle || "Context Window",
|
|
1115
|
+
color: "cyan",
|
|
1116
|
+
focused: false,
|
|
1117
|
+
width,
|
|
1118
|
+
height: detailsHeight,
|
|
1119
|
+
lines: modal.detailsLines,
|
|
1120
|
+
scrollOffset: 0,
|
|
1121
|
+
scrollMode: "top",
|
|
1122
|
+
fillColor: "surface",
|
|
1123
|
+
}),
|
|
1124
|
+
));
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function ContextTierPickerModalContainer({ controller }) {
|
|
1128
|
+
const state = useControllerSelector(controller, (rootState) => ({
|
|
1129
|
+
ui: {
|
|
1130
|
+
modal: rootState.ui.modal,
|
|
1131
|
+
},
|
|
1132
|
+
}), shallowEqualObject);
|
|
1133
|
+
return React.createElement(ContextTierPickerModal, { state });
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1062
1136
|
function SessionAgentPickerModal({ state }) {
|
|
1063
1137
|
const platform = useUiPlatform();
|
|
1064
1138
|
const modal = selectSessionAgentPickerModal(state);
|
|
@@ -2037,6 +2111,7 @@ export function SharedPilotSwarmApp({ controller, versionLabel = null }) {
|
|
|
2037
2111
|
React.createElement(HelpModalContainer, { controller }),
|
|
2038
2112
|
React.createElement(ModelPickerModalContainer, { controller }),
|
|
2039
2113
|
React.createElement(ReasoningEffortPickerModalContainer, { controller }),
|
|
2114
|
+
React.createElement(ContextTierPickerModalContainer, { controller }),
|
|
2040
2115
|
React.createElement(ThemePickerModalContainer, { controller }),
|
|
2041
2116
|
React.createElement(SessionAgentPickerModalContainer, { controller }),
|
|
2042
2117
|
React.createElement(SessionGroupPickerModalContainer, { controller }),
|
package/ui/react/src/web-app.js
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
selectLogFilterModal,
|
|
31
31
|
selectModelPickerModal,
|
|
32
32
|
selectReasoningEffortPickerModal,
|
|
33
|
+
selectContextTierPickerModal,
|
|
33
34
|
selectRenameSessionModal,
|
|
34
35
|
selectSessionAgentPickerModal,
|
|
35
36
|
selectSessionGroupNameModal,
|
|
@@ -1747,20 +1748,25 @@ function ScrollLinesPanel({ title, titleRight = null, color, focused, actions, l
|
|
|
1747
1748
|
});
|
|
1748
1749
|
}, []);
|
|
1749
1750
|
|
|
1750
|
-
|
|
1751
|
-
|
|
1751
|
+
// Edge fades follow real overflow: fade the TOP only when content is
|
|
1752
|
+
// clipped above (scrolled down) and the BOTTOM only when clipped below
|
|
1753
|
+
// (scrolled up). With content that fits, neither fades — the first and
|
|
1754
|
+
// last lines stay crisp.
|
|
1755
|
+
const [scrollShadow, setScrollShadow] = React.useState({ up: false, down: false });
|
|
1756
|
+
const updateScrollShadow = React.useCallback((el) => {
|
|
1752
1757
|
if (!el) return;
|
|
1753
1758
|
const up = el.scrollHeight - el.scrollTop - el.clientHeight > 4;
|
|
1754
|
-
|
|
1759
|
+
const down = el.scrollTop > 4;
|
|
1760
|
+
setScrollShadow((cur) => (cur.up === up && cur.down === down ? cur : { up, down }));
|
|
1755
1761
|
}, []);
|
|
1756
1762
|
const handleBodyScroll = React.useCallback((event) => {
|
|
1757
1763
|
onScroll();
|
|
1758
|
-
|
|
1764
|
+
updateScrollShadow(event.currentTarget);
|
|
1759
1765
|
if (!preserveHorizontalScroll || syncingHorizontalRef.current) return;
|
|
1760
1766
|
syncScrollLeft(event.currentTarget, stickyRef.current);
|
|
1761
|
-
}, [onScroll, preserveHorizontalScroll, syncScrollLeft,
|
|
1767
|
+
}, [onScroll, preserveHorizontalScroll, syncScrollLeft, updateScrollShadow]);
|
|
1762
1768
|
React.useEffect(() => {
|
|
1763
|
-
|
|
1769
|
+
updateScrollShadow(ref.current);
|
|
1764
1770
|
});
|
|
1765
1771
|
|
|
1766
1772
|
const handleStickyScroll = React.useCallback((event) => {
|
|
@@ -1779,7 +1785,7 @@ function ScrollLinesPanel({ title, titleRight = null, color, focused, actions, l
|
|
|
1779
1785
|
normalizedSticky.map((line, index) => React.createElement(Line, { key: `sticky:${index}`, line, theme })),
|
|
1780
1786
|
)
|
|
1781
1787
|
: null,
|
|
1782
|
-
React.createElement("div", { ref, className: `ps-scroll-panel ${className}${
|
|
1788
|
+
React.createElement("div", { ref, className: `ps-scroll-panel ${className}${scrollShadow.down ? " is-scrolled-down" : ""}${scrollShadow.up ? " is-scrolled-up" : ""}`.trim(), onScroll: handleBodyScroll, onWheel, onTouchStart, onTouchMove, onTouchEnd, onTouchCancel: onTouchEnd },
|
|
1783
1789
|
typeof renderBody === "function"
|
|
1784
1790
|
? renderBody(normalizedLines, theme)
|
|
1785
1791
|
: structuredBlocks
|
|
@@ -1804,25 +1810,27 @@ function SessionRowContent({ row, theme, structured = false }) {
|
|
|
1804
1810
|
: row.text;
|
|
1805
1811
|
}
|
|
1806
1812
|
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1813
|
+
// Dense row: the title takes one line (clamped by CSS) and the context %
|
|
1814
|
+
// is pinned to the right. The full id · time · model · ctx detail unfolds
|
|
1815
|
+
// only under the selected row.
|
|
1816
|
+
const ctxRuns = Array.isArray(row.ctxRuns) ? row.ctxRuns : [];
|
|
1817
|
+
const detailRuns = Array.isArray(row.detailRuns)
|
|
1818
|
+
? row.detailRuns
|
|
1819
|
+
: (Array.isArray(row.selectedMetaRuns) ? row.selectedMetaRuns : []);
|
|
1820
|
+
const hasCtx = ctxRuns.length > 0;
|
|
1821
|
+
const hasDetail = row.active && detailRuns.length > 0;
|
|
1810
1822
|
|
|
1811
1823
|
return React.createElement(React.Fragment, null,
|
|
1812
|
-
React.createElement("div", { className: "ps-session-row-
|
|
1813
|
-
React.createElement(
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
? React.createElement("
|
|
1817
|
-
React.createElement(Runs, { runs:
|
|
1824
|
+
React.createElement("div", { className: "ps-session-row-line" },
|
|
1825
|
+
React.createElement("div", { className: "ps-session-row-title" },
|
|
1826
|
+
React.createElement(Runs, { runs: row.titleRuns, theme })),
|
|
1827
|
+
hasCtx
|
|
1828
|
+
? React.createElement("div", { className: "ps-session-row-ctx" },
|
|
1829
|
+
React.createElement(Runs, { runs: ctxRuns, theme }))
|
|
1818
1830
|
: null),
|
|
1819
|
-
|
|
1820
|
-
? React.createElement("div", { className: "ps-session-row-
|
|
1821
|
-
React.createElement(Runs, { runs:
|
|
1822
|
-
: null,
|
|
1823
|
-
hasSelectedMeta
|
|
1824
|
-
? React.createElement("div", { className: "ps-session-row-selected-meta" },
|
|
1825
|
-
React.createElement(Runs, { runs: row.selectedMetaRuns, theme }))
|
|
1831
|
+
hasDetail
|
|
1832
|
+
? React.createElement("div", { className: "ps-session-row-detail" },
|
|
1833
|
+
React.createElement(Runs, { runs: detailRuns, theme }))
|
|
1826
1834
|
: null);
|
|
1827
1835
|
}
|
|
1828
1836
|
|
|
@@ -3345,6 +3353,7 @@ function ModalLayer({ controller }) {
|
|
|
3345
3353
|
themePicker: selectThemePickerModal(state),
|
|
3346
3354
|
modelPicker: selectModelPickerModal(state),
|
|
3347
3355
|
reasoningEffortPicker: selectReasoningEffortPickerModal(state),
|
|
3356
|
+
contextTierPicker: selectContextTierPickerModal(state),
|
|
3348
3357
|
sessionAgentPicker: selectSessionAgentPickerModal(state),
|
|
3349
3358
|
sessionGroupPicker: selectSessionGroupPickerModal(state),
|
|
3350
3359
|
sessionGroupName: selectSessionGroupNameModal(state),
|
|
@@ -3399,6 +3408,7 @@ function ModalLayer({ controller }) {
|
|
|
3399
3408
|
"themePicker",
|
|
3400
3409
|
"modelPicker",
|
|
3401
3410
|
"reasoningEffortPicker",
|
|
3411
|
+
"contextTierPicker",
|
|
3402
3412
|
"sessionAgentPicker",
|
|
3403
3413
|
"sessionGroupPicker",
|
|
3404
3414
|
"artifactPicker",
|
|
@@ -3422,6 +3432,7 @@ function ModalLayer({ controller }) {
|
|
|
3422
3432
|
modalState.themePicker?.selectedRowIndex,
|
|
3423
3433
|
modalState.modelPicker?.selectedRowIndex,
|
|
3424
3434
|
modalState.reasoningEffortPicker?.selectedRowIndex,
|
|
3435
|
+
modalState.contextTierPicker?.selectedRowIndex,
|
|
3425
3436
|
modalState.sessionAgentPicker?.selectedRowIndex,
|
|
3426
3437
|
modalState.sessionGroupPicker?.selectedRowIndex,
|
|
3427
3438
|
modalState.artifactPicker?.selectedRowIndex,
|
|
@@ -3438,7 +3449,7 @@ function ModalLayer({ controller }) {
|
|
|
3438
3449
|
const renderListModal = (presentation, confirmLabel = "Apply") => {
|
|
3439
3450
|
const rows = Array.isArray(presentation.rows) ? presentation.rows : [];
|
|
3440
3451
|
const rowItemIndexes = Array.isArray(presentation.rowItemIndexes) ? presentation.rowItemIndexes : null;
|
|
3441
|
-
const usesHangingIndent = modal.type === "modelPicker" || modal.type === "reasoningEffortPicker" || modal.type === "sessionAgentPicker";
|
|
3452
|
+
const usesHangingIndent = modal.type === "modelPicker" || modal.type === "reasoningEffortPicker" || modal.type === "contextTierPicker" || modal.type === "sessionAgentPicker";
|
|
3442
3453
|
const renderedList = rowItemIndexes && rowItemIndexes.length === rows.length
|
|
3443
3454
|
? rows.map((row, rowIndex) => {
|
|
3444
3455
|
const itemIndex = rowItemIndexes[rowIndex];
|
|
@@ -3533,6 +3544,9 @@ function ModalLayer({ controller }) {
|
|
|
3533
3544
|
if (modal.type === "reasoningEffortPicker" && modalState.reasoningEffortPicker) {
|
|
3534
3545
|
return renderListModal(modalState.reasoningEffortPicker, pickerConfirmLabel);
|
|
3535
3546
|
}
|
|
3547
|
+
if (modal.type === "contextTierPicker" && modalState.contextTierPicker) {
|
|
3548
|
+
return renderListModal(modalState.contextTierPicker, pickerConfirmLabel);
|
|
3549
|
+
}
|
|
3536
3550
|
if (modal.type === "sessionAgentPicker" && modalState.sessionAgentPicker) {
|
|
3537
3551
|
return renderListModal(modalState.sessionAgentPicker, "Create Session");
|
|
3538
3552
|
}
|