Commit Graph
40 Commits
Author SHA1 Message Date
pastilhasandClaude Opus 5 31ffe084f5 revert the second server too: one officer, one token, this origin
Andre wants to log out and log back in against a single server, so this takes out dc6b623 and my
token-resolution change with it — the latter first, because it was written against
useServerClient, which dc6b623 introduced.

Gone: the connections store, the server chips, the per-server client and the per-server socket
url. `useClient()` is back to one origin, `/api`, and the session it already holds. The chat
socket url is back to what it was:

  const token = localStorage.getItem('BEARER_TOKEN');
  const wsUrl = `${protocol}//${window.location.host}/api/chat/ws?token=${token}`;

Verified: the staged tree is byte-identical to dc6b623^ across all of src/.

Two things he should know rather than discover.

The old line reads localStorage and nothing else — the same single spelling I widened an hour ago
and have now removed again. If his token is NOT in localStorage, this code fails exactly as
before, and worse: a missing one interpolates as the literal string "null" rather than an empty
value. Reverting cannot fix that class of problem; it restores it.

`officer.connections.v1` stays in his browser's localStorage with alpha's API key in it. Nothing
reads it now, so it is inert, but it is a credential sitting in a store nobody owns any more and
should be cleared by hand.

Typecheck clean. 600 pass, 2 fail — cliamp and pty, unchanged all evening and unrelated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 23:37:02 +01:00
pastilhasandClaude Opus 5 0a4ff548b9 revert the chat tabs and panes work, back to one conversation
Andre asked for zero, not another fix on top. Reverts ec4f06a..7726c9f — the ten commits from
"tabs and panes" onward: the tab bar and pane splitting, tab renaming and its page title, the
per-server directory picker, the render-loop fix, pane transcript resolution, the send queue,
the two socket fixes from the other session, the pane-socket notes, and my own socket-set change
from tonight. He is rebuilding from here.

Deliberately KEPT: dc6b623, "talk to two officers at once from one browser". That was a separate
ask that predates the tabs one, and the multi-server client, the server chips and the connections
store stand on their own without panes. Reverting it too is one more command if that was the
intent.

Collateral, worth naming: cb7ab55 carried an unrelated MusicPlayerHost change alongside its
socket instrumentation, so that came out with it.

Reverts, not a reset — every one of these is pushed and a second session is live in this repo.

Typecheck clean. 600 pass, 2 fail — cliamp path-escape and the pty transport test, both failing
identically before this and unrelated to chat.

What is NOT explained by this revert: the browser symptoms tonight. The server was verified good
throughout — two real turns streamed back through the public URL on both models, and the full
2,281-message history came through nginx intact. Whatever the client fault is, it is still
unfound, and the pre-tabs code is where it now has to be looked for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 23:29:18 +01:00
pastilhasandClaude Opus 5 a19895c216 tabs and panes: several conversations, several machines, one window
The iPad layout in the browser. A tab holds one to three panes; each pane is a whole chat —
its own server chips, its own list, its own conversation, its own socket.

The blocker was that chat:selected-session is ONE channel for the screen, so two detail
panels would have shown the same conversation. A pane now provides its own selection through
context and usePaneSelection prefers it; outside a pane the context is absent and the channel
behaves exactly as before, so the dashboard chat panel and the mobile layout are untouched.
Context rather than props because SessionList and ChatDetailPanel sit at different depths and
neither should know whether it is inside a pane.

A pane shows its LIST until something is open and the CHAT afterwards, with one way back.
Mobile can afford both at once inside a pane; three of those in a browser column would leave
nothing for the conversation itself.

The layout lives in one unscoped localStorage entry, deliberately not per server — a tab
holding one conversation from the laptop and one from alpha belongs to neither. Pane keys are
re-minted on restore, because keys from a previous page whose counter restarted at zero make
React reuse the wrong subtree and a conversation appears in the wrong column.

What this gives up, and it is the only thing: /chat/<id> still deep-links but can only open
in the first pane. With three conversations on screen there is no single one for the address
bar to name.

WorkspaceView and the fixed three-panel layout are gone from this screen; the panels
themselves are unchanged and still registered for the dashboard.

Typecheck, 602 tests and the SPA bundle all pass. Nobody has clicked it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 21:17:02 +01:00
pastilhasandClaude Opus 5 dc6b623ee1 talk to two officers at once from one browser
The chat app on the iPad does this already and this is its model, not the music app one.
Music keeps its active server — one library at a time is the right question there. Chat is
the exception: two panels side by side, one on the laptop and one on alpha, both live, no
switching.

The mechanism is one string. A panel holds a serverId; that same string picks the base URL,
the credential, the websocket host and the tail of the react-query key. Nothing global is
consulted when it is named, which is exactly why two can be live at once — there is no
active server in connections.ts at all, because there is nothing to switch.

THIS ORIGIN IS NOT IN THE LIST. It is represented by null, so every existing useClient()
call is untouched and adding a connection cannot break the app you are already signed into.
That property is what makes this shippable before anyone has tried it.

A second server is reached with an ofk_ API key minted there, verified against /api/auth/me
before it is stored — a URL typo and a key from the wrong machine are otherwise
indistinguishable from an empty conversation list an hour later.

Copied deliberately from the mobile code: the base URL is derived per call rather than
memoised (a cached one hands back whichever server was asked for first), the row stamps its
server onto the selection BEFORE navigating (or the resolver reads the transcript from this
origin, where two officers can hold the same uuid), and changing server clears the cwd and
the open conversation, because a path from the machine you left names nothing on the one you
arrived at.

Not yet opened in a browser. Typecheck and 602 tests pass, and the cross-origin request with
an API key is verified by curl, but no human has clicked any of this.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 20:06:35 +01:00
pastilhasandClaude Opus 5 8f2d80573a actually give existing users the live panel
The previous commit added it to defaultLayout, which anyone who has ever opened /chat never sees:
useDashboardState seeds its default only when the key is ABSENT, so a stored layout keeps the shape it
had when it was first written. appTypes/normalizeLayout does not cover this — it repairs which app a
panel runs, never the tree — so the change was visible only on a fresh account. It was shipped with a
note to reset the layout by hand, which is not a fix.

The screen now replaces a layout with no chat-live panel. Replacing outright is safe here specifically
because the screen is locked: the structure is dictated by code, and the only user contribution is
column sizes. Terminates because the replacement contains the panel it tests for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 01:15:40 +01:00
pastilhasandClaude Opus 5 c4241c23da show what the agent is actually running, above the history
The /chat sidebar is now a vertical split: live sessions on top, the transcript list below. They look
similar and answer completely different questions — the list reads conversations from disk, thousands of
them, while this reads the agent's in-memory map over `claude:list`. Only the second can tell you a
conversation is still working while nothing is on screen, which is exactly the state that has been
invisible: after a `pm2 restart officer`, or from a browser that has never seen the session, officer has
no record of a live turn and only the agent can say.

`pendingTasks` is surfaced per row because it is the load-bearing number. It is what keeps a session
alive with nothing on screen, and what makes restarting the agent sidecar unsafe at that moment.

Polled at 10s rather than pushed: liveness changes without officer being told — a turn ends, a
background task reports — so there is no single event to subscribe to. The request is one map read.

Titles come from the sessions query already in cache, so they cost nothing, but that query only covers
the group being browsed and a live session can be in any of them. Unmatched rows show a short key rather
than inventing a name, and an unsaved chat renders unlinked rather than pointing at a transcript that
does not exist yet.

Closes the UI half of step 2 in docs/chat-session-lifetime.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 01:13:44 +01:00
pastilhasandClaude Opus 5 c9735580fc declare panel channels once, and fix the bump that could lose a refresh
Four channels were bare string literals repeated across files, with the payload type supplied by each
caller. Neither hole errors: a typo yields a different, empty channel — publisher publishing into nowhere,
subscriber waiting forever — and a publisher and subscriber can simply disagree about the payload with
nothing to check them. defineChannel(name, initial) returns the hook, officerdev/src/channels.ts declares
the four, and every usePanelChannel call site in the repo now passes a shared constant.

files:refresh-signal was bumped two different ways: Date.now() at the Chat sites, setSignal((n) => n + 1)
at the FileViewer ones. The increment is wrong — useGlobal's functional setter applies against the value
captured at render, so two bumps in one render window both compute snapshot + 1 and the second writes the
same number as the first. Nobody re-reads and the file that was just written stays stale. Date.now() has
the same flaw at millisecond scale, and the four FileViewer sites (save, delete, extract, transcribe) sit
close enough to hit it. useFilesRefresh's bump is a module counter that never reads React state, so it is
right however many times it is called between renders, and it is identity-stable through a ref because
useGlobal's setter is a fresh closure every render and this goes into dependency lists.

system-settings:run-command is deleted. It had a writer once — 7c0b11c wired the AI harness installer to
it — and when that install moved server-side to POST /server-settings/chat-providers/install the write
went with it, leaving a channel whose only remaining writes were clears, a terminal pane nothing could
open, and a second layout nothing could select.

PanelComponentEntry's component, header and provider are typed with { panelId: string }, which is what
PanelSlot has always rendered them with. A no-prop component is still assignable, so no screen changed.

chat:active-session and preview:refresh are declared but still have no subscriber. preview:refresh has no
plausible one — the PreviewProvider that read it is gone from the repo — but both are published by the
chat panel, and that is not this branch's to change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 11:14:55 +00:00
pastilhasandClaude Opus 5 1de1d925a5 make the appType allow-list a prop instead of fourteen copies
Every locked screen shipped the same recursive normaliser: an ALLOWED_APP_TYPES set, a
normalizeLayout, a useMemo to apply it before the wrong panel could render, and a useEffect to
persist the fix. Fourteen copies, character-for-character identical except the two names — so a
fifteenth screen was a copy-paste, and a bug in the shape was a bug in fourteen places.

It is now `<WorkspaceView appTypes={{ allowed, fallback }} />`. WorkspaceView normalises before it
renders and persists the diff itself, which is the same two effects the screens were writing by hand.

One deliberate behaviour change: the framework normaliser drops `config` when it replaces an app.
The fourteen copies did `{ ...node, appType: fallback }`, keeping the old app's config on the panel
the new app now owns. That is the opposite of what `setApp` does, and a config belongs to whoever
wrote it.

Headscale keeps a local useMemo. Its check is not "is this appType allowed" but "is the server
picker present at all" — a layout saved before that panel existed is discarded for the default
wholesale. That is about a panel being missing, which the allow-list cannot see.

QrTransfer gains a persist-back it never had: it normalised on read and threw the result away every
time.

Tests: normalizeLayout is pinned on reference-identity for a no-op, null always allowed, config
dropped on replacement, rebuilding only changed branches, and idempotence — because a normaliser
that does not normalise to itself makes the persist-back an infinite write loop.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 10:06:59 +00:00
pastilhasandClaude Opus 5 47894702ac merge a /clear chain into one conversation
The list collapses each chain to its newest link — the only one that can be
resumed — carrying the root's title and start time, the summed message count
and a part badge. The detail splices the chain's transcripts oldest-first with
a divider between parts, server-side, so the client's index-window pagination
needed no change.

The divider says "context cleared — nothing above this is in memory", because
the whole risk of merging is that the history reads as continuous when the
agent's context is not. Delete cascades the chain and the confirm says how many.

Supersedes the "continues X" line from the previous commit: there is nowhere to
link to once the parent is scrolled up above you.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 00:44:33 +00:00
pastilhasandClaude Opus 5 3e01de100e retire a leftover ?cwd= from chat urls
nothing reads it and nothing writes it any more, but a refresh re-requests the
address bar verbatim, so one left over from before the path-based groups sits
there indefinitely looking like it means something. on a bare /chat it still
says which group you wanted, so upgrade it to /chat/g/<path> rather than
dropping it — an old bookmark keeps working. anywhere else the session decides
its own directory, so it is just removed. other params untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 00:27:41 +00:00
pastilhasandClaude Opus 5 942cc2b61d link cleared sessions back to the conversation they continue
/clear starts a new claude session and the list showed it as an unrelated
conversation. claude records no parent link anywhere — not in compactMetadata,
logicalParentUuid, summary.leafUuid, the per-session slug, or the live process
registry — so infer it: a cleared transcript opens with /clear, and /clear
happens inside one process, so the parent is the conversation in the same group
that was writing to disk at the instant this one began (4ms apart, measured).

matching is on per-minute activity rather than updatedAt, because resuming a
parent moves its end time past its child's birth and lost the link entirely for
two of the three cleared sessions here. the window is symmetric because clearing
makes claude summarise the conversation it is ending, so the parent's final
record can land after the child's first. ambiguity fails closed — the wrong
parent also renames the conversation.

read-only: nothing is written back to claude's store, and an explicit title
always wins.

also: cleared sessions were titled "<command-name>/clear</command-name>" because
claude does not set isMeta on slash commands; and the chat header was hardcoded
to undefined, so it read "New chat" above every conversation you opened.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 00:22:28 +00:00
pastilhasandClaude Opus 5 236541fa2a chat urls: a group is a path, a session decides its own directory
the chat group moves from ?cwd= to a path suffix behind a g/ discriminator
(/chat/g/home/me/project), and a session url carries no group at all.

the real fix is not the spelling. a session's working directory was read back
off the query string to decide where the agent executes, so the address bar was
the authority on where code runs. a pasted or refreshed /chat/<id> arrives with
no ?cwd= at all, so a turn sent before the resolve landed ran in the default
general_chat_sessions dir instead of the project; and a hand-edited ?cwd= could
name a group the session doesn't belong to, with nothing to reconcile them.

loadClaudeSessionById already resolves a session's cwd from the id alone, so the
id is the only source of truth there. it now travels on SelectedSession.cwd,
which is what the composer reads. the url can no longer contradict it.

the vocabulary lives in apps/ChatHistory/chat-routes.ts so a link built in a
panel and one built in a screen cannot drift.

also: startAgentRun no longer returns a literal chatUrl — it returns cwd and
AgentRunnerModal builds the link, so the server holds no copy of the frontend
url shape. and the post-turn permalink strips a stale ?cwd= instead of carrying
it forward onto the new session's url.

walkthrough doc gains item 13.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 23:59:39 +00:00
pastilhasandClaude Opus 5 306def14f3 rebuild the chat session list on the shared data primitives
the list drew its own border per row on top of nothing, so every boundary
between two rows was a double hairline, and it emphasised two things per row
where the design language allows one. DataRow/DataList settle both.

loading and empty were the same grey sentence, which made a slow transcript
read look like an account with no history; they are now LoadingBlock and
EmptyBlock, and a failed read gets an ErrorBlock with the actual message
instead of rendering as "no sessions".

rename and delete swallowed their failures whole — useClient only raises a
dialog for 401, 403 and 5xx, and the likely error here is a 404 from a
transcript that vanished under you. both toast now, as does a deep link to a
session that cannot be read, which used to open an empty pane and say nothing.

the New Chat button was duck-teal filled with duck-yellow text: duck-teal is
a bright cyan in dark mode and duck-yellow has no dark override, so the pair
sat near 2:1 contrast in both themes.

active-row highlight now comes from the route rather than the selection
channel, so it is right on a deep link before any panel has published, and
deleting the open session navigates out of it instead of leaving a dead route.

deletes SessionBar and SessionContextMenu: the first was exported through two
barrels and imported nowhere, the second was never imported at all and typed
its session id as a number.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 23:24:24 +00:00
pastilhasandClaude Opus 5 c591a8a771 make the /chat url a permalink again
reloading /chat/<id>?cwd=<dir> landed on an empty general_chat_sessions instead
of the conversation. one line did both halves of it:

  if (replaceUrl) window.history.replaceState(null, '', `/chat/${msg.sessionId}`)

that ran on session:init, and session:init's sessionId is officer's own
per-connection key — websocket.ts mints it as `msg.sessionId || randomUUID()`.
/chat/sessions/:id resolves a CLAUDE TRANSCRIPT uuid, so the address bar ended
up naming something no lookup could find; the detail fetch 404'd and the catch
dropped you into a blank chat. the template also had no location.search, so
?cwd= — added later, for agent grounds — was thrown away every time the socket
connected, which is why the pwd picker fell back to the default group.

the transcript uuid is only known once the turn reports it, and it only started
crossing the wire in 7b6ca5f, so move the rewrite to `result`, use
claudeSessionId, and carry the query string through untouched. new chats gain a
working permalink too — /chat/new used to become an unresolvable id the same way.

mobile back had the same query-string hole; it now keeps the search.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 02:15:30 +00:00
pastilhasandClaude Opus 5 6b46d9f1f9 chat: put the session list cwd in the url, not a panel channel
Which project group you are looking at is addressable state, so /chat?cwd=<dir> has to be a
link anyone can hand out — it survives a refresh and an agent run can point straight at its
own runs directory. Was usePanelChannel('chat:active-cwd'), which per the navigation audit is
for signals and refresh buses only. Row links and New Chat now carry the query string along.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 00:28:44 +00:00
pastilhasandClaude Opus 4.8 b27dd7512b load chat transcripts as a tail window and lazy-load older on scroll-up
extremely long transcripts made bottom-anchoring the virtualized list unreliable
(thousands of unmeasured variable-height items = a huge estimate the scroll never
lands on). now GET /chat/sessions/:id takes limit+before and returns a windowed
slice plus total+offset. the chat opens on the last 20 messages, anchors to the
bottom instantly, and scrolling near the top pages in the next older window,
prepending it and pinning the previously-top message so the view stays put.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 03:49:13 +00:00
pastilhasandClaude Opus 4.8 3682269936 resolve /chat/<id> by scanning project groups and make session rows real links
deep-linking or refreshing /chat/<id> only has the id, so add loadClaudeSessionById
to scan every project group for the transcript and return its real cwd. the session
list page resolves on load from that, setting the cwd picker and resuming. session
rows are now <Link to=/chat/<id>> so clicking updates the url and flows through the
same resolve path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 03:35:47 +00:00
pastilhasandClaude Opus 4.8 70555c8d71 chat: retire the old saved-session store (Stage 4a)
Deletes all session persistence that isn't Claude's native transcript store, per
the "only harness-native session management survives" rule.

Backend: delete api/pi/storage.ts (meta.json+messages.json file store), the
api/saved-sessions router (+ unmount), the /pi/sessions REST endpoints, and the
storage.save/loadSession calls in the chat WS handler (in-memory session-manager
stays for live turns; no disk persistence — Claude's transcript is the record).
Also drops the Postgres saved_sessions layer: schema/chat.ts, queries/saved-sessions.ts,
its types and re-exports.

Frontend: delete state/useSavedSessions, ChatList, and the ChatHistory Widget
(all pure saved-session UI); slim ChatHeader to a label; strip the auto-load-latest
+ Save wiring from ChatPanelWrapper and ChatDetailPanel; drop the old resume path
from useChat and SessionListPage; remove the /chat/saved/:id route and the
useInitialData prefetch.

Behavior removed (intended): the Save-session button, email/project panels
auto-resuming the last chat, and /chat/saved/:id. /chat itself is unchanged —
already fully on Claude transcripts. The orphaned saved_sessions Postgres table
is dropped on the next `bun db:push`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 15:29:26 +00:00
pastilhasandClaude Opus 4.6 406d8f9e54 saved sessions: make routable via /chat/saved/:id URLs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 20:15:36 +00:00
pastilhasandClaude Opus 4.6 2a8df0def1 saved sessions: replace transient history with persistent DB storage and auto-save
- Add saved_sessions table and CRUD endpoints (save, list, resume, update, delete)
- Save is instant (no LLM summarization), stores exact conversation with tool calls
- Resume loads full message history into chat UI, sends transcript to agent on first message
- Auto-save updates DB after every agent response once a session is saved
- Delete old filesystem-based session/group management (sessions router, useChatSessions, useChatGroups)
- Clean up ChatHeader, SessionList, ChatDetailPanel for saved sessions flow

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:36:25 +00:00
pastilhasandClaude Opus 4.6 927267e041 workspaces to dashboards, imap email sync, ffmpeg tool, tts fix, file browser refresh, automation sidebar reorder
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 20:00:45 +00:00
pastilhas 2126f3912e pi sills 2026-02-23 22:52:27 +00:00
pastilhas 8fb96c7cf8 mobile 2026-02-23 11:16:33 +00:00
pastilhas d9b69c8209 fixed /chat 2026-02-23 08:00:53 +00:00
pastilhas ed0debfeac WorkspaceLayout in officerdev workspace 2026-02-22 22:43:28 +00:00
pastilhas 7fe7260f51 better chat ui 2026-02-22 20:52:19 +00:00
pastilhas ab03b175e7 Projects 2026-02-22 01:50:15 +00:00
pastilhas 23b369c7e1 Workspaces 2026-02-22 00:05:29 +00:00
pastilhas 9e9acd9631 major apps and hooks refactor 2026-02-21 21:24:31 +00:00
pastilhas af803236c8 Chat refactoring 2026-02-21 15:05:56 +00:00
pastilhas 68c7973281 fix: Pi harness - dynamic models, correct RPC protocol, proper event handling
Backend:
- /api/pi/models now calls 'pi --list-models' with stored API keys
- pi-bridge.ts: callback-based event handling (matches pi-monorepo)
- pi-bridge.ts: correct RPC format (type: 'prompt' not jsonrpc)
- pi-bridge.ts: pass API keys to Pi process env
- websocket.ts: event handler runs in background, no blocking
- rest.ts: fix user home path (getHomeDir instead of hardcoded)

Frontend:
- Fix /api/ double prefix in useChatSessions, useChatGroups, useModels
- Add PROVIDER_DISPLAY mapping in SystemSettings.tsx
- Provider tabs show friendly names (e.g., 'OpenCode Zen')

UI (from previous session):
- Grouped session list with collapsible folders
- CreateGroupDialog, GroupContextMenu, SessionContextMenu components
2026-02-20 22:55:00 +00:00
pastilhas ba51ee0320 Complete frontend migration to unified Pi harness (Phase 7 + Phase 9)
- Delete legacy hooks: useClaude.ts, useOpenCode.ts, usePiMono.ts, App.backup.tsx
- Update all components to use usePi instead of legacy hooks
- Replace useVisiblePiMonoModels/useClaudeModels/useOpenCodeModels with useVisiblePiModels
- Migrate from LegacyChatMessage to ChatMessage type throughout
- Update SessionBar to remove provider and archive props
- Simplify ChatDetailPanel to Pi-only (remove Claude/OpenCode components)
- Fix useChatSessions calls (remove provider parameter)
- Update user-settings types: provider now only 'pi' instead of legacy values
- Update PI_HARNESS_REBUILD.md to mark phases complete
2026-02-20 21:42:26 +00:00
pastilhas 2a7485add2 PI 2026-02-20 04:32:50 +00:00
pastilhas d9f6eb14ed PI-MONO 2026-02-20 04:32:50 +00:00
pastilhas 6a83342013 pi-mono 2026-02-19 18:05:50 +00:00
pastilhas dd8ab84df5 Workspaces in Workspaces all around 2026-02-19 01:49:15 +00:00
pastilhas 485ae4c3d7 Workspaces layout, automation page 2026-02-18 17:04:32 +00:00
pastilhas c2660cd125 frontmatter in skills files 2026-02-18 01:23:22 +00:00
pastilhas 3733e99ba8 harness uniformization 2026-02-17 20:37:40 +00:00
pastilhas 74e98e95e1 extracted chat component to widgets 2026-02-17 19:52:41 +00:00