@olares/lares-core 0.15.5 → 0.15.7
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 +226 -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/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,25 @@
|
|
|
1
|
+
export function createDraftImageEntry(file) {
|
|
2
|
+
return {
|
|
3
|
+
id: globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`,
|
|
4
|
+
file,
|
|
5
|
+
previewUrl: URL.createObjectURL(file),
|
|
6
|
+
status: "uploading",
|
|
7
|
+
path: "",
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function draftImageStatus(row) {
|
|
12
|
+
return row?.status ?? "ready";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function draftImagesUploading(images) {
|
|
16
|
+
return (images ?? []).some((row) => draftImageStatus(row) === "uploading");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function draftImagesSendable(images) {
|
|
20
|
+
return (images ?? []).some((row) => draftImageStatus(row) === "ready");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function draftHasSendableContent(draft, images) {
|
|
24
|
+
return Boolean(String(draft ?? "").trim()) || draftImagesSendable(images);
|
|
25
|
+
}
|
package/files/filename.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/** Display name of a workspace path, which may arrive with either separator. */
|
|
2
|
+
export function fileName(path) {
|
|
3
|
+
const parts = String(path).split(/[/\\]/);
|
|
4
|
+
return parts.at(-1) || path;
|
|
5
|
+
}
|
|
6
|
+
|
|
1
7
|
export function posixBasename(path) {
|
|
2
8
|
const raw = String(path ?? "");
|
|
3
9
|
const trimmed = raw.replace(/\/+$/, "");
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tab-strip rules for workspace file previews. The dsh web overlay and the
|
|
3
|
+
* LarePass PC shell fetch through different transports but must open, evict and
|
|
4
|
+
* close tabs the same way, so the rules live here and each surface keeps only
|
|
5
|
+
* its own content cache.
|
|
6
|
+
*/
|
|
7
|
+
import { fileName } from "./filename.js";
|
|
8
|
+
|
|
9
|
+
export const MAX_PREVIEW_TABS = 8;
|
|
10
|
+
|
|
11
|
+
/** Least-recently-used order, oldest first. */
|
|
12
|
+
export function touchTab(lru, path) {
|
|
13
|
+
return [...lru.filter((item) => item !== path), path];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @returns tabs, the new LRU order, and the tab an over-full strip dropped so
|
|
18
|
+
* the caller can release whatever it cached for it.
|
|
19
|
+
*/
|
|
20
|
+
export function openTab({ tabs, lru }, path) {
|
|
21
|
+
if (tabs.some((tab) => tab.path === path)) {
|
|
22
|
+
return { tabs, lru: touchTab(lru, path), evicted: null };
|
|
23
|
+
}
|
|
24
|
+
let kept = tabs;
|
|
25
|
+
let evicted = null;
|
|
26
|
+
if (kept.length >= MAX_PREVIEW_TABS) {
|
|
27
|
+
evicted = kept.find((tab) => tab.path === lru[0]) ?? kept[0];
|
|
28
|
+
kept = kept.filter((tab) => tab.path !== evicted.path);
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
tabs: [...kept, { path, name: fileName(path) }],
|
|
32
|
+
lru: touchTab(evicted ? lru.filter((item) => item !== evicted.path) : lru, path),
|
|
33
|
+
evicted,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @returns null when the path owns no tab, otherwise the strip without it. The
|
|
39
|
+
* reader lands on the tab they read last; an empty strip reports no active path.
|
|
40
|
+
*/
|
|
41
|
+
export function closeTab({ tabs, lru, activePath }, path) {
|
|
42
|
+
if (!tabs.some((tab) => tab.path === path)) return null;
|
|
43
|
+
const kept = tabs.filter((tab) => tab.path !== path);
|
|
44
|
+
const order = lru.filter((item) => item !== path);
|
|
45
|
+
if (kept.length === 0) return { tabs: kept, lru: order, activePath: null };
|
|
46
|
+
return {
|
|
47
|
+
tabs: kept,
|
|
48
|
+
lru: order,
|
|
49
|
+
activePath: activePath === path ? order.at(-1) ?? kept.at(-1).path : activePath,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { closeTab, openTab, touchTab } from "./preview-tabs.js";
|
|
2
|
+
|
|
1
3
|
export class NullScrollport {
|
|
2
4
|
offset() {
|
|
3
5
|
return null;
|
|
@@ -6,13 +8,6 @@ export class NullScrollport {
|
|
|
6
8
|
scrollTo() {}
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
const MAX_TABS = 8;
|
|
10
|
-
|
|
11
|
-
export function fileName(path) {
|
|
12
|
-
const parts = String(path).split(/[/\\]/);
|
|
13
|
-
return parts.at(-1) || path;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
11
|
function initialSnapshot() {
|
|
17
12
|
return {
|
|
18
13
|
mode: "chat",
|
|
@@ -174,33 +169,23 @@ export class FilePreviewWorkspace {
|
|
|
174
169
|
open(sessionId, path, resolved) {
|
|
175
170
|
if (resolved?.status === "ready" && resolved.data?.path) path = resolved.data.path;
|
|
176
171
|
const state = this.session(sessionId);
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const evicted = tabs.find((tab) => tab.path === victim);
|
|
184
|
-
tabs = tabs.filter((tab) => tab.path !== victim);
|
|
185
|
-
state.lru = state.lru.filter((item) => item !== victim);
|
|
186
|
-
state.contents.delete(victim);
|
|
187
|
-
state.offsets.delete(victim);
|
|
188
|
-
state.requestVersions.delete(victim);
|
|
189
|
-
evictedName = evicted?.name ?? null;
|
|
190
|
-
}
|
|
191
|
-
tabs = [...tabs, { path, name: fileName(path) }];
|
|
172
|
+
const { tabs, lru, evicted } = openTab({ tabs: state.snapshot.tabs, lru: state.lru }, path);
|
|
173
|
+
state.lru = lru;
|
|
174
|
+
if (evicted) {
|
|
175
|
+
state.contents.delete(evicted.path);
|
|
176
|
+
state.offsets.delete(evicted.path);
|
|
177
|
+
state.requestVersions.delete(evicted.path);
|
|
192
178
|
}
|
|
193
179
|
if (resolved !== undefined) {
|
|
194
180
|
state.contents.set(path, resolved);
|
|
195
181
|
state.requestVersions.set(path, (state.requestVersions.get(path) ?? 0) + 1);
|
|
196
182
|
}
|
|
197
|
-
this.touch(state, path);
|
|
198
183
|
this.emit(sessionId, {
|
|
199
184
|
mode: "preview",
|
|
200
185
|
tabs,
|
|
201
186
|
activePath: path,
|
|
202
187
|
content: state.contents.get(path) ?? { status: "idle" },
|
|
203
|
-
evictedName,
|
|
188
|
+
evictedName: evicted?.name ?? null,
|
|
204
189
|
});
|
|
205
190
|
if (resolved === undefined) void this.load(sessionId, path, true);
|
|
206
191
|
}
|
|
@@ -220,9 +205,13 @@ export class FilePreviewWorkspace {
|
|
|
220
205
|
|
|
221
206
|
close(sessionId, path) {
|
|
222
207
|
const state = this.session(sessionId);
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
208
|
+
const closed = closeTab(
|
|
209
|
+
{ tabs: state.snapshot.tabs, lru: state.lru, activePath: state.snapshot.activePath },
|
|
210
|
+
path,
|
|
211
|
+
);
|
|
212
|
+
if (!closed) return;
|
|
213
|
+
const { tabs, activePath } = closed;
|
|
214
|
+
state.lru = closed.lru;
|
|
226
215
|
state.contents.delete(path);
|
|
227
216
|
state.offsets.delete(path);
|
|
228
217
|
state.requestVersions.set(path, (state.requestVersions.get(path) ?? 0) + 1);
|
|
@@ -236,11 +225,10 @@ export class FilePreviewWorkspace {
|
|
|
236
225
|
});
|
|
237
226
|
return;
|
|
238
227
|
}
|
|
239
|
-
if (state.snapshot.activePath
|
|
228
|
+
if (activePath === state.snapshot.activePath) {
|
|
240
229
|
this.emit(sessionId, { tabs, evictedName: null });
|
|
241
230
|
return;
|
|
242
231
|
}
|
|
243
|
-
const activePath = state.lru.at(-1) ?? tabs.at(-1).path;
|
|
244
232
|
this.emit(sessionId, {
|
|
245
233
|
tabs,
|
|
246
234
|
activePath,
|
|
@@ -261,8 +249,7 @@ export class FilePreviewWorkspace {
|
|
|
261
249
|
}
|
|
262
250
|
|
|
263
251
|
touch(state, path) {
|
|
264
|
-
state.lru = state.lru
|
|
265
|
-
state.lru.push(path);
|
|
252
|
+
state.lru = touchTab(state.lru, path);
|
|
266
253
|
}
|
|
267
254
|
|
|
268
255
|
/**
|
package/i18n/mobile.js
CHANGED
|
@@ -12,7 +12,56 @@ export const ZH = {
|
|
|
12
12
|
"probe.entrance": "入口 {origin}",
|
|
13
13
|
"chat.empty": "发一条消息开始对话",
|
|
14
14
|
"chat.placeholder": "发消息或按住说话",
|
|
15
|
+
"chat.placeholderDesktop": "给智能体发送消息",
|
|
16
|
+
"chat.placeholderNewSession": "描述你想构建的内容",
|
|
17
|
+
"chat.placeholderPlan": "描述要规划的任务",
|
|
18
|
+
"chat.placeholderNoModel": "请选择一个可用模型",
|
|
19
|
+
"chat.placeholderSteerQueue": "按 {shortcut} 立即发送全部排队消息",
|
|
15
20
|
"chat.send": "发送",
|
|
21
|
+
"chat.stop": "停止生成",
|
|
22
|
+
"queue.count": "{n} 条排队消息",
|
|
23
|
+
"queue.edit": "编辑",
|
|
24
|
+
"queue.save": "保存",
|
|
25
|
+
"queue.cancelEdit": "取消编辑",
|
|
26
|
+
"queue.remove": "移除",
|
|
27
|
+
"queue.steer": "立即发送",
|
|
28
|
+
"access.confirm.title": "确认启用 Full access?",
|
|
29
|
+
"access.confirm.description": "启用 Full access 后,智能体将减少确认步骤,并且可以直接执行更多操作,包括敏感操作、文件修改或外部命令。仅建议在你信任当前任务时使用。",
|
|
30
|
+
"access.confirm.acknowledge": "我已了解风险,并愿意继续",
|
|
31
|
+
"access.confirm.cancel": "取消",
|
|
32
|
+
"access.confirm.enable": "启用 Full access",
|
|
33
|
+
"plan.active": "规划模式",
|
|
34
|
+
"plan.disable": "退出规划模式",
|
|
35
|
+
"context.title": "上下文占用",
|
|
36
|
+
"context.used": "{percent}% · 约 {used} / {total} tokens",
|
|
37
|
+
"context.system": "系统提示",
|
|
38
|
+
"context.tools": "工具定义",
|
|
39
|
+
"context.conversation": "对话",
|
|
40
|
+
"todo.progress": "任务 {done}/{total}",
|
|
41
|
+
"goal.edit": "编辑目标",
|
|
42
|
+
"goal.pause": "暂停目标",
|
|
43
|
+
"goal.resume": "继续目标",
|
|
44
|
+
"goal.clear": "清除目标",
|
|
45
|
+
"approval.waiting": "等待确认",
|
|
46
|
+
"approval.escalation": "{toolName} 请求执行权限",
|
|
47
|
+
"approval.reject": "拒绝",
|
|
48
|
+
"approval.allowOnce": "允许一次",
|
|
49
|
+
"question.waiting": "需要你的选择",
|
|
50
|
+
"question.other": "其他",
|
|
51
|
+
"question.back": "上一步",
|
|
52
|
+
"question.next": "下一步",
|
|
53
|
+
"question.submit": "提交",
|
|
54
|
+
"settings.input.title": "输入",
|
|
55
|
+
"settings.input.busyEnter.hint": "智能体运行时,普通 Enter 的发送方式;Cmd/Ctrl+Enter 使用相反方式。",
|
|
56
|
+
"settings.input.busyEnter.queue": "Enter 加入队列",
|
|
57
|
+
"settings.input.busyEnter.steer": "Enter 立即插队",
|
|
58
|
+
"references.empty": "没有匹配的文件或会话",
|
|
59
|
+
// Section and kind words are dsh's `reference` dictionary; keep them in step.
|
|
60
|
+
"references.section.files": "文件与文件夹",
|
|
61
|
+
"references.section.sessions": "Session 对话",
|
|
62
|
+
"references.file": "文件",
|
|
63
|
+
"references.directory": "文件夹",
|
|
64
|
+
"references.session": "Session",
|
|
16
65
|
"chat.sending": "正在回复…",
|
|
17
66
|
"action.copy": "复制",
|
|
18
67
|
"action.copied": "已复制",
|
|
@@ -21,6 +70,8 @@ export const ZH = {
|
|
|
21
70
|
"action.share": "转发",
|
|
22
71
|
"action.bar": "回复操作",
|
|
23
72
|
"composer.attach": "添加文件",
|
|
73
|
+
"composer.removeAttachment": "移除 {name}",
|
|
74
|
+
"composer.previewAttachment": "查看 {name}",
|
|
24
75
|
"composer.voice": "语音输入",
|
|
25
76
|
"composer.think": "深度思考",
|
|
26
77
|
"composer.search": "智能搜索",
|
|
@@ -38,6 +89,13 @@ export const ZH = {
|
|
|
38
89
|
"message.retry.status": "{label} ({retry}/{maximum}) · {seconds}s",
|
|
39
90
|
"message.retry.delay": "Retry delay: ",
|
|
40
91
|
"message.retry.failure": "Failure reason: ",
|
|
92
|
+
"message.ranFor": "用时 {duration}",
|
|
93
|
+
"message.ttft": "首 token {seconds}秒",
|
|
94
|
+
"message.tokensPerSecond": "{tps} tok/s",
|
|
95
|
+
"duration.seconds": "{seconds}秒",
|
|
96
|
+
"duration.minutes": "{minutes}分{seconds}秒",
|
|
97
|
+
"clock.md": "{m}月{d}日",
|
|
98
|
+
"clock.ymd": "{y}年{m}月{d}日",
|
|
41
99
|
"bar.history": "历史",
|
|
42
100
|
"bar.menu": "菜单",
|
|
43
101
|
"bar.new": "新对话",
|
|
@@ -45,10 +103,66 @@ export const ZH = {
|
|
|
45
103
|
"history.empty": "还没有其他对话",
|
|
46
104
|
"history.untitled": "新对话",
|
|
47
105
|
"history.loading": "正在加载对话列表…",
|
|
106
|
+
"history.noMoveTargets": "没有其他工作区可移动",
|
|
48
107
|
"chat.loading": "正在加载对话…",
|
|
49
108
|
"history.today": "今天",
|
|
50
109
|
"history.week": "之前 7 天",
|
|
51
110
|
"history.older": "更早",
|
|
111
|
+
"desktop.newSession": "新会话",
|
|
112
|
+
"desktop.workspaces": "工作区",
|
|
113
|
+
"desktop.sessions": "会话",
|
|
114
|
+
"desktop.ungrouped": "未分组",
|
|
115
|
+
"desktop.searchSessions": "搜索会话",
|
|
116
|
+
"desktop.clearSearch": "清除搜索",
|
|
117
|
+
"desktop.viewOptions": "视图选项",
|
|
118
|
+
"desktop.addWorkspace": "添加工作区",
|
|
119
|
+
"desktop.addingWorkspace": "正在添加…",
|
|
120
|
+
"desktop.open": "打开",
|
|
121
|
+
"desktop.cancel": "取消",
|
|
122
|
+
"desktop.close": "关闭",
|
|
123
|
+
"desktop.groupBy": "分组方式",
|
|
124
|
+
"desktop.byWorkspace": "按工作区",
|
|
125
|
+
"desktop.oneList": "单列表",
|
|
126
|
+
"desktop.orderBy": "排序方式",
|
|
127
|
+
"desktop.recent": "最近更新",
|
|
128
|
+
"desktop.manual": "手动排序",
|
|
129
|
+
"desktop.commands": "命令",
|
|
130
|
+
"desktop.noCommands": "这个会话没有可用命令",
|
|
131
|
+
// Preset keys arrive from the host as kebab ids; the words live here.
|
|
132
|
+
"permission.read-only": "只读",
|
|
133
|
+
"permission.read-only.hint": "只读文件;写入和风险命令需要确认。",
|
|
134
|
+
"permission.workspace-write": "工作区可写",
|
|
135
|
+
"permission.workspace-write.hint": "可在工作区内写入;超出范围需要确认。",
|
|
136
|
+
"permission.danger-full-access": "完全访问",
|
|
137
|
+
"permission.danger-full-access.hint": "没有文件沙箱,也不再询问。",
|
|
138
|
+
"desktop.selectWorkspaceDirectory": "选择工作区目录",
|
|
139
|
+
"desktop.chooseFolder": "选择 Olares Host 文件夹",
|
|
140
|
+
"desktop.newFolder": "新建文件夹",
|
|
141
|
+
"desktop.newFolderIn": "在「{name}」中新建",
|
|
142
|
+
"desktop.untitledFolder": "未命名文件夹",
|
|
143
|
+
"desktop.createFolder": "创建",
|
|
144
|
+
"desktop.showHiddenFiles": "显示隐藏文件",
|
|
145
|
+
"desktop.folderPath": "文件夹路径",
|
|
146
|
+
"desktop.home": "主目录",
|
|
147
|
+
"desktop.loadingFolders": "正在读取文件夹…",
|
|
148
|
+
"desktop.noFolders": "这里没有子文件夹",
|
|
149
|
+
"desktop.rename": "重命名",
|
|
150
|
+
"desktop.renameWorkspace": "重命名工作区",
|
|
151
|
+
"desktop.renameSession": "重命名会话",
|
|
152
|
+
"desktop.deleteWorkspace": "删除工作区",
|
|
153
|
+
"desktop.forkSession": "复刻会话",
|
|
154
|
+
"desktop.archiveSession": "归档会话",
|
|
155
|
+
"desktop.moveSession": "移动到工作区",
|
|
156
|
+
"desktop.showMoreSessions": "展开其余 {n} 个会话",
|
|
157
|
+
"desktop.showLessSessions": "收起",
|
|
158
|
+
"desktop.sessionActions": "会话操作",
|
|
159
|
+
"desktop.workspaceActions": "工作区操作",
|
|
160
|
+
"desktop.save": "保存",
|
|
161
|
+
"desktop.deleteWorkspaceConfirm": "从列表移除“{name}”?文件和会话记录会保留。",
|
|
162
|
+
"desktop.archiveSessionConfirm": "归档这个会话?",
|
|
163
|
+
"desktop.minutesAgo": "{n}分钟",
|
|
164
|
+
"desktop.hoursAgo": "{n}小时",
|
|
165
|
+
"desktop.daysAgo": "{n}天",
|
|
52
166
|
"agent.refresh": "刷新",
|
|
53
167
|
"agent.loading": "正在读取配置…",
|
|
54
168
|
"question.title": "需要你选一项",
|
|
@@ -70,7 +184,55 @@ export const EN = {
|
|
|
70
184
|
"probe.entrance": "Entrance {origin}",
|
|
71
185
|
"chat.empty": "Send a message to start chatting",
|
|
72
186
|
"chat.placeholder": "Message or hold to speak",
|
|
187
|
+
"chat.placeholderDesktop": "Send a message to the agent",
|
|
188
|
+
"chat.placeholderNewSession": "Describe what you want to build",
|
|
189
|
+
"chat.placeholderPlan": "Describe the task to plan",
|
|
190
|
+
"chat.placeholderNoModel": "Choose an available model",
|
|
191
|
+
"chat.placeholderSteerQueue": "Press {shortcut} to send all queued messages now",
|
|
73
192
|
"chat.send": "Send",
|
|
193
|
+
"chat.stop": "Stop",
|
|
194
|
+
"queue.count": "{n} queued messages",
|
|
195
|
+
"queue.edit": "Edit",
|
|
196
|
+
"queue.save": "Save",
|
|
197
|
+
"queue.cancelEdit": "Cancel edit",
|
|
198
|
+
"queue.remove": "Remove",
|
|
199
|
+
"queue.steer": "Send now",
|
|
200
|
+
"access.confirm.title": "Enable Full access?",
|
|
201
|
+
"access.confirm.description": "Full access reduces confirmation steps and lets the agent perform more actions directly, including sensitive operations, file changes, or external commands. Only use it when you trust the current task.",
|
|
202
|
+
"access.confirm.acknowledge": "I understand the risks and want to continue",
|
|
203
|
+
"access.confirm.cancel": "Cancel",
|
|
204
|
+
"access.confirm.enable": "Enable Full access",
|
|
205
|
+
"plan.active": "Plan",
|
|
206
|
+
"plan.disable": "Exit plan mode",
|
|
207
|
+
"context.title": "Context usage",
|
|
208
|
+
"context.used": "{percent}% · ~{used} / {total} tokens",
|
|
209
|
+
"context.system": "System prompt",
|
|
210
|
+
"context.tools": "Tool definitions",
|
|
211
|
+
"context.conversation": "Conversation",
|
|
212
|
+
"todo.progress": "Tasks {done}/{total}",
|
|
213
|
+
"goal.edit": "Edit goal",
|
|
214
|
+
"goal.pause": "Pause goal",
|
|
215
|
+
"goal.resume": "Resume goal",
|
|
216
|
+
"goal.clear": "Clear goal",
|
|
217
|
+
"approval.waiting": "Waiting for approval",
|
|
218
|
+
"approval.escalation": "{toolName} requests permission to run",
|
|
219
|
+
"approval.reject": "Reject",
|
|
220
|
+
"approval.allowOnce": "Allow once",
|
|
221
|
+
"question.waiting": "Your input is needed",
|
|
222
|
+
"question.other": "Other",
|
|
223
|
+
"question.back": "Back",
|
|
224
|
+
"question.next": "Next",
|
|
225
|
+
"question.submit": "Submit",
|
|
226
|
+
"settings.input.title": "Input",
|
|
227
|
+
"settings.input.busyEnter.hint": "While the agent is running, plain Enter uses this mode; Cmd/Ctrl+Enter uses the opposite.",
|
|
228
|
+
"settings.input.busyEnter.queue": "Enter queues",
|
|
229
|
+
"settings.input.busyEnter.steer": "Enter steers",
|
|
230
|
+
"references.empty": "No matching files or sessions",
|
|
231
|
+
"references.section.files": "Files & folders",
|
|
232
|
+
"references.section.sessions": "Session conversations",
|
|
233
|
+
"references.file": "File",
|
|
234
|
+
"references.directory": "Folder",
|
|
235
|
+
"references.session": "Session",
|
|
74
236
|
"chat.sending": "Replying…",
|
|
75
237
|
"action.copy": "Copy",
|
|
76
238
|
"action.copied": "Copied",
|
|
@@ -79,6 +241,8 @@ export const EN = {
|
|
|
79
241
|
"action.share": "Share",
|
|
80
242
|
"action.bar": "Reply actions",
|
|
81
243
|
"composer.attach": "Add file",
|
|
244
|
+
"composer.removeAttachment": "Remove {name}",
|
|
245
|
+
"composer.previewAttachment": "View {name}",
|
|
82
246
|
"composer.voice": "Voice input",
|
|
83
247
|
"composer.think": "Deep think",
|
|
84
248
|
"composer.search": "Search",
|
|
@@ -96,6 +260,13 @@ export const EN = {
|
|
|
96
260
|
"message.retry.status": "{label} ({retry}/{maximum}) · {seconds}s",
|
|
97
261
|
"message.retry.delay": "Retry delay: ",
|
|
98
262
|
"message.retry.failure": "Failure reason: ",
|
|
263
|
+
"message.ranFor": "Ran for {duration}",
|
|
264
|
+
"message.ttft": "TTFT {seconds}s",
|
|
265
|
+
"message.tokensPerSecond": "{tps} tok/s",
|
|
266
|
+
"duration.seconds": "{seconds}s",
|
|
267
|
+
"duration.minutes": "{minutes}m {seconds}s",
|
|
268
|
+
"clock.md": "{m}/{d}",
|
|
269
|
+
"clock.ymd": "{y}/{m}/{d}",
|
|
99
270
|
"bar.history": "History",
|
|
100
271
|
"bar.menu": "Menu",
|
|
101
272
|
"bar.new": "New chat",
|
|
@@ -103,10 +274,65 @@ export const EN = {
|
|
|
103
274
|
"history.empty": "No other chats yet",
|
|
104
275
|
"history.untitled": "New chat",
|
|
105
276
|
"history.loading": "Loading chats…",
|
|
277
|
+
"history.noMoveTargets": "No other workspaces to move to",
|
|
106
278
|
"chat.loading": "Loading conversation…",
|
|
107
279
|
"history.today": "Today",
|
|
108
280
|
"history.week": "Previous 7 days",
|
|
109
281
|
"history.older": "Older",
|
|
282
|
+
"desktop.newSession": "New Session",
|
|
283
|
+
"desktop.workspaces": "Workspaces",
|
|
284
|
+
"desktop.sessions": "Sessions",
|
|
285
|
+
"desktop.ungrouped": "Ungrouped",
|
|
286
|
+
"desktop.searchSessions": "Search sessions",
|
|
287
|
+
"desktop.clearSearch": "Clear search",
|
|
288
|
+
"desktop.viewOptions": "View options",
|
|
289
|
+
"desktop.addWorkspace": "Add workspace",
|
|
290
|
+
"desktop.addingWorkspace": "Adding…",
|
|
291
|
+
"desktop.open": "Open",
|
|
292
|
+
"desktop.cancel": "Cancel",
|
|
293
|
+
"desktop.close": "Close",
|
|
294
|
+
"desktop.groupBy": "Group by",
|
|
295
|
+
"desktop.byWorkspace": "By workspace",
|
|
296
|
+
"desktop.oneList": "In one list",
|
|
297
|
+
"desktop.orderBy": "Order by",
|
|
298
|
+
"desktop.recent": "Last updated",
|
|
299
|
+
"desktop.manual": "Manual",
|
|
300
|
+
"desktop.commands": "Commands",
|
|
301
|
+
"desktop.noCommands": "No commands for this session",
|
|
302
|
+
"permission.read-only": "Read Only",
|
|
303
|
+
"permission.read-only.hint": "Read files only; writes and risky commands require approval.",
|
|
304
|
+
"permission.workspace-write": "Workspace Write",
|
|
305
|
+
"permission.workspace-write.hint": "Write inside the workspace; anything wider requires approval.",
|
|
306
|
+
"permission.danger-full-access": "Full access",
|
|
307
|
+
"permission.danger-full-access.hint": "No file sandbox and no approval prompts.",
|
|
308
|
+
"desktop.selectWorkspaceDirectory": "Select Workspace Directory",
|
|
309
|
+
"desktop.chooseFolder": "Choose an Olares Host folder",
|
|
310
|
+
"desktop.newFolder": "New folder",
|
|
311
|
+
"desktop.newFolderIn": "New folder in \"{name}\"",
|
|
312
|
+
"desktop.untitledFolder": "Untitled folder",
|
|
313
|
+
"desktop.createFolder": "Create",
|
|
314
|
+
"desktop.showHiddenFiles": "Show hidden files",
|
|
315
|
+
"desktop.folderPath": "Folder path",
|
|
316
|
+
"desktop.home": "Home",
|
|
317
|
+
"desktop.loadingFolders": "Loading folders…",
|
|
318
|
+
"desktop.noFolders": "No subfolders here",
|
|
319
|
+
"desktop.rename": "Rename",
|
|
320
|
+
"desktop.renameWorkspace": "Rename workspace",
|
|
321
|
+
"desktop.renameSession": "Rename session",
|
|
322
|
+
"desktop.deleteWorkspace": "Delete workspace",
|
|
323
|
+
"desktop.forkSession": "Fork session",
|
|
324
|
+
"desktop.archiveSession": "Archive session",
|
|
325
|
+
"desktop.moveSession": "Move to workspace",
|
|
326
|
+
"desktop.showMoreSessions": "Show {n} more sessions",
|
|
327
|
+
"desktop.showLessSessions": "Show less",
|
|
328
|
+
"desktop.sessionActions": "Session actions",
|
|
329
|
+
"desktop.workspaceActions": "Workspace actions",
|
|
330
|
+
"desktop.save": "Save",
|
|
331
|
+
"desktop.deleteWorkspaceConfirm": "Remove “{name}”? Files and session logs will be kept.",
|
|
332
|
+
"desktop.archiveSessionConfirm": "Archive this session?",
|
|
333
|
+
"desktop.minutesAgo": "{n}min",
|
|
334
|
+
"desktop.hoursAgo": "{n}h",
|
|
335
|
+
"desktop.daysAgo": "{n}d",
|
|
110
336
|
"agent.refresh": "Refresh",
|
|
111
337
|
"agent.loading": "Loading settings…",
|
|
112
338
|
"question.title": "Choose an option",
|
package/i18n/preview.js
CHANGED
|
@@ -2,6 +2,7 @@ export const ZH = {
|
|
|
2
2
|
hierarchy: "会话层级",
|
|
3
3
|
tabs: "工作区标签页",
|
|
4
4
|
chat: "对话",
|
|
5
|
+
back: "返回",
|
|
5
6
|
preview: "文件预览",
|
|
6
7
|
produced: "产物",
|
|
7
8
|
mediaLoading: "正在准备媒体预览…",
|
|
@@ -32,6 +33,7 @@ export const EN = {
|
|
|
32
33
|
hierarchy: "Session hierarchy",
|
|
33
34
|
tabs: "Workspace tabs",
|
|
34
35
|
chat: "Chat",
|
|
36
|
+
back: "Back",
|
|
35
37
|
preview: "File preview",
|
|
36
38
|
produced: "Produced",
|
|
37
39
|
mediaLoading: "Preparing media preview…",
|
package/larepass/chat.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isAuthFailure } from "./host.js";
|
|
2
2
|
import { consumeMux, MUX_PATH } from "./mux.js";
|
|
3
|
+
import { permissionSelect } from "./permissions.js";
|
|
3
4
|
import { foldTranscript, mergeEvents } from "./transcript.js";
|
|
4
5
|
import { promptPayload, rpcPath, unwrapServerResponse, wrapClientRequest } from "./rpc.js";
|
|
5
6
|
|
|
@@ -31,18 +32,18 @@ export async function callRpc(request, method, payload = {}) {
|
|
|
31
32
|
return { ...parsed, status: "ok", http };
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
export async function
|
|
35
|
+
export async function listNavigableSessions(rpc) {
|
|
35
36
|
const listed = await rpc("session.list", {});
|
|
36
37
|
if (!listed.ok) return listed;
|
|
37
38
|
return {
|
|
38
39
|
ok: true,
|
|
39
40
|
status: listed.status || "ok",
|
|
40
|
-
value: { items:
|
|
41
|
+
value: { items: navigableSessions(listed.value?.items).map(summarizeSession) },
|
|
41
42
|
};
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
export async function ensureSession(rpc) {
|
|
45
|
-
const listed = await
|
|
46
|
+
const listed = await listNavigableSessions(rpc);
|
|
46
47
|
if (!listed.ok) return listed;
|
|
47
48
|
const existing = listed.value.items[0];
|
|
48
49
|
if (existing) {
|
|
@@ -54,9 +55,13 @@ export async function ensureSession(rpc) {
|
|
|
54
55
|
return { ok: true, status: "ok", value: { sessionId: row.sessionId, sessions: [row] } };
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Sessions a person can open. `parentSessionId` is lineage, not a hide signal:
|
|
60
|
+
* forked children carry it too, and only subagent runs are off-list.
|
|
61
|
+
*/
|
|
62
|
+
export function navigableSessions(items) {
|
|
58
63
|
return (Array.isArray(items) ? items : []).filter(
|
|
59
|
-
(row) => row?.sessionId &&
|
|
64
|
+
(row) => row?.sessionId && row.origin !== "subagent",
|
|
60
65
|
);
|
|
61
66
|
}
|
|
62
67
|
|
|
@@ -75,11 +80,22 @@ export function sessionTitleOf(row) {
|
|
|
75
80
|
}
|
|
76
81
|
|
|
77
82
|
export function summarizeSession(row) {
|
|
83
|
+
const permissions = permissionSelect(row);
|
|
84
|
+
const projections = row?.projections?.values ?? {};
|
|
78
85
|
return {
|
|
79
86
|
sessionId: row.sessionId,
|
|
80
87
|
title: sessionTitleOf(row),
|
|
81
88
|
updatedAt: Number(row.updatedAt || row.mtime || row.createdAt || 0) || 0,
|
|
82
89
|
blank: Boolean(row.blank),
|
|
90
|
+
...(typeof row.cwd === "string" ? { cwd: row.cwd } : {}),
|
|
91
|
+
// The composer's permission chip reads its preset from here, so the one
|
|
92
|
+
// projection this summary keeps is the one a session row can answer for.
|
|
93
|
+
...(permissions ? { permissions } : {}),
|
|
94
|
+
...(projections.plan ? { plan: projections.plan } : {}),
|
|
95
|
+
...(projections.contextPressure ? { contextPressure: projections.contextPressure } : {}),
|
|
96
|
+
...(projections.contextBreakdown ? { contextBreakdown: projections.contextBreakdown } : {}),
|
|
97
|
+
...(projections.goal ? { goal: projections.goal } : {}),
|
|
98
|
+
...(Array.isArray(projections.todos) ? { todos: projections.todos } : {}),
|
|
83
99
|
};
|
|
84
100
|
}
|
|
85
101
|
|
|
@@ -127,8 +143,8 @@ export function clientTimeZone() {
|
|
|
127
143
|
}
|
|
128
144
|
}
|
|
129
145
|
|
|
130
|
-
export function sendPrompt(rpc, sessionId,
|
|
131
|
-
return rpc("session.prompt", promptPayload(sessionId,
|
|
146
|
+
export function sendPrompt(rpc, sessionId, textOrContent, mode = "queue") {
|
|
147
|
+
return rpc("session.prompt", promptPayload(sessionId, textOrContent, clientTimeZone(), mode));
|
|
132
148
|
}
|
|
133
149
|
|
|
134
150
|
export { consumeMux, mergeEvents, MUX_PATH };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The host's slash commands, for a composer that offers them instead of making
|
|
3
|
+
* people remember the names.
|
|
4
|
+
*
|
|
5
|
+
* dsh keeps its command registry behind a Typert remote, which this client's
|
|
6
|
+
* plain `POST /api/<method>` transport cannot reach, so the Lares Host exposes
|
|
7
|
+
* the registry over `/api/lares/commands` instead. Running one stays ordinary:
|
|
8
|
+
* a prompt whose single text block starts with `/` is dispatched by the host to
|
|
9
|
+
* the registry and never reaches the model.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const COMMANDS_PATH = "/api/lares/commands";
|
|
13
|
+
|
|
14
|
+
export function commandsUrl(sessionId) {
|
|
15
|
+
return sessionId ? `${COMMANDS_PATH}?sessionId=${encodeURIComponent(sessionId)}` : COMMANDS_PATH;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {unknown} body - the endpoint's payload.
|
|
20
|
+
* @returns {Array<{ name: string, description: string, hint: string }>}
|
|
21
|
+
*/
|
|
22
|
+
export function normalizeCommands(body) {
|
|
23
|
+
const rows = Array.isArray(body?.commands) ? body.commands : [];
|
|
24
|
+
return rows
|
|
25
|
+
.filter((row) => typeof row?.name === "string" && row.name.trim())
|
|
26
|
+
.map((row) => ({
|
|
27
|
+
name: row.name.trim(),
|
|
28
|
+
description: String(row.description || ""),
|
|
29
|
+
hint: String(row.input?.hint ?? row.hint ?? ""),
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A command that takes input is handed over with the cursor past the space. */
|
|
34
|
+
export function commandLine(command) {
|
|
35
|
+
return command?.hint ? `/${command.name} ` : `/${command?.name ?? ""}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Put a chosen command on the composer. The host reads a command only as the
|
|
40
|
+
* whole message, so the line replaces any command already typed there while
|
|
41
|
+
* keeping the words after it — picking `goal` over `plan` keeps the input the
|
|
42
|
+
* person had already written for it.
|
|
43
|
+
*/
|
|
44
|
+
export function applyCommand(draft, command) {
|
|
45
|
+
const line = commandLine(command);
|
|
46
|
+
const rest = String(draft ?? "").replace(/^\s*\/\S*\s*/, "").trimStart();
|
|
47
|
+
if (!rest) return line;
|
|
48
|
+
return line.endsWith(" ") ? `${line}${rest}` : `${line} ${rest}`;
|
|
49
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const BREAKDOWN = [
|
|
2
|
+
["system", "systemTokens"],
|
|
3
|
+
["tools", "toolsTokens"],
|
|
4
|
+
["conversation", "messageTokens"],
|
|
5
|
+
];
|
|
6
|
+
|
|
7
|
+
export function compactTokens(value) {
|
|
8
|
+
const n = Number(value) || 0;
|
|
9
|
+
if (n >= 1_000_000) return `${Math.round(n / 100_000) / 10}M`;
|
|
10
|
+
if (n >= 1_000) return `${Math.round(n / 100) / 10}K`;
|
|
11
|
+
return String(Math.round(n));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function occupancyFromPressure(pressure) {
|
|
15
|
+
const used = Number(pressure?.projectedTokens ?? pressure?.pressureTokens);
|
|
16
|
+
const total = Number(pressure?.contextWindow);
|
|
17
|
+
if (!(used >= 0) || !(total > 0)) return null;
|
|
18
|
+
return {
|
|
19
|
+
percent: Math.min(100, Math.max(0, Math.round((used / total) * 100))),
|
|
20
|
+
used: compactTokens(used),
|
|
21
|
+
total: compactTokens(total),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function occupancyBreakdown(breakdown) {
|
|
26
|
+
return BREAKDOWN.map(([key, field]) => ({
|
|
27
|
+
key,
|
|
28
|
+
compact: compactTokens(Number(breakdown?.[field]) || 0),
|
|
29
|
+
}));
|
|
30
|
+
}
|