@olares/lares-core 0.15.4 → 0.15.6
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/files/draft-images.js +25 -0
- package/files/filename.js +6 -0
- package/files/preview-tabs.js +51 -0
- package/files/preview-workspace.js +18 -31
- package/i18n/mobile.js +222 -0
- package/i18n/preview.js +2 -0
- package/larepass/chat.js +23 -7
- package/larepass/commands.js +49 -0
- package/larepass/context-occupancy.js +30 -0
- package/larepass/host.js +17 -0
- package/larepass/mux.js +37 -0
- package/larepass/permissions.js +56 -0
- package/larepass/queue.js +33 -0
- package/larepass/references.js +101 -0
- package/larepass/rpc.js +6 -3
- package/larepass/runtime.js +403 -17
- package/larepass/settings.js +49 -0
- package/larepass/submission-settings.js +26 -0
- package/larepass/transcript.js +12 -1
- package/larepass/turn-metrics.js +142 -0
- package/larepass/unread.js +64 -0
- package/larepass/workspace.js +161 -0
- package/olares/trusted-host.js +9 -0
- package/package.json +14 -2
- package/router/session-model.js +1 -1
- package/workspace/constants.js +1 -0
- package/workspace/seed.js +2 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const CONVERSATION_SETTINGS_NAMESPACE = "ui-conversation";
|
|
2
|
+
export const DEFAULT_BUSY_ENTER = "queue";
|
|
3
|
+
|
|
4
|
+
export function busyEnterBehavior(value) {
|
|
5
|
+
return value === "steer" ? "steer" : DEFAULT_BUSY_ENTER;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function resolveSubmitMode(running, accelerated, steeringAvailable, preferred) {
|
|
9
|
+
if (!running || !steeringAvailable) return "queue";
|
|
10
|
+
const busyEnter = busyEnterBehavior(preferred);
|
|
11
|
+
if (!accelerated) return busyEnter;
|
|
12
|
+
return busyEnter === "queue" ? "steer" : "queue";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function conversationSettings(described) {
|
|
16
|
+
const rows = Array.isArray(described?.namespaces) ? described.namespaces : [];
|
|
17
|
+
const section = rows.find((row) => row?.ns === CONVERSATION_SETTINGS_NAMESPACE);
|
|
18
|
+
if (!section) {
|
|
19
|
+
return { busyEnter: DEFAULT_BUSY_ENTER, revision: undefined, writable: false };
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
busyEnter: busyEnterBehavior(section.value?.busyEnter),
|
|
23
|
+
revision: Number.isInteger(section.revision) ? section.revision : undefined,
|
|
24
|
+
writable: described?.writable !== false,
|
|
25
|
+
};
|
|
26
|
+
}
|
package/larepass/transcript.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { producedPathsFromView } from "../files/deliverables.js";
|
|
2
2
|
import { contextForm, contextProvenance, isHumanUserSource } from "./context-provenance.js";
|
|
3
3
|
import { toolRowModel } from "./tool-row.js";
|
|
4
|
+
import { createTurnMetrics } from "./turn-metrics.js";
|
|
4
5
|
|
|
5
6
|
export function textFromBlocks(blocks) {
|
|
6
7
|
if (!Array.isArray(blocks)) return "";
|
|
@@ -98,6 +99,8 @@ export function foldTranscript(entries) {
|
|
|
98
99
|
const items = [];
|
|
99
100
|
const blocks = new Map();
|
|
100
101
|
const tools = new Map();
|
|
102
|
+
const metrics = createTurnMetrics();
|
|
103
|
+
const lastReply = new Map();
|
|
101
104
|
const retries = new Map();
|
|
102
105
|
const fileSeen = new Set();
|
|
103
106
|
let files = [];
|
|
@@ -132,7 +135,10 @@ export function foldTranscript(entries) {
|
|
|
132
135
|
};
|
|
133
136
|
|
|
134
137
|
const putBlock = (turn, step, index, row) => {
|
|
135
|
-
upsert(items, blocks, blockKey(turn, step, index), row);
|
|
138
|
+
const stored = upsert(items, blocks, blockKey(turn, step, index), row);
|
|
139
|
+
// The turn's readings belong under its closing reply, the row whose action
|
|
140
|
+
// bar ends the turn on screen.
|
|
141
|
+
if (stored.type === "assistant") lastReply.set(Number(turn) || 0, stored);
|
|
136
142
|
};
|
|
137
143
|
|
|
138
144
|
const chunkIndex = (data, chunk) => {
|
|
@@ -219,6 +225,7 @@ export function foldTranscript(entries) {
|
|
|
219
225
|
if (!event) continue;
|
|
220
226
|
const data = event.data ?? {};
|
|
221
227
|
const view = entryView(entry, event);
|
|
228
|
+
metrics.observe(event);
|
|
222
229
|
|
|
223
230
|
if (event.type === "turn/start") {
|
|
224
231
|
closeTurn();
|
|
@@ -322,6 +329,10 @@ export function foldTranscript(entries) {
|
|
|
322
329
|
}
|
|
323
330
|
|
|
324
331
|
settleOpen(items, running);
|
|
332
|
+
for (const [turn, row] of lastReply) {
|
|
333
|
+
const value = metrics.value(turn);
|
|
334
|
+
if (value) row.metrics = value;
|
|
335
|
+
}
|
|
325
336
|
if (running) syncFiles();
|
|
326
337
|
return {
|
|
327
338
|
items,
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn timings read off the session log the way the harness client reads them:
|
|
3
|
+
* the turn's own wall time, its first step's time to first token, and decode
|
|
4
|
+
* throughput over the steps that report both a first token and provider usage.
|
|
5
|
+
* Every event carries `time` (epoch ms), so nothing here needs a clock.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function isTokenDelta(chunk) {
|
|
9
|
+
return chunk?.type === "text-delta" || chunk?.type === "reasoning-delta";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function pad2(value) {
|
|
13
|
+
return String(value).padStart(2, "0");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createTurnMetrics() {
|
|
17
|
+
const turns = new Map();
|
|
18
|
+
|
|
19
|
+
const turnRow = (turn) => {
|
|
20
|
+
const key = Number(turn) || 0;
|
|
21
|
+
let row = turns.get(key);
|
|
22
|
+
if (!row) {
|
|
23
|
+
row = {
|
|
24
|
+
startTime: 0,
|
|
25
|
+
endTime: 0,
|
|
26
|
+
messageTime: 0,
|
|
27
|
+
ttftMs: null,
|
|
28
|
+
ttftStep: Infinity,
|
|
29
|
+
decodeMs: 0,
|
|
30
|
+
decodeTokens: 0,
|
|
31
|
+
steps: new Map(),
|
|
32
|
+
};
|
|
33
|
+
turns.set(key, row);
|
|
34
|
+
}
|
|
35
|
+
return row;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const stepRow = (row, step) => {
|
|
39
|
+
const key = Number(step) || 0;
|
|
40
|
+
let cell = row.steps.get(key);
|
|
41
|
+
if (!cell) {
|
|
42
|
+
cell = { startTime: 0, firstTokenTime: 0 };
|
|
43
|
+
row.steps.set(key, cell);
|
|
44
|
+
}
|
|
45
|
+
return cell;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
observe(event) {
|
|
50
|
+
const time = Number(event?.time) || 0;
|
|
51
|
+
if (!time) return;
|
|
52
|
+
const data = event.data ?? {};
|
|
53
|
+
const turn = data.turn ?? data.message?.turn;
|
|
54
|
+
const step = data.step ?? data.message?.step;
|
|
55
|
+
if (event.type === "turn/start") {
|
|
56
|
+
turnRow(turn).startTime = time;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (event.type === "turn/end") {
|
|
60
|
+
turnRow(turn).endTime = time;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (event.type === "step/start") {
|
|
64
|
+
stepRow(turnRow(turn), step).startTime = time;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (event.type === "assistant/chunk") {
|
|
68
|
+
if (!isTokenDelta(data.chunk)) return;
|
|
69
|
+
const cell = stepRow(turnRow(turn), step);
|
|
70
|
+
if (!cell.firstTokenTime) cell.firstTokenTime = time;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (event.type !== "assistant/message") return;
|
|
74
|
+
const row = turnRow(turn);
|
|
75
|
+
const index = Number(step) || 0;
|
|
76
|
+
const cell = stepRow(row, index);
|
|
77
|
+
row.messageTime = time;
|
|
78
|
+
// The turn shows its earliest step's latency; later steps answer a tool
|
|
79
|
+
// result rather than the person, so their wait is a different question.
|
|
80
|
+
if (cell.startTime && cell.firstTokenTime && index < row.ttftStep) {
|
|
81
|
+
row.ttftStep = index;
|
|
82
|
+
row.ttftMs = Math.max(0, cell.firstTokenTime - cell.startTime);
|
|
83
|
+
}
|
|
84
|
+
const outputTokens = Number(data.usage?.outputTokens);
|
|
85
|
+
if (cell.firstTokenTime && Number.isFinite(outputTokens) && outputTokens > 0) {
|
|
86
|
+
row.decodeMs += Math.max(0, time - cell.firstTokenTime);
|
|
87
|
+
row.decodeTokens += outputTokens;
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
/** Readings for a turn the log has closed; null while it is still open. */
|
|
91
|
+
value(turn) {
|
|
92
|
+
const row = turns.get(Number(turn) || 0);
|
|
93
|
+
if (!row?.startTime || !row.endTime) return null;
|
|
94
|
+
const metrics = {
|
|
95
|
+
time: row.messageTime || row.endTime,
|
|
96
|
+
runMs: Math.max(0, row.endTime - row.startTime),
|
|
97
|
+
};
|
|
98
|
+
if (row.ttftMs !== null) metrics.ttftMs = row.ttftMs;
|
|
99
|
+
if (row.decodeMs > 0 && row.decodeTokens > 0) {
|
|
100
|
+
metrics.tokensPerSecond = row.decodeTokens / (row.decodeMs / 1000);
|
|
101
|
+
}
|
|
102
|
+
return metrics;
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Wall clock of the reply, plus the calendar scope a reader needs to place it. */
|
|
108
|
+
export function clockParts(time, now = Date.now()) {
|
|
109
|
+
const at = new Date(Number(time) || 0);
|
|
110
|
+
const today = new Date(Number(now) || 0);
|
|
111
|
+
const sameYear = at.getFullYear() === today.getFullYear();
|
|
112
|
+
const sameDay = sameYear
|
|
113
|
+
&& at.getMonth() === today.getMonth()
|
|
114
|
+
&& at.getDate() === today.getDate();
|
|
115
|
+
return {
|
|
116
|
+
clock: `${pad2(at.getHours())}:${pad2(at.getMinutes())}`,
|
|
117
|
+
scope: sameDay ? "day" : sameYear ? "md" : "ymd",
|
|
118
|
+
y: at.getFullYear(),
|
|
119
|
+
m: at.getMonth() + 1,
|
|
120
|
+
d: at.getDate(),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Run time as whole seconds, carrying into minutes with padded seconds. */
|
|
125
|
+
export function durationParts(ms) {
|
|
126
|
+
const total = Math.max(0, Math.floor((Number(ms) || 0) / 1000));
|
|
127
|
+
const minutes = Math.floor(total / 60);
|
|
128
|
+
const seconds = total % 60;
|
|
129
|
+
return { minutes, seconds: minutes > 0 ? pad2(seconds) : String(seconds) };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Sub-turn latency: one decimal under ten seconds, whole seconds beyond. */
|
|
133
|
+
export function latencySeconds(ms) {
|
|
134
|
+
const seconds = Math.max(0, Number(ms) || 0) / 1000;
|
|
135
|
+
return seconds < 10 ? String(Math.round(seconds * 10) / 10) : String(Math.round(seconds));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Decode throughput: whole tokens from ten up, one decimal below. */
|
|
139
|
+
export function throughputTokens(tokensPerSecond) {
|
|
140
|
+
const value = Math.max(0, Number(tokensPerSecond) || 0);
|
|
141
|
+
return value >= 10 ? String(Math.round(value)) : String(Math.round(value * 10) / 10);
|
|
142
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which sessions moved on without being watched.
|
|
3
|
+
*
|
|
4
|
+
* The host has no read state to hand down: `session.list` carries `updatedAt`,
|
|
5
|
+
* `running` and `blank` and nothing else. The mux, however, streams events for
|
|
6
|
+
* every session at once, so a client knows about work landing in a session it
|
|
7
|
+
* is not showing. "Seen" is therefore decided here, per device, against the
|
|
8
|
+
* last moment each session was the open one.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const SEEN_STORAGE_KEY = "lares.session.seen";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {unknown} value - whatever the store held, including nothing.
|
|
15
|
+
* @param {number} [now]
|
|
16
|
+
* @returns {{ since: number, sessions: Record<string, number> }}
|
|
17
|
+
*/
|
|
18
|
+
export function readSeen(value, now = Date.now()) {
|
|
19
|
+
const rows = value && typeof value === "object" ? value.sessions : null;
|
|
20
|
+
const sessions = {};
|
|
21
|
+
if (rows && typeof rows === "object") {
|
|
22
|
+
for (const [sessionId, at] of Object.entries(rows)) {
|
|
23
|
+
const stamp = Number(at);
|
|
24
|
+
if (sessionId && Number.isFinite(stamp) && stamp > 0) sessions[sessionId] = stamp;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const since = Number(value && typeof value === "object" ? value.since : 0);
|
|
28
|
+
return { since: Number.isFinite(since) && since > 0 ? since : now, sessions };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function markSeen(seen, sessionId, at = Date.now()) {
|
|
32
|
+
if (!sessionId) return seen;
|
|
33
|
+
const previous = seen.sessions[sessionId] ?? 0;
|
|
34
|
+
const stamp = Math.max(previous, Number(at) || 0);
|
|
35
|
+
if (stamp === previous) return seen;
|
|
36
|
+
return { since: seen.since, sessions: { ...seen.sessions, [sessionId]: stamp } };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Forget rows the host no longer lists, so the store cannot grow forever. */
|
|
40
|
+
export function pruneSeen(seen, sessions) {
|
|
41
|
+
const live = new Set((Array.isArray(sessions) ? sessions : []).map((row) => row?.sessionId).filter(Boolean));
|
|
42
|
+
const kept = Object.entries(seen.sessions).filter(([sessionId]) => live.has(sessionId));
|
|
43
|
+
if (kept.length === Object.keys(seen.sessions).length) return seen;
|
|
44
|
+
return { since: seen.since, sessions: Object.fromEntries(kept) };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A session never opened on this device is only news when it moved after
|
|
49
|
+
* tracking began — otherwise every history row would light up on first run.
|
|
50
|
+
* The open session is never marked: it is being watched right now.
|
|
51
|
+
*
|
|
52
|
+
* @returns {Record<string, true>} keyed by session id, for template lookups.
|
|
53
|
+
*/
|
|
54
|
+
export function unseenSessions(sessions, seen, openSessionId = "") {
|
|
55
|
+
const marks = {};
|
|
56
|
+
for (const row of Array.isArray(sessions) ? sessions : []) {
|
|
57
|
+
const sessionId = row?.sessionId;
|
|
58
|
+
if (!sessionId || sessionId === openSessionId || row.blank) continue;
|
|
59
|
+
const updatedAt = Number(row.updatedAt) || 0;
|
|
60
|
+
if (!updatedAt) continue;
|
|
61
|
+
if (updatedAt > (seen.sessions[sessionId] ?? seen.since)) marks[sessionId] = true;
|
|
62
|
+
}
|
|
63
|
+
return marks;
|
|
64
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { visibleHistorySessions } from "./chat.js";
|
|
2
|
+
|
|
3
|
+
export const UNGROUPED_WORKSPACE = "";
|
|
4
|
+
|
|
5
|
+
/** Rows a workspace group shows before the "show more" cut (Lares app parity). */
|
|
6
|
+
export const COLLAPSED_SESSION_LIMIT = 5;
|
|
7
|
+
|
|
8
|
+
export function collapsedSessions(sessions, expanded) {
|
|
9
|
+
const rows = Array.isArray(sessions) ? sessions : [];
|
|
10
|
+
return expanded ? rows : rows.slice(0, COLLAPSED_SESSION_LIMIT);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function hiddenSessionCount(sessions) {
|
|
14
|
+
return Math.max(0, (Array.isArray(sessions) ? sessions.length : 0) - COLLAPSED_SESSION_LIMIT);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const FLAT_SESSION_ORDER_KEY = "__flat_session_order__";
|
|
18
|
+
|
|
19
|
+
export function reconciledSessionOrder(sessionIds, stored) {
|
|
20
|
+
if (!Array.isArray(stored)) return [...sessionIds];
|
|
21
|
+
const available = new Set(sessionIds);
|
|
22
|
+
const included = new Set();
|
|
23
|
+
const ordered = [];
|
|
24
|
+
for (const id of stored) {
|
|
25
|
+
if (!available.has(id) || included.has(id)) continue;
|
|
26
|
+
ordered.push(id);
|
|
27
|
+
included.add(id);
|
|
28
|
+
}
|
|
29
|
+
for (const id of sessionIds) {
|
|
30
|
+
if (!included.has(id)) ordered.push(id);
|
|
31
|
+
}
|
|
32
|
+
return ordered;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function compareSessionRecency(leftId, rightId, byId) {
|
|
36
|
+
const left = Number(byId.get(leftId)?.updatedAt) || 0;
|
|
37
|
+
const right = Number(byId.get(rightId)?.updatedAt) || 0;
|
|
38
|
+
if (left !== right) return right - left;
|
|
39
|
+
return leftId < rightId ? -1 : 1;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function sessionIdsByRecency(rows) {
|
|
43
|
+
const byId = new Map(rows.map((row) => [row.sessionId, row]));
|
|
44
|
+
return rows
|
|
45
|
+
.map((row) => row.sessionId)
|
|
46
|
+
.sort((left, right) => compareSessionRecency(left, right, byId));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function nextSessionOrderAccount({
|
|
50
|
+
sessionIds,
|
|
51
|
+
previousOrder,
|
|
52
|
+
previousUpdatedAt = {},
|
|
53
|
+
sessions,
|
|
54
|
+
orderBy,
|
|
55
|
+
sortByRecency = false,
|
|
56
|
+
}) {
|
|
57
|
+
const byId = new Map((Array.isArray(sessions) ? sessions : []).map((row) => [row.sessionId, row]));
|
|
58
|
+
let order = reconciledSessionOrder(sessionIds, previousOrder);
|
|
59
|
+
if (sortByRecency) {
|
|
60
|
+
order.sort((left, right) => compareSessionRecency(left, right, byId));
|
|
61
|
+
} else if (orderBy === "updated") {
|
|
62
|
+
const promoted = sessionIds
|
|
63
|
+
.filter((id) => {
|
|
64
|
+
const row = byId.get(id);
|
|
65
|
+
return row && (previousUpdatedAt[id] === undefined || Number(row.updatedAt) > previousUpdatedAt[id]);
|
|
66
|
+
})
|
|
67
|
+
.sort((left, right) => compareSessionRecency(left, right, byId));
|
|
68
|
+
if (promoted.length) {
|
|
69
|
+
const ids = new Set(promoted);
|
|
70
|
+
order = [...promoted, ...order.filter((id) => !ids.has(id))];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
order,
|
|
75
|
+
updatedAt: Object.fromEntries(sessionIds.flatMap((id) => {
|
|
76
|
+
const row = byId.get(id);
|
|
77
|
+
return row ? [[id, Number(row.updatedAt) || 0]] : [];
|
|
78
|
+
})),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function workspaceForSession(workspaces, sessionId) {
|
|
83
|
+
if (!sessionId) return undefined;
|
|
84
|
+
return (Array.isArray(workspaces) ? workspaces : []).find(
|
|
85
|
+
(workspace) => Array.isArray(workspace?.sessionIds) && workspace.sessionIds.includes(sessionId),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function normalizeWorkspaceList(value) {
|
|
90
|
+
return {
|
|
91
|
+
items: Array.isArray(value?.items) ? value.items.filter((row) => row?.workspaceId) : [],
|
|
92
|
+
archivedSessionIds: Array.isArray(value?.archivedSessionIds) ? value.archivedSessionIds : [],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function groupWorkspaceSessions(
|
|
97
|
+
sessions,
|
|
98
|
+
workspaces,
|
|
99
|
+
archivedSessionIds = [],
|
|
100
|
+
{ query = "", flat = false, currentSessionId = "", sessionOrderByAccount = {} } = {},
|
|
101
|
+
) {
|
|
102
|
+
const archived = new Set(archivedSessionIds);
|
|
103
|
+
const normalizedQuery = String(query).trim().toLocaleLowerCase();
|
|
104
|
+
// Same visibility cut as the Lares app: archived rows never show, and a blank
|
|
105
|
+
// session is only ever the selected provisional one.
|
|
106
|
+
const visible = visibleHistorySessions(sessions, currentSessionId).filter((row) => {
|
|
107
|
+
if (archived.has(row.sessionId)) return false;
|
|
108
|
+
if (!normalizedQuery) return true;
|
|
109
|
+
return String(row.title || "").toLocaleLowerCase().includes(normalizedQuery);
|
|
110
|
+
});
|
|
111
|
+
const byId = new Map(visible.map((row) => [row.sessionId, row]));
|
|
112
|
+
|
|
113
|
+
if (flat) {
|
|
114
|
+
const baseIds = sessionIdsByRecency(visible);
|
|
115
|
+
const ids = reconciledSessionOrder(
|
|
116
|
+
baseIds,
|
|
117
|
+
sessionOrderByAccount[FLAT_SESSION_ORDER_KEY],
|
|
118
|
+
);
|
|
119
|
+
return [{
|
|
120
|
+
key: "__all__",
|
|
121
|
+
workspaceId: undefined,
|
|
122
|
+
title: "",
|
|
123
|
+
sessions: ids.map((id) => byId.get(id)).filter(Boolean),
|
|
124
|
+
}];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const accounted = new Set();
|
|
128
|
+
const groups = (Array.isArray(workspaces) ? workspaces : []).map((workspace) => {
|
|
129
|
+
const rows = [];
|
|
130
|
+
const ids = reconciledSessionOrder(
|
|
131
|
+
workspace.sessionIds ?? [],
|
|
132
|
+
sessionOrderByAccount[workspace.workspaceId],
|
|
133
|
+
);
|
|
134
|
+
for (const sessionId of ids) {
|
|
135
|
+
accounted.add(sessionId);
|
|
136
|
+
const row = byId.get(sessionId);
|
|
137
|
+
if (row) rows.push(row);
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
key: workspace.workspaceId,
|
|
141
|
+
workspaceId: workspace.workspaceId,
|
|
142
|
+
title: workspace.title,
|
|
143
|
+
path: workspace.path,
|
|
144
|
+
sessions: rows,
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
const ungroupedRows = visible.filter((row) => !accounted.has(row.sessionId));
|
|
148
|
+
const ungrouped = reconciledSessionOrder(
|
|
149
|
+
sessionIdsByRecency(ungroupedRows),
|
|
150
|
+
sessionOrderByAccount[UNGROUPED_WORKSPACE],
|
|
151
|
+
).map((id) => byId.get(id)).filter(Boolean);
|
|
152
|
+
if (ungrouped.length) {
|
|
153
|
+
groups.push({
|
|
154
|
+
key: UNGROUPED_WORKSPACE,
|
|
155
|
+
workspaceId: undefined,
|
|
156
|
+
title: "",
|
|
157
|
+
sessions: ungrouped,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return groups.filter((group) => !normalizedQuery || group.sessions.length > 0);
|
|
161
|
+
}
|
package/olares/trusted-host.js
CHANGED
|
@@ -34,4 +34,13 @@ export function loopbackAuthority(port) {
|
|
|
34
34
|
export function applyLoopbackHeaders(headers, authority) {
|
|
35
35
|
headers.host = authority;
|
|
36
36
|
if (headers.origin !== undefined) headers.origin = `http://${authority}`;
|
|
37
|
+
// Fetch Metadata outlives the origin rewrite. The browser decides this one and
|
|
38
|
+
// we cannot argue with it: a packaged page calling the entrance is `cross-site`
|
|
39
|
+
// however the host and origin now read, and dsh refuses that outright — the
|
|
40
|
+
// rewrite lands, the request is still 403. Restate it to match the loopback
|
|
41
|
+
// pair the two lines above just produced.
|
|
42
|
+
//
|
|
43
|
+
// Only secure contexts get `Sec-` headers at all, which is why the LAN
|
|
44
|
+
// entrance (plain http) never ran into this and the https one always did.
|
|
45
|
+
if (headers["sec-fetch-site"] !== undefined) headers["sec-fetch-site"] = "same-origin";
|
|
37
46
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@olares/lares-core",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.6",
|
|
4
4
|
"description": "Lares 共享业务核心:Router、workspace、文件/媒体、Olares 身份(无 UI、无平台壳)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"./workspace/env": "./workspace/env.js",
|
|
62
62
|
"./workspace/home": "./workspace/home.js",
|
|
63
63
|
"./workspace/path": "./workspace/path.js",
|
|
64
|
+
"./workspace/constants": "./workspace/constants.js",
|
|
64
65
|
"./workspace/seed": "./workspace/seed.js",
|
|
65
66
|
"./workspace/session": "./workspace/session.js",
|
|
66
67
|
"./files/filename": "./files/filename.js",
|
|
@@ -70,12 +71,14 @@
|
|
|
70
71
|
"./files/upload-client": "./files/upload-client.js",
|
|
71
72
|
"./files/preview": "./files/preview.js",
|
|
72
73
|
"./files/preview-workspace": "./files/preview-workspace.js",
|
|
74
|
+
"./files/preview-tabs": "./files/preview-tabs.js",
|
|
73
75
|
"./files/preview-groups": "./files/preview-groups.js",
|
|
74
76
|
"./files/preview-cache": "./files/preview-cache.js",
|
|
75
77
|
"./files/markdown": "./files/markdown.js",
|
|
76
78
|
"./files/deliverables": "./files/deliverables.js",
|
|
77
79
|
"./files/disposition": "./files/disposition.js",
|
|
78
80
|
"./files/intake": "./files/intake.js",
|
|
81
|
+
"./files/draft-images": "./files/draft-images.js",
|
|
79
82
|
"./files/mention": "./files/mention.js",
|
|
80
83
|
"./drive/files-path": "./drive/files-path.js",
|
|
81
84
|
"./drive/paths": "./drive/paths.js",
|
|
@@ -110,6 +113,15 @@
|
|
|
110
113
|
"./larepass/session-cache": "./larepass/session-cache.js",
|
|
111
114
|
"./larepass/runtime": "./larepass/runtime.js",
|
|
112
115
|
"./larepass/questions": "./larepass/questions.js",
|
|
113
|
-
"./larepass/settings": "./larepass/settings.js"
|
|
116
|
+
"./larepass/settings": "./larepass/settings.js",
|
|
117
|
+
"./larepass/workspace": "./larepass/workspace.js",
|
|
118
|
+
"./larepass/unread": "./larepass/unread.js",
|
|
119
|
+
"./larepass/permissions": "./larepass/permissions.js",
|
|
120
|
+
"./larepass/commands": "./larepass/commands.js",
|
|
121
|
+
"./larepass/queue": "./larepass/queue.js",
|
|
122
|
+
"./larepass/references": "./larepass/references.js",
|
|
123
|
+
"./larepass/submission-settings": "./larepass/submission-settings.js",
|
|
124
|
+
"./larepass/turn-metrics": "./larepass/turn-metrics.js",
|
|
125
|
+
"./larepass/context-occupancy": "./larepass/context-occupancy.js"
|
|
114
126
|
}
|
|
115
127
|
}
|
package/router/session-model.js
CHANGED
|
@@ -127,7 +127,7 @@ export function groupModelsByProvider(models) {
|
|
|
127
127
|
for (const model of models) {
|
|
128
128
|
let group = index.get(model.provider);
|
|
129
129
|
if (group === undefined) {
|
|
130
|
-
group = { provider: model.provider, models: [] };
|
|
130
|
+
group = { provider: model.provider, name: model.providerName || model.provider, models: [] };
|
|
131
131
|
index.set(model.provider, group);
|
|
132
132
|
groups.push(group);
|
|
133
133
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEFAULT_WORKSPACE_TITLE = "Default";
|
package/workspace/seed.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { DEFAULT_WORKSPACE_TITLE } from "./constants.js";
|
|
2
3
|
|
|
3
|
-
export
|
|
4
|
+
export { DEFAULT_WORKSPACE_TITLE };
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* @param {{ create: (path: string, title?: string) => Promise<{ id: string, path: string, title: string }> }} registry
|