Commit Graph
789 Commits
Author SHA1 Message Date
Federico Jaramillo Martinez 69b125b001 feat(sessions): surface cross-worktree parent and child sessions
A session spawned into another worktree recorded a parent that no
listing contained, so the row showed only "parent unavailable" and its
parent's row looked childless. Both facts were accurate and useless:
neither said where the related session actually was.

Report both directions from the session store instead. A missing
parent's cwd and id come from its own file header, so one 4 KB read per
distinct missing parent resolves it without listing other workspaces;
children are counted by listing sibling workspaces and matching the
parent path they already recorded, needing no header reads. Reads are
memoized per path because Pi writes headers once, and the cache is
released on dispose. Both directions are best-effort: an unreadable
header or an unlistable worktree leaves a session unannotated rather
than failing the listing.

In the browser, an orphan child keeps the same child marker as a nested
one, dimmed, so it no longer renders as a root; whereabouts are stated
once on the meta line ("parent in feature/foo", "2 children
elsewhere"), where a clamped title cannot hide them. A "Go to parent
session" action switches to the owning workspace and selects the
parent. Live session.created events keep child counts current instead
of leaving them stale until the next listing.

Session and workspace paths reach the browser from two producers: store
enumeration for a listing, and the live runtime for a broadcast. They
are now compared through one normalizing helper, so tree nesting and
child counts cannot silently miss a link when only a trailing separator
differs.

Extract the shared "workspaces of the project containing this cwd"
lookup out of ProjectScopedSpawnTargetResolver so spawn targeting and
child counting share one implementation, and register it regardless of
whether spawning is enabled: children can predate a config change, and
the tree should stay honest about them either way.
2026-07-28 11:56:21 +02:00
Federico Jaramillo Martinez a4f513dbd1 docs(test): record happy-dom as the standard Lit component test harness
The harness landed in 4973ada and spread to 11 component test files
without the guide mentioning it, while relays cited the guide as
sanctioning it. Document the per-file @vitest-environment opt-in, the
seam-selection order (pure exported seam, happy-dom, TemplateResult
extraction), happy-dom conventions and limits, and the opportunistic
migration stance for existing extraction tests.
2026-07-27 20:42:58 +02:00
Federico Jaramillo Martinez 4a5150309d feat(ui): add copy buttons to workspace menu details 2026-07-27 19:31:15 +02:00
Federico Jaramillo Martinez 8517800a24 feat(plugins): turn Info panel into a PI WEB status view
Rework the bundled Info plugin from a demo into an always-available
status view: running and installed versions, installation details,
release state, and per-service health rendered from host-provided
state, plus machine and workspace details. Replace the window.alert
demo action with a Copy PI WEB Diagnostics action that copies a
plain-text summary for bug reports.

Add state.selectedMachine to the stable plugin runtime state so
actions and other runtime callbacks can read the selected machine's
identity, and split the plugin into a copy-paste-friendly skeleton
(pi-web-plugin.ts) and replaceable internals (infoInternals.ts).
2026-07-27 18:03:25 +02:00
Federico Jaramillo Martinez 0a35748478 Merge pull request #105 from jmfederico/feat/unread-ux
feat(ui): improve unread-messages experience
2026-07-27 15:58:55 +02:00
Federico Jaramillo Martinez f76a9fabc8 feat(ui): unify row indicators into one mark with an unread ring
Unread is no longer a competing ActivityIndicatorKind or a separate
name-adjacent dot: every row renders a single indicator. An accent ring
wraps the still-pulsing work dot when a row is both busy and unread, a
filled accent dot shows while idle and unread, and activity kinds keep
their existing precedence (sending > session > terminal). Session rows
surface unread state even while busy, so the unread header count and
mobile Sessions badge now count busy unread sessions too.
2026-07-27 15:55:06 +02:00
Federico Jaramillo Martinez 8172defca3 Merge pull request #104 from jmfederico/fix/session-list-scroll-reveal
fix(ui): scroll session/workspace lists only on positive reveal triggers
2026-07-27 15:46:50 +02:00
Federico Jaramillo Martinez f2307b8c62 test(ui): explain the double updateComplete await in WorkspaceList scroll tests 2026-07-27 15:41:32 +02:00
Federico Jaramillo Martinez 8af637b00e feat(ui): bubble unread presence up to workspace, project, and machine rows
Wire the derived UnreadPresence into dot indicators (no counts) across the
navigation panel: workspace, project, and machine rows (machine-switcher and
machine-list) now show a static accent dot whenever a session beneath them is
unread, including offline machines with stale-but-present state. Presence
flows PiWebApp -> AppNavigationPanel -> leaf id sets, mirroring the
unreadSessionIds chain, and is covered by happy-dom component tests per list
plus panel- and app-level wiring tests. Adds changesets for the mark-as-read
actions and the bubble-up indicators.
2026-07-27 15:08:31 +02:00
Federico Jaramillo Martinez 76f292cfde fix(ui): scroll session/workspace lists only on positive reveal triggers
Live data refreshes (messageCount churn from session status publication,
workspace topology refreshes) replace the sessions/workspaces arrays and
the selected object with same-id copies, and each replacement re-scrolled
the selected row into view.

Replace the broad updated() scroll triggers with positive reveal triggers:
a different row becoming selected (first render included), the archived
reveal path, a restore moving the selected row from archived back to
current (same id, archived flag cleared), and section expansion. The
sessions/workspaces array triggers are removed, not guarded.
2026-07-27 14:46:25 +02:00
Federico Jaramillo Martinez 9267174736 feat(ui): derive bubble-up unread presence from unread projections
Add pure unreadPresence helpers mapping unread summary cwds to machine,
project, and workspace presence (machine = any summary, workspace = exact
path match, project = known workspace ownership), plus a PiWebApp seam
that keeps the derived UnreadPresence state current on any machine's
projection change and on app-state transitions. No UI consumption yet.
2026-07-27 14:41:40 +02:00
Federico Jaramillo Martinez b4a7a9230b feat(ui): add mark-as-read actions for unread sessions
Add a "Mark as read" item to the session row menu (shown only for
unread, non-archived, non-transient sessions) and a bulk "Mark read"
action to the current-selection toolbar (enabled when any selected
session is unread). Both flow through AppNavigationPanel to PiWebApp,
which acknowledges via SessionUnreadController with the exact observed
completion order.
2026-07-27 14:22:17 +02:00
Federico Jaramillo Martinez 0a080f4390 test(sessions): cover unread cleanup across all archive paths
Relay unread-ux leg 1: audit verified every archive path (single, bulk,
tree, cleanup, restore, delete, rebind) clears unread state in the right
order with no resurrection vector. Add regression tests for the uncovered
paths: bulk archive, archive with descendants, cleanup, bulk delete, and
archiving an active session with a pending activity latch.
2026-07-27 13:59:29 +02:00
Federico Jaramillo Martinez 4191d526f4 Merge pull request #103 from jmfederico/feat/model-questions-ux
feat(sessions): add ask_user question forms
2026-07-27 13:01:59 +02:00
Federico Jaramillo Martinez 80494dd6d8 fix(ui): prevent iOS zoom on custom answers 2026-07-27 12:42:19 +02:00
Federico Jaramillo Martinez e567d43042 feat(settings): add Ask Questions toggle 2026-07-27 12:39:40 +02:00
Federico Jaramillo Martinez 0e83146315 fix(ui): streamline ask_user question forms 2026-07-27 10:55:49 +02:00
Federico Jaramillo Martinez 4e47c23f23 Merge remote-tracking branch 'origin/main' into feat/model-questions-ux
# Conflicts:
#	src/server/sessiond.ts
#	src/server/sessions/sessionRoutes.test.ts
2026-07-27 09:44:40 +02:00
Federico Jaramillo Martinez ce4b469727 feat(ui): add model and thinking selector actions 2026-07-27 09:40:24 +02:00
Federico Jaramillo Martinez 9191f59146 docs: document ask_user question forms 2026-07-27 01:17:48 +02:00
Federico Jaramillo Martinez 7bbf5aa73c feat(client): project ask_user transcript records 2026-07-27 01:09:28 +02:00
Federico Jaramillo Martinez 4973adaa80 feat(client): render ask_user question cards 2026-07-27 00:52:47 +02:00
Federico Jaramillo Martinez 862ae73fcb feat(client): carry the open ask_user question set into browser state
Parse the daemon-owned `pendingAsk` in `parseSessionStatus` and validate
`ask.opened` / `ask.closed` frames rather than accepting them on their type,
since they drive a form the user answers on the model's behalf.

Add `submitAsk` / `cancelAsk` through `request()` + `sessionPath()`, both
returning the recomputed session status so closing an ask needs no follow-up
status request, and derive `pendingAsk` in `SessionController` from status plus
live events with `sessions.askUser` capability gating.

New `askDrafts.ts` keeps what the user has typed in browser-local storage under
`pi-web:ask-draft:<sessionId>:<askId>` and owns the pure answer-state helpers,
so the daemon owns "there is an open ask" and the browser owns "what I have
typed so far".
2026-07-26 23:26:43 +02:00
Federico Jaramillo Martinez e502d569a9 test(sessiond): make the concurrent-refresh note's wiring verifiable
One optional line in sessiond.ts passed the model catalog refresher into
PiSessionService, and it was the only thing making "· provider model lists are
refreshing" reachable in the product. Deleting it left typecheck, lint, Knip,
and all 1860 tests green while the note silently disappeared — confirmed by
actually deleting it and running each check. sessiond.ts starts a daemon as an
import side effect, so nothing in it could be tested.

Extract the dependency assembly into a pure sessionServiceDependencies()
function, following the sessionDaemonStartup.ts precedent, and assert what a
user is actually told: a service built by the real assembly, with a refresh in
flight, reports the note on its startup phases. A companion test pins the note
to the refresher's answer so the first cannot pass on unconditional wording.

Only the object literal moved. Everything createRuntime() constructs before it
stays in place and in order — the provider freeze must still precede any real
session, for the reason its comment gives — and the extracted function performs
no side effect, so construction order, side effects, and routes are unchanged.

The wiring is now guarded at both hops. Dropping the field from the assembly
fails the new test; dropping it from sessiond.ts fails typecheck, because the
assembly's input type requires every collaborator the daemon constructs.
PiSessionServiceDependencies.catalogRefreshStatus stays optional, so the 98
existing service constructions in the suite are untouched, and the refresher's
getter stays read-only: the test injects a fake in-flight status rather than
touching its cadence, timeout, or coalescing.

subsessionsEnabled's spawn-capability conjunction moved into the assembly with
the literal it lived in. The semantics are identical, and it is now covered by a
test instead of being another untested decision in an untestable file.
2026-07-26 23:15:09 +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 51ebfe4c00 feat(sessions): deliver ask_user answers
Integrate the pending-ask store into the session service so an open ask is
visible, observable, and closable.

- `statusFromSession` projects `pendingAsk`, so a browser rehydrates an open
  ask from `GET /sessions/:sessionId/status` after reload or a web/API restart.
- `openAsk` publishes `ask.opened`, and publishes `ask.closed` first when the
  new ask supersedes an unanswered one.
- `submitAsk` / `cancelAsk` close the ask and hand the outcome to the model as
  a `pi-web.ask.answers` follow-up custom message (`triggerTurn`,
  `deliverAs: "followUp"`), the same delivery subsession notices use. A stale
  ask id is reported, not thrown: losing the race against a supersede or
  another browser is ordinary. Cancel still reports every question as
  unanswered so the model is not left waiting for a promised message.
- The open ask is forgotten when its runtime closes; nothing is left to
  receive the answers.
- `POST /sessions/:sessionId/ask/{submit,cancel}` behind the existing
  `/api/sessions/*` daemon proxy, allowlisted for machine federation.
2026-07-26 22:38:15 +02:00
Federico Jaramillo Martinez cd1326a8ca Merge pull request #102 from jmfederico/feat/worktree-autodetect
feat(workspaces): detect worktrees created or removed outside PI WEB
2026-07-26 22:32:00 +02:00
Federico Jaramillo Martinez c9fabbf1b6 chore: untrack the relay working notes
.pi-web/ is gitignored local state; the relay packet was force-added while
running the work and should not land on main. The notes stay on disk for the
worktree, they are just no longer tracked.
2026-07-26 22:25:59 +02:00
Federico Jaramillo Martinez ba37bd51ae docs(relay): record review nits 2 and 3 2026-07-26 22:25:59 +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 9848fc3e40 refactor(workspaces): drop the unused locked worktree flag
Nothing consumed GitWorktreeInfo.locked: a locked worktree is a real checkout
and stays a usable workspace, so no caller needs to distinguish it. Unknown
porcelain keys are already ignored, so parsing it was speculative.

The parser test still feeds a real `locked` line and asserts it is ignored, and
the service test still pins that a present, non-prunable worktree is kept, so
the policy stays covered without the field.
2026-07-26 22:25:59 +02:00
Federico Jaramillo Martinez d7fc25312d docs(relay): record the post-relay selected-workspace freshness fix 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 36d4e9a296 docs(relay): record worktree-autodetect leg 3 and relay completion 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 1970ba14bc docs(relay): record worktree-autodetect leg 2 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 e12e5d16c6 docs(relay): record worktree-autodetect leg 1 2026-07-26 22:25:59 +02:00
Federico Jaramillo Martinez 4eca5fe6f2 fix(workspaces): hide worktrees whose checkout directory is gone
git worktree list keeps reporting a linked worktree after its directory is deleted
outside PI WEB, marking it prunable. The workspace list showed those as normal
selectable workspaces. The porcelain parser now reads the prunable and locked keys,
and WorkspaceService filters prunable linked worktrees out while always keeping the
project's own path. Listing stays read-only; git worktree prune is never run.
2026-07-26 22:25:59 +02:00
Federico Jaramillo Martinez 8a3fe0daa6 docs(relay): add worktree-autodetect relay packet
Assessment of auto-detecting worktrees created outside PI WEB. Worktree
discovery is already derived per request with no cache and no registry, so
the gap is that the browser never re-lists. Recommends the reduced scope:
filter prunable worktrees, add a non-disruptive topology refresh, and call
it from the existing browser-resume path. No watchers, timers, processes, or
push channels.

Packet is repo-only and outside the published files allowlist, so no
changeset is needed.
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 07bdd7ad6b feat(sessions): add the ask_user tool
Register a core ask_user custom tool that posts a question set to the user's
browser and terminates the run instead of awaiting an answer. The tool is thin:
it shapes its TypeBox params into domain questions, lets PendingAskStore own
validation, and reports a superseded unanswered ask back to the model.

Gated by the askUser config key, threaded through PiSessionServiceDependencies
and sessiond. Unlike the delegation tools, ask_user is available to tracked
children too: the questions reach the user of the asking session.
2026-07-26 22:01:15 +02:00
Federico Jaramillo Martinez 49393e1cb9 feat(sessions): add the pending ask store
Own the one-open-ask-per-session lifecycle in daemon-side domain logic: validate
model-authored question sets, validate submitted answers against them, and
compute the answered-versus-unanswered outcome both the model-facing follow-up
message and the browser record are rendered from.

Also lands the answer half of the shared ask contract alongside its first
consumer.
2026-07-26 21:45:51 +02:00
Federico Jaramillo Martinez 6fa57b524b feat(config): add askUser capability and shared ask contract
Introduce the shared contract for the upcoming ask_user tool: question and
pending-ask types in the API contract, the pendingAsk field on
SessionStatus, ask.opened/ask.closed session UI events, the askUser global
config key with a PI_WEB_ASK_USER env override, and the sessions.askUser
capability requiring both the web and session daemon runtimes.

askUser defaults to true: the questions land in the session the user is
already watching and nothing happens until they act, unlike the beta-off
subsessions flag.
2026-07-26 21:26:23 +02:00
Federico Jaramillo Martinez 2a05d67436 test(sessions): pin the baseline rebase and invalid catalog refresh
Two behaviors of the narrowed provider freeze were unprotected: deleting
the baseline rebase, or swallowing Pi's validation error on the accept
path, both left the suite green.

Add a replay test asserting one applied update and one de-duplicated
ignored entry across four identical registrations, which is what a
per-session session_start handler produces. This fails if the rebase is
removed, because every replay then differs from the original catalog and
is accepted forever.

Add a test for a provider whose models carry their own api/baseUrl, so a
refreshed catalog omitting them fails validation. It pins that the error
reaches the extension rather than being silently swallowed, and that the
recorded baseline and previously registered models survive.

Note that a poisoned-baseline reordering is deliberately not asserted:
recording the incoming config early only corrupts `models`, and models
are expected to differ, so no later comparison can observe it.
2026-07-26 17:32:42 +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 36d67262ec docs(config): describe the model list refresh exception
The Pi extension provider baseline section stated that every later
provider registration is a no-op, naming session_start as an example.
That is now the exact case that is allowed, so the shipped configuration
reference contradicted the behavior. docs/config.md is in the package
files allowlist, so this text reaches users.

Describe what stays frozen, the two conditions under which a known
provider's model list refresh is applied, and why function-valued fields
are always rejected. Record the accepted trade-offs: catalogs are shared
daemon-wide state with last-registration-wins, and a model entry's own
baseUrl/headers take precedence over the provider-level values. Split
the log behavior out and note that ignored mutations are de-duplicated
per provider while applied refreshes are always logged.

Also correct the reload-behavior bullet: a restart is needed to add or
remove a provider or change its connection settings, but not for a known
provider refreshing only its model list.
2026-07-26 14:14:30 +02:00
Federico Jaramillo Martinez 531ccf7b2a feat(sessions): let a known provider refresh its own model list
The global provider bootstrap froze all three ModelRuntime mutation
methods after startup, so a provider extension that fetched an updated
model catalog had that work silently discarded.

registerProvider is now applied when the provider ID is already in the
frozen baseline and the incoming config equals the recorded baseline in
every field except `models`. Refreshing extensions re-send a complete
provider config rather than a models-only delta, so the test is
"equal except models", not "contains only models".

Everything else stays a logged no-op: unknown provider IDs, any change
to name/baseUrl/apiKey/api/streamSimple/headers/authHeader/oauth/
refreshModels, native registration, and unregistration. Function-valued
fields compare by reference and so always read as a mismatch, which is
the intended conservative direction.

An accepted update rebases the stored baseline from Pi's merged record,
so repeat refreshes work and an unchanged replay is correctly ignored
rather than re-applied on every session start. The accept path stays
synchronous and never awaits or networks; Pi's own trailing
fire-and-forget local refresh is untouched.
2026-07-26 14:14:07 +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