One approval for app and agent; /api/v2 files and show; app_api 2
- The app's device login asks for `app models` and, where discovery says
login.device.agent_scope, `agent_scope=models memory link` in the same
approval. The agent's token goes to the bundled CLI on stdin
(`lembas login --token-stdin --server <url> --json`) and is never kept
here; if the CLI does not take it, it is given up on the server. A
computer linked already keeps its link; a server without agent_scope
gets the old two steps.
- The server pane reads GET /api/v2/files and /files/content (mounts open
like folders; `withheld` shown in the server's words). The "not offered
yet" state is gone.
- A show card's file comes from GET /api/v2/chats/{chat}/show?node=&version=
or ?shown= as plain text (new `api_text` command, 1 MB cap); args.path is
read only for agent chats on this computer, locally.
- The gate is link protocol 3 plus app_api 2 (APP_API = 2), not the
server's version number, which says 1.7.2 on dev until its first beta.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
9 files changed
+383
-164
No files matched your search
+24
-22
@@ -6,16 +6,18 @@ import { $, h, clear, icon, ICONS, bytes } from "./dom.js"
|
||||
import { invoke, get, why } from "./bridge.js"
|
||||
import { openViewer } from "./viewer.js"
|
||||
|
||||
// The server's tree as the app's door gives it. `/api/v2` has no file routes yet (the web UI's
|
||||
// /files pages take the session cookie and answer HTML); these are the routes this pane is written
|
||||
// against, and a 404 says "not on this server yet" instead of failing:
|
||||
// GET /files?path=/a -> { path, entries: [{ name, path, kind: "dir"|"file", size, mime }] }
|
||||
// GET /files/content?path= -> { path, name, size, text | null, withheld?: "binary"|"too_big" }
|
||||
// The server's tree as the app's door gives it, read as the person (their own access, no chat):
|
||||
// GET /files?path=/a -> { path, parent, entries: [{ name, path, kind: "dir"|"file"|"mount", size, mime, updated_at }] }
|
||||
// GET /files/content?path= -> { path, name, size, mime, kind, language, sha, text, withheld?: { reason, message } }
|
||||
// A path the person cannot read is the server's 404, in its words.
|
||||
export const SERVER_FILES = {
|
||||
list: (path) => `/files?path=${encodeURIComponent(path || "/")}`,
|
||||
read: (path) => `/files/content?path=${encodeURIComponent(path)}`,
|
||||
}
|
||||
|
||||
/** A directory in either tree: the server's mounts open like its folders. */
|
||||
const isDir = (e) => e.kind === "dir" || e.kind === "mount"
|
||||
|
||||
/** A lazily opened tree in `box`. `list(path)` -> entries; `open(entry)` for a file. */
|
||||
function tree(box, { list, open, rootPath }) {
|
||||
clear(box)
|
||||
@@ -33,15 +35,15 @@ function tree(box, { list, open, rootPath }) {
|
||||
for (const e of entries) {
|
||||
const row = h(
|
||||
"button",
|
||||
{ type: "button", class: "tree__row", title: e.path, "aria-expanded": e.kind === "dir" ? "false" : null },
|
||||
h("span", { class: "tree__chevron" }, e.kind === "dir" ? icon(ICONS.chevron) : null),
|
||||
icon(e.kind === "dir" ? ICONS.folder : ICONS.file),
|
||||
{ type: "button", class: "tree__row", title: e.path, "aria-expanded": isDir(e) ? "false" : null },
|
||||
h("span", { class: "tree__chevron" }, isDir(e) ? icon(ICONS.chevron) : null),
|
||||
icon(isDir(e) ? ICONS.folder : ICONS.file),
|
||||
h("span", { class: "tree__name", text: e.name }),
|
||||
h("span", { class: "tree__size", text: e.kind === "dir" ? "" : bytes(e.size) }),
|
||||
h("span", { class: "tree__size", text: isDir(e) ? "" : bytes(e.size) }),
|
||||
)
|
||||
row.style.setProperty("--depth", String(depth))
|
||||
const li = h("li", { role: "treeitem" }, row)
|
||||
if (e.kind === "dir") {
|
||||
if (isDir(e)) {
|
||||
let kids = null
|
||||
row.addEventListener("click", async () => {
|
||||
const opening = row.getAttribute("aria-expanded") !== "true"
|
||||
@@ -127,22 +129,22 @@ export function renderServer() {
|
||||
const box = $("server-tree")
|
||||
tree(box, {
|
||||
rootPath: "/",
|
||||
list: async (path) => {
|
||||
try {
|
||||
const r = await get(SERVER_FILES.list(path))
|
||||
return r.entries || []
|
||||
} catch (err) {
|
||||
if (err && (err.status === 404 || err.status === 405)) {
|
||||
throw { detail: t("This server does not offer its files to the app yet.") }
|
||||
}
|
||||
throw err
|
||||
}
|
||||
},
|
||||
list: async (path) => (await get(SERVER_FILES.list(path))).entries || [],
|
||||
open: async (e) => {
|
||||
const where = t("Server")
|
||||
try {
|
||||
const f = await readServer(e.path)
|
||||
openViewer({ name: f.name || e.name, path: f.path || e.path, where, text: f.text, withheld: f.withheld, size: f.size })
|
||||
openViewer({
|
||||
name: f.name || e.name,
|
||||
path: f.path || e.path,
|
||||
where,
|
||||
text: f.text,
|
||||
// The server's own kind and language: its table and the viewer's are the same one.
|
||||
kind: ["markdown", "code", "text", "html"].includes(f.kind) ? f.kind : undefined,
|
||||
language: f.language || undefined,
|
||||
withheld: f.withheld,
|
||||
size: f.size,
|
||||
})
|
||||
} catch (err) {
|
||||
openViewer({ name: e.name, path: e.path, where, error: why(err) })
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user