Commit Graph
401 Commits
Author SHA1 Message Date
Federico Jaramillo Martinez ce4b469727 feat(ui): add model and thinking selector actions 2026-07-27 09:40:24 +02:00
Federico Jaramillo Martinez 4940eda352 fix(sessions): stop counting session startup as work in progress
Startup progress rides the per-session activity channel with an "active"
phase, because a startup phase really is in progress. But isSessionActive()
treated any active activity as work, so a session that was merely *opening*
enabled "Stop Active Work", disabled "Reload from disk" with the misleading
"Stop current session activity before reloading" tooltip, showed the row's
active-work indicator, and — for any caller that hands a startup activity to
WorkspaceActivityService — reported the whole workspace as busy. Selecting an
archived, read-only session reported active work while it opened.

Starting is not working. publishStartupProgress now marks its reports with a
new optional SessionActivity.startup field, and isSessionActive() does not
count a marked activity. Every affected consumer — the session list, the core
actions, the app's activity-transition handling, and the server's workspace
aggregation — reads that one helper, so the correction lands in all of them at
once.

The marker is a new field rather than a new phase on purpose: six readers test
phase === "active" directly, including the pending row's "creating · " prefix,
the chat dock's active styling, and the daemon's own heartbeat re-publication.
It can only ever remove the activity-phase reason for being active, so
streaming, bash, compaction, and queued prompts still report as active through
the status even while a startup report is the latest activity. The chat dock
still shows the startup text; this changes what counts as work, not what is
shown.

The browser's own pending-create row keeps its previous appearance: it borrows
only the daemon's phase text and drops the marker, since that row stands for a
create the user is waiting on rather than a session the daemon is opening.
2026-07-26 22:54:27 +02:00
Federico Jaramillo Martinez 12200ab26a fix(workspaces): serialize overlapping topology refreshes
The browser-resume path and the plugin-facing app refresh call
refreshSelectedProjectTopology independently, so two requests for the same
machine and project could be in flight at once. The stale guards check machine
and project but not ordering, so a slower earlier response landing last
overwrote a newer list, making a just-created worktree disappear again.

Route the refresh through TrailingRefreshCoordinator, the primitive already
used for browser resume, session refresh, and activity, keyed by machine and
project. A second caller no longer opens its own request while one is in
flight; it gets a single trailing pass, so the last applied response is the
newest one.
2026-07-26 22:25:59 +02:00
Federico Jaramillo Martinez 92c39c8f35 fix(workspaces): keep selected workspace metadata fresh on refresh
A background topology refresh replaced the workspace list but left
selectedWorkspace pointing at the old object, so a branch switched inside a
worktree outside PI WEB showed the new name in the list while the collapsed
Workspaces header and the mobile context bar kept the old one until reselect.

Re-point selectedWorkspace at its refreshed entry, keyed by id (derived from
the path), so which workspace is selected never changes and the session and
terminal teardown in handleWorkspaceChange still does not fire. Skip the patch
entirely when metadata is unchanged, so an ordinary resume does not churn
object identity into state on every focus.
2026-07-26 22:25:59 +02:00
Federico Jaramillo Martinez 8a24a7c4a5 feat(workspaces): refresh worktrees on browser resume
Call WorkspaceController.refreshSelectedProjectTopology() from the existing
browser-resume refresh and the plugin-facing refreshAppData path, so worktrees
created or removed outside PI WEB become visible with no user action. No new
timer, watcher, process, or push channel; the resume path is already debounced
per animation frame and collapses concurrent requests.

Document the resume-scoped detection and the hiding of gone checkouts in the
FAQ, and add the changeset for the user-visible behavior.
2026-07-26 22:25:59 +02:00
Federico Jaramillo Martinez 213ec104ea feat(workspaces): add non-disruptive workspace topology refresh
Adds WorkspaceController.refreshSelectedProjectTopology(), which re-lists the
selected project's workspaces and applies them through applyProjectWorkspaces
only. It never routes through selectWorkspace, which lacks an already-selected
guard and would clear the active session and all workspace-scoped state.

Stale responses for a project or machine the user has since left are discarded,
and failures go to an injectable background error sink instead of state.error.
2026-07-26 22:25:59 +02:00
Federico Jaramillo Martinez bd4a891b95 fix(sessions): correlate startup progress by token instead of workspace
Startup progress could still be shown on the wrong session's row. Routing by
known session id first closed the case where the browser knew the other
session, but left open the case where it does not -- which the browser is
designed to produce. While a create is pending for a workspace,
applyCreatedSession deliberately withholds a session.created event for that
workspace and stashes it, to avoid a duplicate row. So during exactly the
window this feature exists for, a session created by an agent's spawn or by
another tab is intentionally absent from the session list. Its startup events
carried an unrecognised id and a matching cwd, and were routed onto the user's
pending create row, showing a phase and a label belonging to another session.

Workspace path was never evidence of identity; it was the only key both sides
happened to share. Give them a real one. The browser already invents a
temporary row id for a pending create, so it now sends that id with the create
request as an opaque startupToken; the daemon carries it through construction,
echoes it on the startup events it publishes for that construction, and the
browser matches it exactly. The token is a throwaway label the daemon never
interprets. It never becomes the session id: activity.sessionId still carries
Pi's SessionManager id, which remains how an open of an already-known session
is routed.

With exact identity available, the guessing is deleted rather than gated.
startupProgressPendingStart goes entirely, and with it the selected-machine
comparison, the cwd filter, and the single-match ambiguity rule: a second
concurrent create carries a different token, and a foreign workspace or
non-selected machine carries no token this browser is waiting on, so those
cases stop existing rather than needing detection. One Map lookup replaces a
filtered scan. cwd comes off the event, since it existed only as the routing
key and nothing else read it.

No compatibility path is needed. session.startup is unreleased -- checked
against the published tarball, not only git tags -- so no deployed daemon
emits these events and no deployed browser parses them. An older daemon
ignores the extra request field; a newer daemon talking to an older browser
degrades to the pre-existing generic wording, as does any unmatched token.

One silent behaviour change to state plainly: startupProgress guarded on
`sessionId === "" || cwd === ""`. Removing cwd from the event removes the
meaningful half of that guard, and that half had no test. The session-id half
is kept, which is the half that actually protects honest reporting.

The replaced ambiguity test is rewritten rather than dropped, so the same three
scenarios still pin the user-visible guarantee -- no match means the generic
wording stays -- now including the reproduced foreign-session case, which fails
against the previous code. Session creation ordering, semantics, and queueing
are unchanged; the token is a passthrough label read only to build an event.
2026-07-26 22:10:58 +02:00
Federico Jaramillo Martinez 67be9fb523 fix(sessions): attribute session startup progress by id before workspace
Startup progress resolved its target row by workspace path first and only
fell back to a known session id, which let one row be shown another row's
phase. While a create is pending in a workspace, an existing session in that
same workspace can also be opened -- by selecting another row, by another
tab, or by a subsession open -- and that open publishes the same cwd. The
cwd-first order rewrote such an event onto the pending create row, so a user
watching a session being created could be told a phase that belonged to a
different session. That is exactly the dishonest attribution this work set
out to avoid.

A known session id is the strongest available proof of the target, so it is
now checked first; workspace routing is used only when the id is unknown,
which is precisely the pre-session case it exists for. No wording changed and
no event changed; only which row an event is applied to.

Two tests were added where behavior was asserted but not proved. The
controller test fails against the previous order, so the misattribution is
now pinned. The service test covers a startup whose extension binding
rejects, proving the window still ends with an idle report rather than
leaving a waiting row labelled with a phase the service has left.
2026-07-26 17:05:56 +02:00
Federico Jaramillo Martinez 49e7c390f3 feat(sessions): tell the user what a session start is waiting on
Creating or opening a session could stall for reasons the daemon knew
about and never shared. The browser invented the whole message it showed
while waiting -- "Creating session: Waiting for the backend session to be
ready" -- which says that we are waiting but never what for. A shared
ModelRuntime read during startup can be handed a network refresh that is
already in flight, and extensions may do their own network I/O while
loading, so the wait is real and previously unattributable.

The pre-session gap turned out to be a missing shared key rather than a
missing channel: publishActivity needs the PiAgentSession being built, but
the session id and cwd are both known before the first await. So create()
now publishes a new global session.startup event carrying an ordinary
SessionActivity, routed by cwd -- the one identity a browser row waiting
for a session id can match, since the client-invented pending id is
unknown to the daemon and the daemon's id is unknown to the browser.

Two phases are reported, each published before the await it describes so
the label changes during the wait rather than after it: "Starting the Pi
session" and "Loading session extensions". Both are facts, because the
service awaits exactly one call for each. A concurrent background catalog
refresh is appended as a note ("provider model lists are refreshing"),
never as the cause: the refresher can prove a refresh is running but not
that this startup joined it. ModelCatalogRefresher gains only a read-only
isRefreshInFlight() getter; cadence, timeout, and coalescing are untouched.

Reporting is event-only and synchronous. It writes no activities entry, no
workspace activity, and no unread state, so a failed creation leaves
nothing stranded, no await is added, and creation ordering and semantics
are unchanged. The window-ending idle report is skipped when a real
activity was published during startup, so an extension error survives.

The browser applies startup progress only when it can prove the target:
one non-discarded pending start in that cwd on the selected machine, or a
session whose id it already knows. A foreign workspace, another machine,
or two concurrent starts in one workspace keep today's generic wording
rather than showing one row the phase of another. An idle report restores
that generic wording, including the queued-messages variant.

docs/config.md said nothing a request triggers waits on a catalog fetch.
That is not strictly true for a refresh already in flight, so both it and
the generated docs/config.html now state the exception and say PI WEB
reports it while it happens.
2026-07-26 16:19:05 +02:00
Federico Jaramillo Martinez 111db63f4a fix(chat): scroll wide markdown tables horizontally
Wrap chat markdown tables in a focusable scroll region and let the table
keep its natural width so narrow screens can scroll instead of squeezing
columns into the chat width.
2026-07-26 10:32:42 +02:00
Federico Jaramillo Martinez 095adfe550 test(ui): add WorkspaceGitPanel component-boundary tests 2026-07-24 12:30:56 +02:00
Federico Jaramillo Martinez 6db065e984 refactor(ui): dedupe git file helpers and memoize git view model
- N1: extract the copy-pasted pointerName/segmentName helpers from
  gitFileList.ts and gitFileTree.ts into gitFileShared.ts.
- N3: drop the dead conditional "tree" class (no CSS rule exists).
- N4 (P3): memoize computeViewState on (status, view) identity so renders
  from expand/collapse or diff selection skip the full model rebuild;
  expand state is read live at render time, never cached.
2026-07-24 12:09:55 +02:00
Federico Jaramillo Martinez 10ee3daa0c fix(ui): keep git view toggle stationary when expand-all appears
The right-anchored .toolbar-actions group rendered the view toggle left of
the conditional expand/collapse-all button, so the toggle jumped left
whenever the button appeared. Render expand/collapse-all first (leftmost)
so only the space to its left changes; the toggle and Refresh stay put.
2026-07-24 12:06:33 +02:00
Federico Jaramillo Martinez 63b85fc246 Merge branch 'pr-92-review' into test/pr-92 2026-07-24 10:21:29 +02:00
Federico Jaramillo Martinez 8f6688e76e fix(ui): restore in-pane minimise chevron for session warnings
Re-add the minimise chevron to the expanded session-warnings pane in
ChatView, wired to the existing unified onToggleWarnings (toggle ≡
collapse in the expanded state). The status-bar warning toggle from
a13778c is retained unchanged; both controls share the single
sessionWarningVisibility mutation, so they cannot desync.

- ChatView: onToggleWarnings prop + handleToggleWarnings; chevron
  rendered inline via html (no svg re-import), guarded by
  onToggleWarnings === undefined.
- PiWebApp: renderChatView <chat-view> passes .onToggleWarnings.
- shared.ts: restore .session-warnings-controls / -collapse / icon CSS.
- ChatView.test.ts: restore the chevron-wiring test against
  onToggleWarnings via the session-warnings-collapse marker.

Relay restore-warning-chevron leg 1.
2026-07-23 12:03:28 +02:00
lzr dd435cb1b0 feat(git): view submodule working-tree changes in the Git panel
Recurse into dirty submodules when building the Git status so their
modified and untracked files appear as full-path entries, and add a
commit-pointer entry (with short SHAs) only when the recorded commit
actually moved. Route diffs whose path falls inside a submodule to run
in that submodule's working tree so real per-file diffs are shown.

The changed-file list groups these under the submodule: tree view keeps
the nested structure and marks the submodule root with a badge, list
view flattens them into one expandable group pinned above the ordinary
files. Depth 1 only; ignored files are excluded; the panel stays
read-only.

Covered by client tree/list-grouping tests, parser tests, and a
server test that drives a real temporary repository and submodule.
2026-07-22 21:35:06 +08:00
lzr 8a5aaf93a1 feat(git): add list/tree toggle for changed files in the Git panel
Extract the Git panel into a dedicated `workspace-git-panel` Lit component
(mirroring the Files panel) and add a segmented List/Tree toggle next to
Refresh.

Tree view builds an in-memory, collapsible directory tree from the changed
files, starts fully collapsed, and offers a single expand-all/collapse-all
button (visible only in tree view). List view keeps the existing flat,
full-path rows. The selected view mode persists in localStorage under
`pi-web.gitFileView`; per-directory expand state is intentionally ephemeral.

- gitFileViewPreference.ts: localStorage-backed view preference (+ test)
- gitFileTree.ts: pure flat-paths -> nested-tree builder (+ test)
- WorkspaceGitPanel.ts: the panel component with toggle + tree state
2026-07-22 21:34:54 +08:00
Federico Jaramillo Martinez 24a3d3611e fix(ui): compact session tree indentation and allow default navigation after invalid custom input
- Only increment visual branch depth after forks so long linear session
  histories stay in one lane instead of scrolling off-screen; lower the
  max visual depth cap to match.
- Reset to the no-summary default when leaving an invalid custom summary
  choice so Navigate is never permanently disabled by a stale invalid entry.
2026-07-21 11:11:10 +02:00
Federico Jaramillo Martinez a13778c97b fix(ui): keep warning toggle visible 2026-07-21 10:43:13 +02:00
Federico Jaramillo Martinez 115d74e79a feat(sessions): persist shared unread state 2026-07-20 19:36:16 +02:00
Federico Jaramillo Martinez a20a8c8c09 fix: preserve minimised warnings across session navigation 2026-07-20 19:35:45 +02:00
Federico Jaramillo Martinez 4ca4a1d096 feat: add hierarchical session tree navigator 2026-07-20 10:40:17 +02:00
Federico Jaramillo Martinez 2c777b4959 feat: add collapsible session warnings 2026-07-19 22:39:35 +02:00
Federico Jaramillo Martinez d5650aef5b fix: scope notifications to the selected chat 2026-07-19 10:37:36 +02:00
Federico Jaramillo Martinez df8cf5a250 Merge pull request #77 from jmfederico/agent/session-notification-inbox
feat(sessions): add ephemeral notification inbox
2026-07-19 06:15:05 +02:00
Federico Jaramillo Martinez b85e1b9ebe fix: show project activity for external worktrees 2026-07-19 06:13:09 +02:00
Federico Jaramillo Martinez 71fd091e0e fix(sessions): preserve notification inbox authority 2026-07-19 02:17:59 +02:00
Federico Jaramillo Martinez 503c2c743d fix(sessions): harden notification inbox reconnects 2026-07-19 01:49:06 +02:00
Federico Jaramillo Martinez ada6f0ce1f feat(ui): add session notification tray and badges 2026-07-19 01:28:45 +02:00
Federico Jaramillo Martinez 6e09df8329 feat(sessions): add daemon notification inbox protocol 2026-07-19 01:28:45 +02:00
Federico Jaramillo Martinez 8ea3018415 fix(auth): guard post-login status refresh
Skip opportunistic status requests when the flow's originating machine is no longer selected, and discard in-flight status results after the machine or session selection changes.\n\nRefs #74
2026-07-18 21:54:26 +02:00
Federico Jaramillo Martinez 55cc1b7a3b test(auth): cover completion machine affinity
Verify that an auth flow's terminal status refresh remains targeted at the flow's originating machine after the selected machine changes.\n\nRefs #74
2026-07-18 21:18:53 +02:00
Federico Jaramillo Martinez 62396c3165 fix(auth): bind flows to their originating machine
Retain client-owned machine affinity for each interactive auth flow and use it for prompt responses, polling, cancellation, and completion refreshes. This prevents a later machine selection from forwarding secrets to a different remote.\n\nRefs #74
2026-07-18 21:09:51 +02:00
Federico Jaramillo Martinez 67f673b227 fix(auth): cancel stale started flows
Best-effort cancel a running auth flow when its start response arrives after the browser operation was closed or superseded, so sessiond does not retain orphaned provider polling or callback listeners.\n\nRefs #72
2026-07-18 20:55:28 +02:00
Federico Jaramillo Martinez d2e2d512cd Merge pull request #64 from jmfederico/fix/issue-62-authstorage
fix: migrate auth/model plumbing to ModelRuntime (fixes #62)
2026-07-18 18:20:03 +02:00
Federico Jaramillo Martinez c569a03f54 fix(auth): make API-key setup and status truthful 2026-07-18 08:28:03 +02:00
Federico Jaramillo Martinez cc8f379143 fix(auth): invalidate stale browser OAuth operations 2026-07-18 08:06:18 +02:00
Federico Jaramillo Martinez 3a208e648e fix(auth): preserve OAuth interaction semantics 2026-07-17 23:34:58 +02:00
Federico Jaramillo Martinez 50defbae58 refactor(test): remove unused template-inspection helpers
Remove the three shared template-inspection test helpers that had no
consumer after the Slice B migrations: templateStaticMarkup,
collectTemplateStrings, and collectStringValues. They were kept alive
only by a temporary @public knip shim; with no in-window test needing
them, the finish-line requirement forbids dead helpers, so remove them
outright. knip is now satisfied without the shim.
2026-07-17 22:17:09 +02:00
Federico Jaramillo Martinez 2d7ff629be refactor(test): migrate promptAttachmentCapture.test.ts to shared template-inspection seam 2026-07-17 21:47:28 +02:00
Federico Jaramillo Martinez 9a94f41a61 refactor(test): migrate ChatView.test.ts to shared template-inspection seam
Reclassify ChatView.test.ts per the testing-guide skill: move content/text/
attribute/ordering assertions to new pure public seams on ChatView.ts
(chatSessionWarningRows, chatQueuedSectionShowsClearAction, chatGroupAnchorKey,
chatEventAnchorKey, chatGroupScrollMarkerId, chatMessageGroupClassName,
chatMessageGroupLabel) with the component render code delegating to them, and
route the genuine Clear-queue/dismiss/toggle event wiring through the shared
templateInspection.testSupport helpers with escape-hatch comments. Delete the
per-file TemplateResult-inspection cluster. Drop the now-consumed @public tag
from templateEventHandlerNearMarker.
2026-07-17 21:37:30 +02:00
Federico Jaramillo Martinez 6ba215503f refactor(test): migrate ChatView image test to shared inspection seam
Replace the per-file TemplateResult inspection helper cluster in
ChatView.image.test.ts with the shared templateInspection.testSupport
escape hatch for genuine event wiring (@load re-pin, @click zoom), and
move content/attribute assertions to new pure public seams on ChatView
(chatImagePartSource, chatToolOutputLabel, chatMessageAnchorKey) that
the component's own render code now delegates to.
2026-07-17 21:15:35 +02:00
Federico Jaramillo Martinez d02bbfa545 refactor(test): migrate WorkspaceFilesPanel test to shared template-inspection seam
Route genuine Lit event-wiring (upload input change, form submit, file-tree
row clicks) through the shared templateInspection.testSupport escape hatch and
add the required proportionality comment. Move viewer content messaging
(empty/loading/binary states) to a new public workspaceFileViewerStatusLabel
seam on the component instead of scraping Lit markup for text. Delete the
per-file inspection helper cluster; drop @public from the two shared helpers
that now have a real importer.
2026-07-17 21:06:12 +02:00
Federico Jaramillo Martinez e7926e173b refactor(test): route SettingsDialog panel test through a public seam
Replace the SettingsDialog.general.test.ts markup-scraping assertion (which
scraped the rendered TemplateResult for '<settings-general-panel', 'scope-note',
'This tab edits:') with an exported pure routing seam activeSettingsPanelTag()
and assert the section->panel contract directly. Delete the now-unused
collectTemplateStrings helper and its private template-reflection cluster from
SettingsDialog.testSupport.ts.

The .packages/.plugins/.sessiond siblings inspect no TemplateResult internals
(orchestration reflection only), so no migration was needed for them.
2026-07-17 20:48:17 +02:00
Federico Jaramillo Martinez 946f7612ae refactor(test): move SettingsSessiondPanel assertions to a public seam
Migrate the prime Finding-2 offender off Lit TemplateResult scraping. The
panel's dynamic notice-stack logic is extracted into an exported pure
sessiondPanelNotices() (plus SessiondPanelNoticeContext) and the existing
sessiondDescription is exported, so the test asserts notice
composition/ordering and the scope description through public seams instead of
flattening rendered template internals. Save and draft-preservation behavior
are observed via the injected onSave callback and public state.

Removes all per-file template-inspection helpers and the escape-hatch need for
this file (testing-guide Findings 1-3).
2026-07-17 20:41:49 +02:00
Federico Jaramillo Martinez 55bc567b7b refactor(test): add shared template-inspection test helper (Slice A)
Consolidate the duplicated, per-file Lit TemplateResult inspection helper
cluster into one shared, strictly-typed, type-guarded seam at
src/client/src/templateInspection.testSupport.ts, documented as the
testing-guide escape hatch. Migrate PiWebApp.clearQueue.test.ts to it as a
proof and delete its local copies. Tag not-yet-consumed public entry points
with @public so knip does not flag them until Slice B adds consumers.

Relay: testing-skill-compliance leg 1 (Finding 1).
2026-07-17 20:23:21 +02:00
Federico Jaramillo Martinez aedcbf885e feat(sessions): surface live session startup warnings in the web UI
Show a pinned banner at the top of the session view with resource and
runtime diagnostics (skills, prompts, themes, extension load errors) plus
the Anthropic subscription-auth billing notice, recomputed live from the
current runtime so they stay accurate across browser reloads.

Warnings carry an optional dismiss capability; the Anthropic notice is
dismissable and durably suppressed through pi's own anthropicExtraUsage
warning setting. Also fixes the testing-guide skill frontmatter so it
loads.
2026-07-17 19:16:58 +02:00
Federico Jaramillo Martinez 2b17145291 feat(sessions): stream in-flight partial when joining a mid-turn session
Seed the in-flight partial assistant message (text, thinking, and
in-progress tool calls) when opening or reconnecting to a session that is
mid-stream, then continue streaming live deltas on top of it. Replaces the
blocking "Catching up..." placeholder and the end-of-turn transcript reload.

Server stamps every per-session UI event with a monotonic seq at the
SessionEventHub publish choke point and exposes
GET /sessions/:sessionId/stream-snapshot returning { seq, partial }. The
client fetches the snapshot on join, seeds the normalized partial into the
in-memory transcript (never the history cache), and applies buffered/live
events using the seq watermark for exactly-once delivery.

The snapshot is a progressive enhancement: a 404 from an older remote
pi-web or a not-yet-restarted session daemon falls back to an empty seed
(seq 0, drops nothing), so sessions still open and stream normally. The
stream-snapshot route is registered in the federation allowlist for
remote-machine proxying.
2026-07-17 14:49:56 +02:00
Federico Jaramillo Martinez ae9eaf3082 Match image zoom close button to app button styling 2026-07-17 11:15:05 +02:00
Federico Jaramillo Martinez 6cd666f9b8 Add click-to-enlarge modal viewer for chat images 2026-07-17 09:52:54 +02:00