diff options
| -rw-r--r-- | desktop/assistant/Assistant.qml | 19 | ||||
| -rw-r--r-- | desktop/assistant/Card.qml | 13 | ||||
| -rw-r--r-- | desktop/assistant/Files.js | 9 | ||||
| -rw-r--r-- | desktop/assistant/Picker.qml | 9 | ||||
| -rw-r--r-- | desktop/assistant/test_files.js | 2 | ||||
| -rw-r--r-- | desktop/island/Island.qml | 3 |
6 files changed, 41 insertions, 14 deletions
diff --git a/desktop/assistant/Assistant.qml b/desktop/assistant/Assistant.qml index b038dd6..977b4dd 100644 --- a/desktop/assistant/Assistant.qml +++ b/desktop/assistant/Assistant.qml @@ -78,7 +78,9 @@ Scope { // A conversation idle for 10 minutes is over: opening starts a new one, // the old one staying in History. ponytail: a constant, not a setting. function show() { - if (turns.count && Date.now() - root.lastActive > 10 * 60 * 1000) root.fresh(); + // Never while a reply runs: new would stop it, and a stopped turn is + // not saved. + if (turns.count && !root.busy && Date.now() - root.lastActive > 10 * 60 * 1000) root.fresh(); root.open = true; } @@ -120,15 +122,20 @@ Scope { link.item.flush(); } + // The chips stay until the daemon's user event takes them: a refused + // file (unreadable, too big, a model without vision) comes back as an + // error, and the chips must still be there to fix it. chatting keeps the + // chat layout meanwhile, so the card does not shrink while a PDF is read. function ask(text) { if (!root.connected || root.blocked || (text.trim() === "" && !root.staged.length)) return false; root.notice = ""; + root.chatting = true; root.send({ type: "ask", text: text, files: root.staged.map(f => f.path) }); - root.staged = []; return true; } function showHistory() { + root.pickerOpen = false; root.historyOpen = true; root.send({ type: "list" }); } @@ -172,15 +179,21 @@ Scope { root.ctxUsed = e.ctx_used; root.ctxMax = e.ctx_max; root.load(e.id, e.turns, e.pending); - if (e.pending) root.lastActive = Date.now(); + // A restart or reload counts as active: the conversation it + // reconnects to is not stale just because this process is new. + if (e.pending || e.turns.length) root.lastActive = Date.now(); break; case "conversation": root.load(e.id, e.turns, null); // Opened from History on purpose: it counts as active now. if (e.turns.length) root.lastActive = Date.now(); + // Emptied (New, or the current one deleted) with nothing staged: + // back to the entry line. + else if (!root.staged.length) root.chatting = false; break; case "user": turns.append(root.row({ user: e.text, files: e.files }, true)); + root.staged = []; root.lastActive = Date.now(); break; case "think": root.bufThink += e.delta; break; diff --git a/desktop/assistant/Card.qml b/desktop/assistant/Card.qml index 477f430..228cfdb 100644 --- a/desktop/assistant/Card.qml +++ b/desktop/assistant/Card.qml @@ -244,7 +244,10 @@ Rectangle { height: box.height text: "" font.family: Theme.iconFamily - onClicked: card.canvas.pickerOpen = !card.canvas.pickerOpen + onClicked: { + card.canvas.historyOpen = false; + card.canvas.pickerOpen = !card.canvas.pickerOpen; + } } Rectangle { @@ -298,7 +301,8 @@ Rectangle { anchors { left: parent.left; right: parent.right; top: parent.top; margins: 8 } visible: input.text === "" elide: Text.ElideRight - text: card.mode === "entry" ? "How can I help you?" + text: !card.canvas.connected ? "assistant not running" + : card.mode === "entry" ? "How can I help you?" : card.canvas.staged.length ? `Ask about ${card.canvas.staged[0].name}…` : "Message the assistant" color: Theme.overlay @@ -332,12 +336,13 @@ Rectangle { } } - // --- panels, over everything above the input --- + // --- panels, over the transcript (under the header in chat) --- Item { id: panelArea anchors { - top: parent.top; topMargin: 14; bottom: chips.visible ? chips.top : footer.top + top: header.visible ? header.bottom : parent.top; topMargin: header.visible ? 8 : 14 + bottom: chips.visible ? chips.top : footer.top bottomMargin: 8; left: parent.left; right: parent.right; leftMargin: 14; rightMargin: 14 } diff --git a/desktop/assistant/Files.js b/desktop/assistant/Files.js index b7f8c2b..bcfe1f0 100644 --- a/desktop/assistant/Files.js +++ b/desktop/assistant/Files.js @@ -45,11 +45,12 @@ function kind(path) { function chats(k) { return CHAT.includes(k); } -// A dropped URL to a local path, or "" for anything that is not a file. +// A dropped URL to a local path, or "" for anything that is not a local +// file: file:///path, or file://localhost/path; any other host is remote. function fromUrl(url) { - const s = String(url); - if (!s.startsWith("file://")) return ""; - try { return decodeURIComponent(s.slice(7)); } catch (e) { return ""; } + const m = /^file:\/\/(localhost)?(\/.*)$/.exec(String(url)); + if (!m) return ""; + try { return decodeURIComponent(m[2]); } catch (e) { return ""; } } function file(path) { return { path: path, name: name(path), kind: kind(path) }; } diff --git a/desktop/assistant/Picker.qml b/desktop/assistant/Picker.qml index 1465d1b..6d7b8ad 100644 --- a/desktop/assistant/Picker.qml +++ b/desktop/assistant/Picker.qml @@ -49,13 +49,18 @@ Rectangle { FolderListModel { id: files - folder: "file://" + picker.dir + // Encoded: a raw "#" or "?" in a folder name would end the path. + folder: "file://" + picker.dir.split("/").map(encodeURIComponent).join("/") showDirsFirst: true + // Name filters never apply to folders, so while filtering they would + // sit first and catch Enter. ponytail: no filtering to a folder. + showDirs: field.text === "" showDotAndDotDot: false // Hidden entries only on asking for them. showHidden: field.text.startsWith(".") caseSensitive: false - nameFilters: field.text ? ["*" + field.text + "*"] : ["*"] + // Glob characters are taken out: typed, they are meant literally. + nameFilters: field.text ? ["*" + field.text.replace(/[*?\[\]]/g, "") + "*"] : ["*"] sortCaseSensitive: false } diff --git a/desktop/assistant/test_files.js b/desktop/assistant/test_files.js index a15495b..5ece64f 100644 --- a/desktop/assistant/test_files.js +++ b/desktop/assistant/test_files.js @@ -34,6 +34,8 @@ assert.ok(!F.chats("video") && !F.chats("archive") && !F.chats("other")); assert.strictEqual(F.fromUrl("file:///home/you/My%20Docs/caf%C3%A9.pdf"), "/home/you/My Docs/café.pdf"); assert.strictEqual(F.fromUrl("file:///tmp/a.txt"), "/tmp/a.txt"); assert.strictEqual(F.fromUrl("https://example.org/a.pdf"), ""); +assert.strictEqual(F.fromUrl("file://localhost/tmp/a.pdf"), "/tmp/a.pdf"); +assert.strictEqual(F.fromUrl("file://otherhost/tmp/a.pdf"), ""); assert.strictEqual(F.fromUrl("file:///bad%E0%A4%A"), ""); // malformed escape assert.deepStrictEqual(F.file("/x/y/a b.pdf"), { path: "/x/y/a b.pdf", name: "a b.pdf", kind: "pdf" }); diff --git a/desktop/island/Island.qml b/desktop/island/Island.qml index 4fba033..412cc58 100644 --- a/desktop/island/Island.qml +++ b/desktop/island/Island.qml @@ -203,7 +203,8 @@ PanelWindow { x: win.cx - width / 2 y: 0 z: 10 - width: 260 + // The sliver's hit width at rest, so it takes no clicks beside it. + width: win.dropHold ? 260 : 220 height: win.dropHold ? 100 : 10 DropArea { |
