Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
125 commits
Select commit Hold shift + click to select a range
4d91bb7
Keep the keyboard cursor row scrolled into view; arrows in the window
Fileri Sep 5, 2026
15e56df
Stop the keyboard cursor at the list ends instead of wrapping
Fileri Sep 5, 2026
419d3e5
Up from the first row focuses the search field
Fileri Sep 5, 2026
78e042c
ui.test.ts: pin the keyboard-cursor invariants
Fileri Sep 5, 2026
0e2b37b
Expose `opened` on the bar widget so Omarchy's panel hotkeys find Blip
Fileri Sep 5, 2026
328ba4a
Tooltips stop naming keys the plugin does not bind
Fileri Sep 5, 2026
6187d19
showApp() closes the popout, whichever way the app is opened
Fileri Sep 5, 2026
5bbbcef
Read history from the keyboard: a bubble cursor, paging, Home/End
Fileri Sep 5, 2026
221f9f7
Enter, Ctrl+C and Ctrl+R act on the selected bubble
Fileri Sep 5, 2026
c6ee163
Ctrl+C on a picture bubble copies the picture
Fileri Sep 5, 2026
7e2b625
The status line keeps its height, and only failures are red
Fileri Sep 5, 2026
3e51720
Walk the bubbles with a draft in the field: edge rule, PgUp/PgDn, Shift
Fileri Sep 5, 2026
617c7e8
The thread cursor hides while the search field has focus
Fileri Sep 5, 2026
1f9aa4b
chats: follow Messages for re-keyed group pins and merged DMs
Sep 6, 2026
b6f152d
Keep an unsent draft per conversation across thread switches
Fileri Sep 6, 2026
21b6499
Size pinned avatars through Layout hints so they do not grow on the f…
Fileri Sep 6, 2026
aac54f7
Enter on a selected link opens the share sheet; the sheet takes the k…
Fileri Sep 6, 2026
ddbae47
contacts: one card saved twice is one person; filtered SMS shows its …
zachwilke Sep 6, 2026
8bb1ff6
send: the bubble appears as Enter is pressed, not three seconds later
zachwilke Sep 6, 2026
de715bb
Merge #42: size pinned avatars through Layout hints
nixfred Sep 6, 2026
6a089f2
Merge #40: pins and merged DMs follow the whole conversation cluster
nixfred Sep 6, 2026
31599ec
Merge #41: keep an unsent draft per conversation
nixfred Sep 6, 2026
11c915c
Merge #37: the keyboard cursor scrolls, stops at the ends, reaches th…
nixfred Sep 6, 2026
7feb4c0
Merge #43: duplicate Contacts cards resolve; sending is instant
nixfred Sep 6, 2026
733930e
Merge #38: expose `opened` so Omarchy's panel hotkeys reach Blip
nixfred Sep 6, 2026
68b87b3
Merge #39: read the conversation from the keyboard
nixfred Sep 7, 2026
7d569c3
Remove undefined/st.json, a test artifact committed at 2.2.0
nixfred Sep 7, 2026
4496c5e
Two bugs found by driving the live panel, not by reading it
nixfred Sep 7, 2026
ee4708f
docs: regenerate the screenshots against the current UI
nixfred Sep 7, 2026
00a1463
fix: IPC `window` could never report a window it had just shown
nixfred Sep 7, 2026
ba1d8f9
fix: a long draft scrolls to the caret instead of typing off the bottom
nixfred Sep 7, 2026
a57f6b3
feat: read-only contact review — cross-source duplicate scan, fingerp…
jondkinney Sep 4, 2026
ee0e33a
test: isolate clipboard fixtures from the user cache
jondkinney Sep 5, 2026
295fbf3
refactor: make contact review a conversation tool
jondkinney Sep 5, 2026
99e38c0
fix: restore the app on its last workspace without taking focus
Sep 8, 2026
4e1b6a7
fix: fit contact review to the menubar panel
jondkinney Sep 8, 2026
8b53d94
fix: reading a conversation can reach the phone, and says so in status
nixfred Sep 8, 2026
b616154
Window: the sidebar cursor previews a thread without marking it read
Fileri Sep 5, 2026
a309afe
blip-setup pins the dedicated key to the enrolling machine over Tails…
Fileri Sep 7, 2026
993712a
Window: Right steps into the compose field, Left from the text's star…
Fileri Sep 5, 2026
89bc0bb
Window: leaving the list for the search or new-message field ends a p…
Fileri Sep 6, 2026
6144052
Window: the sidebar keeps one spacing whatever the pane shows
Fileri Sep 6, 2026
6ce78d5
Blip 2.4.0 — read it from the keyboard, send without the wait
nixfred Sep 8, 2026
daf8b77
docs: settle the BlueFerry question, and record why the experiment st…
nixfred Sep 9, 2026
c0dc916
Hide Spam and unknown-sender chats via bridge.conf
joshuaswarren Sep 9, 2026
7e5c7ea
Do not restore hidden Spam onto a capped unread ledger
joshuaswarren Sep 9, 2026
af1d160
fix(#36): the optional read-push probe no longer asks for consent uni…
nixfred Sep 10, 2026
f782bf0
docs(#44): explain the fork-PR approval gate instead of letting it re…
nixfred Sep 10, 2026
dc64ae4
Merge #46: blip-setup pins the dedicated key to the enrolling machine
nixfred Sep 10, 2026
fc782b9
Merge #48: restore the app window on its last workspace without takin…
nixfred Sep 10, 2026
33f72f9
Merge #45: the window's sidebar previews the thread under the cursor
nixfred Sep 10, 2026
7903e26
Merge #49: hide Spam and unknown senders through bridge.conf
nixfred Sep 10, 2026
174b58e
Merge #24: read-only contact review, reachable from a conversation
nixfred Sep 10, 2026
d32c036
Make the composer accessible with spelling and native navigation
jondkinney Sep 10, 2026
b17265b
Add pinned shortcuts and a visible conversation back button
jondkinney Sep 10, 2026
46156dc
Stop opening the share menu after sending a link
jondkinney Sep 9, 2026
77a0a57
Make the menubar panel resizable with persistent display bounds
jondkinney Sep 10, 2026
e1f0d0e
fix: retain optimistic send failures in the conversation
jondkinney Sep 9, 2026
70a1881
Use resolved participant names for unnamed groups in the full list
jondkinney Sep 10, 2026
0615278
Prefer structured short names for generated group labels
jondkinney Sep 10, 2026
af09e6c
Join generated group labels with a final ampersand
jondkinney Sep 10, 2026
8624327
Render participant avatars for groups without custom photos
jondkinney Sep 10, 2026
b64764d
Keep avatar request identity until its output is consumed
jondkinney Sep 10, 2026
68c2be8
Show group composites while custom photo requests are pending
jondkinney Sep 10, 2026
226e89a
Inset participant composites from the outer avatar rim
jondkinney Sep 10, 2026
898ec9f
Cap menubar panel width at 500 logical pixels
jondkinney Sep 10, 2026
6c3fd40
Restore conversation row separators and full-height highlights
jondkinney Sep 10, 2026
f293a8a
Compact the list header and add an outlined search icon
jondkinney Sep 10, 2026
b2739f3
Place the version immediately after the app name
jondkinney Sep 10, 2026
24ddac5
Align list status and version within a single header row
jondkinney Sep 10, 2026
78927f6
Center pinned tile contents with even vertical padding
jondkinney Sep 10, 2026
55aab5a
Lists page from the keyboard: PgUp/PgDn/Home/End in threads, search, new
Fileri Sep 5, 2026
d96ab4c
A reaction on a note to yourself survives the self-echo fold
Fileri Sep 7, 2026
65f02dd
A tapback on a picture-only message gets its pill on the picture
Fileri Sep 7, 2026
ee04001
Scan contacts in bounded batches and page all findings
jondkinney Sep 10, 2026
386d9d1
Show full read-only fields for an exact contact source card
jondkinney Sep 10, 2026
c47f62b
Copy an exact contact source card as a vCard file
jondkinney Sep 10, 2026
cd7efaf
Offer copied vCards as standard clipboard files
jondkinney Sep 10, 2026
16efcfd
Save exported vCards in a chosen folder
jondkinney Sep 10, 2026
a0bf8ef
Copy contact fields with right click and keyboard shortcuts
jondkinney Sep 10, 2026
ab3e2ee
Name exported vCards with the contact short name
jondkinney Sep 10, 2026
b1931a8
Show clipboard copy feedback above contact review
jondkinney Sep 10, 2026
68a0d07
Merge #50: a failed send keeps its bubble and says why
nixfred Sep 10, 2026
d4545db
Merge #52: the menubar panel remembers the size you dragged it to
nixfred Sep 10, 2026
054956c
Merge #55: unnamed groups get participant names and a composite avatar
nixfred Sep 10, 2026
2c4cf9e
Merge #58: a reaction on a note to yourself survives the self-echo fold
nixfred Sep 10, 2026
8a13690
Merge #53: Ctrl+1-9 jumps to a pinned conversation, and a back arrow
nixfred Sep 10, 2026
24b29fb
Merge #57: a tapback on a picture-only message gets its pill on the p…
nixfred Sep 10, 2026
b5039cf
Merge #59: contact scans go past 200 contacts in bounded batches
nixfred Sep 10, 2026
0f50ba9
Merge #60: the full read-only detail of a contact's source card
nixfred Sep 10, 2026
7401940
Merge #61: copy or save an exact contact source card as a vCard
nixfred Sep 10, 2026
144320c
Merge #51: composer accessibility, spelling, and honest arrow keys
nixfred Sep 10, 2026
0bfa504
Merge #56: PgUp/PgDn/Home/End page whichever list is showing
nixfred Sep 10, 2026
f905cae
spell= in bridge.conf picks the spelling dictionaries; off turns them…
Fileri Sep 10, 2026
f0f2803
Tests read their own bridge.conf, not the developer's
Fileri Sep 10, 2026
d68f5b5
README: say at the Full Disk Access step that the grant covers every …
Fileri Sep 10, 2026
2ae5e07
List rows take the theme's hover fill, like the bubble cursor already…
Fileri Sep 10, 2026
9c51879
Secondary text dims by alpha, so it reads right on light themes too
Fileri Sep 10, 2026
2197e0d
fix: a group whose only name is its chat id names itself after its pe…
nixfred Sep 10, 2026
df4ceb2
imsg attachment serves stickers: StickerCache is part of the Messages…
Fileri Sep 10, 2026
bdee1fe
Merge #64: tests read their own bridge.conf, not the developer's
nixfred Sep 10, 2026
b8d1d39
Merge #65: say at the Full Disk Access step that the grant covers eve…
nixfred Sep 10, 2026
5b8e517
Merge #63: spell= in bridge.conf picks the spelling dictionaries
nixfred Sep 10, 2026
15a6fc6
Merge #66: list rows take the theme's hover fill
nixfred Sep 10, 2026
d1f4d3e
Merge #67: secondary text dims by alpha, so it reads right on light t…
nixfred Sep 10, 2026
d3c4b2d
Merge #68: imsg attachment serves stickers, which live in the Message…
nixfred Sep 10, 2026
0ceb462
docs: CONTRIBUTORS.md — name every contributor and what they actually…
nixfred Sep 12, 2026
9e21d1c
docs: add CONTRIBUTORS-X-POST.md, the length-fitted version for X
nixfred Sep 12, 2026
570c5c0
docs: rewrite the X contributor post from commit authors, not display…
nixfred Sep 12, 2026
e4aa3af
docs: README SUPER+M bind calls omarchy-shell, not qs -p /usr/share/o…
nixfred Sep 13, 2026
d685c67
blip-setup: ssh -n, so the Enter prompts do not hit EOF
ianswope Sep 13, 2026
b55cfc0
Every Text sink declares PlainText, so none can fall back to AutoText
ianswope Sep 13, 2026
6b78ac3
A security code reaches wl-copy on stdin, not through the environment
ianswope Sep 13, 2026
dcf7d58
README: say how to remove Blip, and what it leaves behind
ianswope Sep 13, 2026
a83c026
Merge #73: a security code reaches wl-copy on stdin, not through the …
nixfred Sep 13, 2026
24d63bc
Merge #72: every Text sink declares PlainText, so none can fall back …
nixfred Sep 13, 2026
dbb4573
test: textFormat invariant reads only the element's own depth, at any…
nixfred Sep 13, 2026
2fb0094
Merge #74: README says how to remove Blip, and what it leaves behind
nixfred Sep 13, 2026
7f89aea
Merge #71: blip-setup passes ssh -n, so the Enter prompts do not hit EOF
nixfred Sep 13, 2026
0549c58
docs: CHANGELOG backfills eleven 2.5.0 changes that merged without an…
nixfred Sep 13, 2026
4bad82d
Contact photos stop reloading on every window open: host cache, one b…
nixfred Sep 13, 2026
72d430b
Merge #54: sending a link no longer opens the share sheet
nixfred Sep 13, 2026
c8d6e86
docs: CHANGELOG names #62, a long draft scrolls back with the mouse w…
nixfred Sep 13, 2026
abde42d
Blip 2.5.0 — photos at once, and sends that stay put
nixfred Sep 13, 2026
8da602a
time: stamps cross the bridge as UTC; local time is a display concern
zachwilke Sep 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ jobs:
run: python3 bridge/mac/test_app_cards.py
- name: unsent messages are tombstones, not empty bubbles (no macOS needed)
run: python3 bridge/mac/test_unsent.py
- name: conversation cluster pins (no macOS needed)
run: python3 bridge/mac/test_cluster.py
- name: the optional read-push probe stays opt-in (no macOS needed)
run: python3 bridge/mac/test_blip_check.py
- name: hide Spam / unknown senders (no macOS needed)
run: python3 bridge/mac/test_filtered.py
- name: contact review across sources (no macOS needed)
run: python3 bridge/mac/contacts_test.py
- name: imsg chat metadata (no macOS needed)
run: python3 bridge/mac/imsg_test.py
- name: stamps cross the bridge as UTC (no macOS needed)
run: python3 bridge/mac/test_wire_time.py
- name: shell scripts (bash -n + shellcheck)
run: |
bash -n bridge/linux/blip-shim scripts/blip-setup scripts/sync-bridge.sh scripts/demo/blip-shots bridge/mac/install.sh
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ attempts trigger prompts on the Mac's screen.

```sh
bun test
cp *.qml *.ts manifest.json ~/.config/omarchy/plugins/nixfred.blip/
cp *.qml *.ts *.mjs manifest.json ~/.config/omarchy/plugins/nixfred.blip/
omarchy-restart-shell # NOT a hot-reload: IPC would stay on the old instance
qs -p /usr/share/omarchy/shell ipc call nixfred.blip status
```
Expand Down
83 changes: 66 additions & 17 deletions BarWidget.qml
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,21 @@ BarWidget {
property int unread: 0
property bool online: false // the Mac is reachable
property bool healthy: false // last collector run parsed cleanly
// Which reads reach the Mac, straight from the collector (bridge.conf's
// push_read). "all" is the default and pushes ONLY on the mark-all gesture,
// so reading a conversation clears it here and leaves the phone's badge —
// by design, and indistinguishable from a broken push until status said so.
property string readPush: ""
property string lastError: ""
property string lastRun: ""
// Unsent compose text per chat id, shared by the panel and the app window.
// In memory only: message text never lands on disk.
property var draftCache: ({})
// Contact photo per handle (file:// url, "" = letters), shared by the panel
// and the app window. The window is REBUILT on every show, so a map kept in
// BlipView started empty on each SUPER+M and asked for every photo again.
// Cache paths only, never image bytes.
property var avatarCache: ({})

readonly property bool hasUnread: unread > 0

Expand Down Expand Up @@ -75,16 +88,29 @@ BarWidget {
implicitWidth: button.implicitWidth
implicitHeight: button.implicitHeight

// Omarchy's panel hotkeys (`omarchy-shell shell toggle nixfred.blip`,
// SUPER+CTRL+<n> by bar position) find a bar widget's panel through open(),
// close() AND this property — without it the shell logs "summon: no live
// bar widget" and does nothing. Same line as Omarchy's clock widget.
readonly property bool opened: panelLoader.item ? panelLoader.item.opened === true : false
function open() { if (panelLoader.item) panelLoader.item.open() }
function close() { if (panelLoader.item) panelLoader.item.close() }
function toggle() { if (panelLoader.item) panelLoader.item.toggle() }
/** Open a conversation by chat id. Returns false when the id is not one —
* `goto ""` used to open a nameless thread with no header that nothing
* could send to, and a script with an unset variable is how you get there.
* The shape test is the one the toast's argv guard already uses; an id that
* LOOKS like a handle but is unknown still opens, on purpose, so you can
* start a conversation with a number you have never messaged. */
function show(chat) {
if (!panelLoader.item) return
var raw = String(chat === undefined || chat === null ? "" : chat).trim()
if (raw === "" || !/^[A-Za-z0-9._@:;$-]{1,256}$/.test(raw)) return false
if (!panelLoader.item) return false
panelLoader.item.open()
// `qs ipc call` rejects a leading "+" as a flag, so accept the bare digits too.
// Exact match FIRST, alias second: when both variants exist as threads,
// the alias must never shadow the exact one (Codex HIGH, 1.2.0).
var want = String(chat)
var want = raw
var t = null
for (var i = 0; i < threads.length; i++) {
if (String(threads[i].chat) === want) { t = threads[i]; break }
Expand All @@ -96,15 +122,18 @@ BarWidget {
if (!t && /^[0-9]{10,}$/.test(want)) want = "+" + want
// Unknown to the current window: still open it, with the id as the name.
panelLoader.item.openThread(t || { chat: want, handle: want, name: want })
return true
}

/** Newest arriving link → the share sheet on whichever surface is open. */
function shareArrivingLink(link) {
if (!link || !link.url) return
// Every link of the message: the sheet steps through them.
var urls = Array.isArray(link.urls) && link.urls.length > 0 ? link.urls.map(String) : [String(link.url)]
var w = windowLoader.item
if (w && root.windowVisible && typeof w.shareLink === "function") { w.shareLink(String(link.url)); return }
if (w && root.windowVisible && typeof w.shareLink === "function") { w.shareLink(urls); return }
var p = panelLoader.item
if (p && p.opened === true && typeof p.shareLink === "function") p.shareLink(String(link.url))
if (p && p.opened === true && typeof p.shareLink === "function") p.shareLink(urls)
}

function injectPanel() {
Expand Down Expand Up @@ -157,16 +186,26 @@ BarWidget {
}
// A fresh BlipWindow honours window.json (which may say hidden) — this
// call means SHOW, so make it so once its restore pass has run.
Qt.callLater(function() { if (windowLoader.item && !windowLoader.item.visible) windowLoader.item.visible = true })
Qt.callLater(function() { if (windowLoader.item) windowLoader.item.requestShow() })
}
/** Toggle the app window. Returns what it just DID, not what windowVisible
* says: ensureWindow() defers the actual `visible = true` to a callLater
* (a fresh BlipWindow has to run its restore pass first), so reading the
* property straight after showing still says hidden — IPC `window` reported
* "window hidden" on both paths and could never say it had shown one. */
function toggleWindow() {
if (root.windowVisible) hideWindow()
else ensureWindow()
if (root.windowVisible) { hideWindow(); return false }
ensureWindow()
return true
}
// Show AND focus: a window restored on another workspace is invisible to
// the user, and a plain toggle would HIDE it ("SUPER+M doesn't load the
// app"). Hyprland ≥0.56 dispatch takes Lua; classic focuswindow is rejected.
function showApp() {
// The popout gets out of the way first: the window is the same view,
// larger. Here, once, so double-click, the ⇱ button and IPC `app` (a
// SUPER+M bind) all agree — `app` used to leave the popout open.
root.close()
ensureWindow()
if (!windowLoader.item) return
// Fire-and-forget on purpose: a Process object silently ignores
Expand Down Expand Up @@ -195,7 +234,6 @@ BarWidget {
}
if (dblClick.running) {
dblClick.stop()
root.close()
root.showApp()
return
}
Expand Down Expand Up @@ -358,8 +396,9 @@ BarWidget {
var p = panelLoader.item
if (p && p.opened === true && p.inThread === true) return p
var w = windowLoader.item
// the window must be FOCUSED to count as being read (war room #25)
if (w && w.visible === true && w.focused === true && w.inThread === true) return w
// the window must be FOCUSED to count as being read (war room #25), and
// a thread merely peeked from the sidebar cursor is not being read either
if (w && w.visible === true && w.focused === true && w.inThread === true && w.peeking !== true) return w
return null
}
function activeReadChat() {
Expand Down Expand Up @@ -399,6 +438,7 @@ BarWidget {
root.online = d.online === true
root.lastError = String(d.error || "")
root.lastRun = String(d.ts || "")
if (typeof d.readPush === "string") root.readPush = d.readPush
if (d.ok === true) {
// Filter through the optimistic-read ledger: a poll that was
// already in flight when the user opened a thread must not
Expand Down Expand Up @@ -684,15 +724,22 @@ BarWidget {
// one is recorded — so a body with the code put the code on disk (Astra
// #1). The toast says a code arrived; click copies it from memory.
var body = (pendingCode.domain !== "" ? "For " + pendingCode.domain + " · " : "")
+ "Click to copy · Super+Shift+V types it"
+ "Click to copy"
fireToasts([{ chat: "", name: "Security code from " + who, text: body, ts: pendingCode.ts, key: "", code: pendingCode.code, at: Date.now() }])
}
// sh reads the code from its environment, hands it to the tool on stdin.
// The code goes straight to wl-copy on stdin. An environment variable
// outlived the copy: wl-copy stays resident to serve the selection, so the
// code sat readable in /proc/<wl-copy>/environ for as long as the clipboard
// held it -- past the five minutes copyCode() enforces, and past the toast
// that offered it. stdin is read once and closed.
property string copyValue: "" // what the NEXT copy hands to wl-copy
Process {
id: codeCopyProc
environment: ({ BLIP_CODE: root.copyValue })
command: ["sh", "-c", "printf %s \"$BLIP_CODE\" | wl-copy"]
command: ["wl-copy"]
onStarted: {
write(root.copyValue)
stdinEnabled = false
}
onExited: root.copyValue = ""
}
/** Copy a code. A toast passes the code it DISPLAYED, so clicking an older
Expand All @@ -703,6 +750,7 @@ BarWidget {
// An older toast's code is copied only within ITS five minutes (Astra A#9).
if (code && String(code) !== pendingCode.code && Date.now() - notifyProc.toastCodeAt > 300000) return "code expired"
copyValue = String(code || pendingCode.code)
codeCopyProc.stdinEnabled = true
codeCopyProc.running = true
return "copied"
}
Expand Down Expand Up @@ -795,7 +843,8 @@ BarWidget {
return "online=" + root.online + " unread=" + root.unread + " leader=" + root.leader
+ " window=" + (w && w.visible ? (w.focused ? "focused" : "unfocused") : "hidden")
+ " threads=" + root.threads.length + " healthy=" + root.healthy
+ " push=" + root.watchAlive
+ " watch=" + root.watchAlive
+ " read_push=" + (root.readPush !== "" ? root.readPush : "?")
+ (root.lastError !== "" ? " error=" + root.lastError : "")
}
function threads(): string { return root.automationOn ? JSON.stringify(root.threads) : root.automationOff }
Expand All @@ -804,15 +853,15 @@ BarWidget {
function open(): void { root.open() }
function close(): void { root.close() }
function toggle(): void { root.toggle() }
function goto(chat: string): string { if (!root.automationOn) return root.automationOff; root.show(chat); return "shown" }
function goto(chat: string): string { if (!root.automationOn) return root.automationOff; return root.show(chat) ? "shown" : "not a conversation id" }
function copycode(): string { if (!root.automationOn) return root.automationOff; return root.copyCode() }
function typecode(): string { if (!root.automationOn) return root.automationOff; return root.typeCode() }
function share(url: string): string { if (!root.automationOn) return root.automationOff; root.open(); return panelLoader.item ? panelLoader.item.shareLink(url) : "no panel" }
function compose(text: string): string { if (!root.automationOn) return root.automationOff; return panelLoader.item ? panelLoader.item.composeAndSend(text) : "no panel" }
function bubbles(): string { if (!root.automationOn) return root.automationOff; return panelLoader.item ? panelLoader.item.bubbleModel() : "[]" }
function find(query: string): string { if (!root.automationOn) return root.automationOff; return panelLoader.item ? panelLoader.item.searchFor(query) : "no panel" }
function newchat(query: string): string { if (!root.automationOn) return root.automationOff; return panelLoader.item ? panelLoader.item.newChatFor(query) : "no panel" }
function window(): string { root.toggleWindow(); return root.windowVisible ? "window shown" : "window hidden" }
function window(): string { return root.toggleWindow() ? "window shown" : "window hidden" }
function app(): string { root.showApp(); return "app shown + focused" }
function windowgoto(chat: string): string {
if (!root.automationOn) return root.automationOff
Expand Down
Loading
Loading