diff --git a/README.md b/README.md index da35178..f45e6e5 100644 --- a/README.md +++ b/README.md @@ -1362,7 +1362,7 @@ same action on that session. The presentations are: restores the warning. A live request that is no longer pending does not regain the warning while its old preview awaits a refresh. Unsupported requests can still require action in Codex. - The selected session also offers a **FOLLOW-UP** composer here when the shared + The selected session also offers a follow-up composer here when the shared server confirms it is ready for input and the complete context plus composer fit. Only one inline composer is available at a time. Click its input line or press `Enter` to focus, then `Enter` to send; `Esc` leaves the editor without @@ -1380,6 +1380,29 @@ same action on that session. The presentations are: full-width detail. When a reply editor is available, `Enter` focuses it instead of changing presentation. + **Recent turn history:** `Alt+Left` opens the previous completed turn; + `Alt+Right` moves forwards, eventually returning to live detail. Click + **Previous**, **Next**, or **Live** for the same navigation. These controls + appear when replies have been retained and space permits; narrow layouts + shorten the labels, and the hotkeys remain available on short terminals. + Up/Down, Page Up/Down and the wheel still scroll the displayed reply; Copy + copies that reply. A historical answer stays pinned while queued work runs, + even if newer completions move it outside the recent-history cache. + Each session retains up to ten bounded prompt/reply excerpts in memory, + plus an answer currently being read. Full detail refreshes recent completed + turns through the shared app-server's read-only `thread/turns/list` endpoint, + recovering quick answers missed between telemetry updates and allowing + history to reload after restarting. Older/unavailable servers fall back to + replies actually observed during this launch; that fallback can miss fast + turns. Codexometer does not write conversation history to disk. + Approval controls and the composer always act on the **live session**, + never a saved turn. Current requests are shown separately under **LIVE + SESSION** while browsing history; focusing the composer returns to live + detail and retains your draft. + History reads request only the server's prompt/final-reply summary items, + not full command-output logs, and run asynchronously at most every five + seconds while full detail is visible. + Whenever visible **LAST REPLY** or **LAST ACTIVITY** text is available, its box offers **[ (C)OPY ]** in the bottom-right border (split, expanded and full detail, when space permits). Click it to copy a snapshot of that session's observed text, including offscreen text, @@ -1445,7 +1468,9 @@ status-coloured inversion, and both marker cells are clickable. Sessions without a pill do not cause another session to be marked. Overview pills remain unchanged, without markers or reserved marker space. The strip chooses the least compressed format that -fits all pills: full labels, omit `//`, omit directories, then shorten Turn Complete +fits all pills: full labels, omit `//`, progressively shorten the longest session +names (or directory fallbacks) with `…`, omit names only when useful excerpts +cannot fit, then shorten Turn Complete to **DONE** (translated). The short session ID is always retained. If the compact labels still do not fit, the strip uses overflow paging; unusually long translated states are shortened only as needed. Expanding the terminal restores fuller labels. @@ -1500,22 +1525,88 @@ submit. Text wraps automatically and the editor grows upward, reducing the scrollable context area above it. Once it reaches the available height, the editor scrolls internally without truncating the draft; deleting text shrinks it again. Enter still submits rather than inserting a newline. This is a text -editor, not the Codex slash-command UI. Secret answers retain a single-line +editor, with the catalogue-backed slash-command subset described below. Secret answers retain a single-line masked password field. +**Live slash commands (shared app-server):** type `/` in the ordinary composer +to see a vertical popup with supplied help. Typing `/mo` filters to matching commands; +the popup shrinks as candidates drop out without moving the composer. Longer +lists scroll within the available space (arrows or mouse wheel in the terminal). +Up/Down selects, Tab completes the name, Enter opens its options and Escape +dismisses suggestions while retaining the draft and typing focus. Suggestions +are fetched asynchronously and filtered locally. They also work in a selected +wide terminal session row when space permits. Outside the full-detail composer, +press `/` to open the full command browser. Use arrows and Enter to browse, **C** to confirm a +setting change, Left to go back and Escape to return to the session. Buttons +are clickable. Writable web mode exposes the same catalogue through +**/ COMMANDS**, with the same composer suggestion shortcuts. + +| Command family | Live options and behaviour | +| --- | --- | +| `/model` | Advertised models, their descriptions and supported reasoning levels. Choosing a model resets its explicit speed override to the server default. | +| Advertised speed commands, such as `/fast` | Names, IDs and descriptions come from the current model's service-tier catalogue; enable a tier or clear the explicit override. No speed names or tier IDs are assumed. | +| `/plan` | Advertised collaboration modes, using Codex's built-in mode instructions. | +| `/permissions` | Named profiles allowed for the session's directory, with supplied descriptions. Review carefully: this changes future permission behaviour. | +| `/skills`, `/apps`, `/mcp`, `/hooks`, `/experimental` | **Browse-only** live inventories and supplied help, including MCP tool descriptions. Invoke/configure these through Codex. | +| `/help` | Available command families. Unsupported or empty catalogues are omitted. | +| `/statusline` | **Local Codexometer multi-select:** choose and reorder detail-footer fields with a live preview. Works without the shared daemon. | + +The detail footer uses theme-highlighted text in the bottom border in terminal mode, and +the last line of full session detail on web. Its default fields are observed +model/reasoning, speed, total session tokens (including linked agents) and +directory. `/statusline` also offers model alone, reasoning alone, session name, +full session ID, observed state, linked-agent count and context source. Missing +values are omitted. This is Codexometer's supported field set; the Codex CLI's +own client-side picker is not dynamically exposed by app-server. + +In the terminal, **Space/Enter** toggles a field, **Left/Right** changes its order, +**C** applies and **Escape** cancels. Selecting no fields hides the footer. Web +uses checkboxes, ordering buttons and Apply/Cancel, also available in read-only +mode through the `/statusline` footer button. Selections persist in Codexometer's +existing `preferences.json` on terminal and browser local storage on web; these +are display preferences and do not modify Codex CLI configuration. Terminal +footer text shortens to fit and leaves the Copy button in place. A blank row +separates a visible terminal status line from the global footer; very short +terminals omit the status line and gap to preserve the composer and controls. + +Codex does not expose a general slash-command discovery/execution API. These are +explicit adapters to supported app-server catalogues, not a reproduction of +every CLI command. Options and help are fetched on demand, not hard-coded or +persisted; installed-server support determines what appears. Plugin lifecycle +endpoints are deliberately excluded while OpenAI marks them unsuitable for +production clients. Experimental flags are browse-only because their mutation +scope is process-wide rather than session-local. See the official +[app-server API overview](https://learn.chatgpt.com/docs/app-server#api-overview) +for the upstream catalogues. + +Codex session-setting changes require a loaded, idle session and explicit confirmation within +30 seconds. The command and options are revalidated before sending; stale +connections, pending approvals and changed options fail closed. A successful +request is an acknowledgement, not a claim that a subsequent turn has already +used the setting. Successful confirmations close the menu and return focus to +the composer with a success notice; reopening fetches fresh options. Failed +confirmations retain the draft and error context. These changes do not modify global defaults, and automatic +quota thresholds may later supersede model settings. Uncertain requests are +not retried automatically. Read-only web mode offers no command-control endpoint. + +Slash commands cannot be sent, steered, queued or scheduled as ordinary prompts. +Use `//` to send a literal leading slash (for example `//tmp/file` sends +`/tmp/file`). Structured question answers are unaffected. API help is shown in +the language supplied by Codex; new command-browser explanatory text is English. + In terminal **full detail**, or the **selected wide detail row** when its context and composer fit, a live **WORKING** turn also offers a composer once its turn ID has been observed. Only the selected row accepts input; switching sessions retains a separate in-memory ordinary draft for each session; returning restores it without sending or focusing the editor. Drafts are lost on exit; secret/question answers and in-flight submissions are not saved as drafts. -The full-detail border and composer heading show the short session ID and name +The full-detail border shows the short session ID and name (directory fallback for unnamed sessions); full IDs remain in secondary metadata. Switching between wide-row and full-screen detail for the same session preserves the draft and releases typing focus; click the composer or press Enter -to continue editing. The animated progress dots sit directly above -the input. While the editor is focused, **Enter steers the current turn**, +to continue editing. The animated progress dots sit above FOLLOW-UPS when the +queue is visible, or directly above the input otherwise. While the editor is focused, **Enter steers the current turn**, **Tab queues the message for the next turn**, and **Esc requests interruption** without discarding the draft. **Ctrl+C** leaves the editor without interrupting; Esc outside the editor keeps its normal back-navigation behaviour. Once idle, @@ -1536,10 +1627,15 @@ when they run. The panel remains visible during work and ordinary approvals, above the approval controls when those replace the composer. Quota-threshold reviews remain isolated. Empty panels disappear unless the queue is unavailable; wide session rows show one preview at a time, while full detail shows more entries as space permits. +Successfully sent steering also appears here as **STEER SENT** until the live +**LATEST GUIDANCE** catches up (or a positively newer turn supersedes it). +These are read-only receipts, not queued work: +they have no edit or cancel buttons, survive switching sessions within this run, +and are never resent automatically. Use the mouse wheel over the panel, or **Alt+Q** then **↑/↓**, **Page Up/Down**, to browse. **Enter/E** edits, **X/Delete** requests deletion, and **Esc/Tab** leaves -queue keyboard focus. Each entry also has clickable **EDIT** and **×** buttons; +queue keyboard focus. Editable entries also have clickable **EDIT** and **×** buttons; native deletion requires confirmation. Scheduled **NOW** retains its separate confirmation. Editing a native message does not pause Codex's queue. Its original ID and contents are rechecked; an already-started/deleted message is never recreated. @@ -1724,7 +1820,9 @@ must be enlarged. These diagnostics do not relax any approval safeguards. The readout's `[ H: HIDE DETAIL ]`/`[ H: SHOW DETAIL ]` button or `h` toggles all previews (shortened to `[H:HIDE]`/`[H:SHOW]` in narrow layouts); the choice -survives restarts. No excerpt is saved. Each retained excerpt is capped at 4,096 +survives restarts. No excerpt is saved to disk by Codexometer. Recent-turn browsing +uses a bounded in-memory cache and read-only app-server history where available. +Each retained excerpt is capped at 4,096 Unicode characters; startup reads only a bounded 256 KiB rollout tail, so older context can be unavailable. Terminal escapes and control/bidirectional-formatting characters are stripped. Previews remain in their original language and are not @@ -2555,8 +2653,13 @@ history is hidden unless its account matches the current successful quota read. ### Scheduled session follow-ups (experimental) -In **Sessions → full detail**, use **Ctrl+S** (or click the composer's -schedule heading) to schedule a follow-up for an idle shared app-server session. +In **Sessions → full detail**, use **Ctrl+S** (or click **Schedule** on the +composer's key-hint line) to schedule a follow-up for an idle shared app-server session. +Ordinary composers omit a redundant heading; the box title identifies the +session. Active turns retain animated dots above the input (above FOLLOW-UPS +when the queue is visible), while structured +questions retain their question heading and answer progress. Schedule/Close +Schedule buttons keep their full labels when space allows, truncating hints first. The form replaces the conversation area inside the existing detail pane, not the dashboard. The composer stays anchored below it; on shorter terminals the form scrolls with the mouse wheel and follows keyboard focus. Esc or Ctrl+S @@ -2591,7 +2694,7 @@ theme-coloured background. the trigger restores the underlying status. Full detail retains the saved prompt and trigger information. The full-detail border shows the current state; the body uses content labels - such as **FOLLOW-UP** and **LAST REPLY** rather than repeating that state. + such as **LAST REPLY** rather than repeating that state. Its terminal attention pill comes after TURN COMPLETE, at the lowest priority. Click the pill or the label in the session's left-hand box to open that session's detail page with the saved prompt, timing and actions; web provides equivalent diff --git a/internal/codex/live_usage.go b/internal/codex/live_usage.go index b28d397..4b4400c 100644 --- a/internal/codex/live_usage.go +++ b/internal/codex/live_usage.go @@ -1231,6 +1231,8 @@ func (r *LiveUsageReader) sessionSnapshots(now time.Time, liveWriters map[string // final message. Keep its reply, but retain observed prompt // context only for the same positively identified turn. preview.CurrentTask, preview.LatestGuidance = live.CurrentTask, live.LatestGuidance + preview.LatestGuidanceID = live.LatestGuidanceID + preview.TurnID = live.TurnID } } // A persisted request is not proof that it is still outstanding. Match diff --git a/internal/codex/session_commands.go b/internal/codex/session_commands.go new file mode 100644 index 0000000..8074cd6 --- /dev/null +++ b/internal/codex/session_commands.go @@ -0,0 +1,68 @@ +package codex + +import ( + "context" + "errors" + "strings" +) + +// Command catalogues are interface-neutral, live and memory-only. Front ends +// render metadata; they never manufacture RPC methods or permission profiles. +type SessionCommandChoice struct { + ID string `json:"id"` + Label string `json:"label"` + Help string `json:"help"` + Next string `json:"next,omitempty"` + Action bool `json:"action,omitempty"` + Selected bool `json:"selected,omitempty"` + method string + params map[string]any +} + +type SessionCommandMenu struct { + Title string `json:"title"` + Help string `json:"help"` + Path string `json:"path"` + Revision string `json:"revision"` + Multiple bool `json:"multiple,omitempty"` + Choices []SessionCommandChoice `json:"choices"` +} + +type SessionCommandsClient interface { + SessionCommands(context.Context, string, string) (SessionCommandMenu, error) + ExecuteSessionCommand(context.Context, string, string, string, string) error +} + +var ErrSessionCommand = errors.New("command unavailable or session/options changed; reopen the command menu") + +func IsSessionCommand(text string) bool { + text = strings.TrimSpace(text) + return strings.HasPrefix(text, "/") && !strings.HasPrefix(text, "//") +} + +func literalSessionText(text string) string { + if strings.HasPrefix(strings.TrimSpace(text), "//") { + return strings.Replace(text, "//", "/", 1) + } + return text +} + +func (c Client) commandClient() SessionCommandsClient { + if c.LiveUsage != nil { + p, _ := c.LiveUsage.statusProvider.(SessionCommandsClient) + return p + } + return nil +} +func (c Client) SessionCommands(ctx context.Context, id, path string) (SessionCommandMenu, error) { + if p := c.commandClient(); p != nil { + return p.SessionCommands(ctx, id, path) + } + return SessionCommandMenu{}, ErrSessionCommand +} +func (c Client) ExecuteSessionCommand(ctx context.Context, id, path, revision, choice string) error { + if p := c.commandClient(); p != nil { + return p.ExecuteSessionCommand(ctx, id, path, revision, choice) + } + return ErrSessionCommand +} diff --git a/internal/codex/session_commands_test.go b/internal/codex/session_commands_test.go new file mode 100644 index 0000000..99cc864 --- /dev/null +++ b/internal/codex/session_commands_test.go @@ -0,0 +1,19 @@ +package codex + +import "testing" + +func TestSessionCommandLiteral(t *testing.T) { + for _, tc := range []struct { + text string + command bool + literal string + }{ + {"/model", true, "/model"}, {" /unknown x", true, " /unknown x"}, + {"//tmp/example", false, "/tmp/example"}, {" //help", false, " /help"}, + {"hello /model", false, "hello /model"}, {"", false, ""}, + } { + if IsSessionCommand(tc.text) != tc.command || literalSessionText(tc.text) != tc.literal { + t.Fatalf("unexpected classification/escape: %q", tc.text) + } + } +} diff --git a/internal/codex/session_commands_unix.go b/internal/codex/session_commands_unix.go new file mode 100644 index 0000000..dfb7819 --- /dev/null +++ b/internal/codex/session_commands_unix.go @@ -0,0 +1,442 @@ +//go:build unix + +package codex + +import ( + "context" + "crypto/sha256" + "encoding/json" + "fmt" + "net/url" + "sort" + "strings" + "sync" +) + +type commandModel struct { + ID, Model, DisplayName, Description, DefaultReasoningEffort string + Hidden bool + SupportedReasoningEfforts []struct{ ReasoningEffort, Description string } + ServiceTiers []struct{ ID, Name, Description string } +} + +type commandThread struct { + ID, Cwd string + Status struct{ Type string } +} + +func commandDigest(value any) string { + data, _ := json.Marshal(value) + return fmt.Sprintf("%x", sha256.Sum256(data)) +} +func commandText(value string) string { return SanitizeSessionContext(value) } +func commandSafe(value string) bool { return value != "" && commandText(value) == value } +func commandLabel(value string) string { return strings.Join(strings.Fields(commandText(value)), " ") } + +func (p *daemonStatusProvider) commandThread(ctx context.Context, id string) (commandThread, error) { + if id == "" || len(id) > 512 { + return commandThread{}, ErrSessionCommand + } + if err := p.ensureConnected(ctx); err != nil { + return commandThread{}, err + } + loaded, err := p.loadedThreads(ctx) + if err != nil { + return commandThread{}, err + } + if _, ok := loaded[id]; !ok { + return commandThread{}, ErrSessionCommand + } + var response struct{ Thread commandThread } + err = p.request(ctx, "thread/read", map[string]any{"threadId": id, "includeTurns": false}, &response) + if err != nil { + return response.Thread, err + } + if response.Thread.ID != id || response.Thread.Cwd == "" { + return commandThread{}, ErrSessionCommand + } + return response.Thread, nil +} + +// Pagination and total inventory are bounded. Repeated cursors fail rather +// than returning a partial set which might look authoritative. +func (p *daemonStatusProvider) commandRows(ctx context.Context, method string, params map[string]any) ([]json.RawMessage, error) { + var rows []json.RawMessage + seen := map[string]bool{} + for page := 0; page < 32; page++ { + var response struct { + Data []json.RawMessage + NextCursor *string + } + if err := p.request(ctx, method, params, &response); err != nil { + return nil, err + } + rows = append(rows, response.Data...) + if len(rows) > 2048 { + return nil, ErrSessionCommand + } + if response.NextCursor == nil || *response.NextCursor == "" { + return rows, nil + } + if seen[*response.NextCursor] { + return nil, ErrSessionCommand + } + seen[*response.NextCursor] = true + params["cursor"] = *response.NextCursor + } + return nil, ErrSessionCommand +} + +func (p *daemonStatusProvider) commandModels(ctx context.Context) ([]commandModel, error) { + rows, err := p.commandRows(ctx, "model/list", map[string]any{}) + if err != nil { + return nil, err + } + var models []commandModel + for _, raw := range rows { + var model commandModel + if json.Unmarshal(raw, &model) == nil && !model.Hidden && commandSafe(model.Model) { + models = append(models, model) + } + } + return models, nil +} + +func (p *daemonStatusProvider) SessionCommands(ctx context.Context, id, path string) (SessionCommandMenu, error) { + if len(path) > 2048 { + return SessionCommandMenu{}, ErrSessionCommand + } + path = strings.TrimPrefix(strings.TrimSpace(path), "/") + thread, err := p.commandThread(ctx, id) + if err != nil { + return SessionCommandMenu{}, err + } + menu, err := p.commandMenu(ctx, thread, path) + if err != nil { + return menu, err + } + menu.Path = path + p.mu.Lock() + connection := fmt.Sprintf("%p", p.connection) + p.mu.Unlock() + menu.Revision = commandDigest([]any{connection, thread.ID, thread.Cwd, thread.Status.Type, menu.Revision}) + return normalizeCommandMenu(menu), nil +} + +func normalizeCommandMenu(menu SessionCommandMenu) SessionCommandMenu { + safeHelp := commandText(menu.Help) == strings.TrimSpace(menu.Help) + menu.Title, menu.Help = commandLabel(menu.Title), commandText(menu.Help) + unique := make([]SessionCommandChoice, 0, len(menu.Choices)) + seen := map[string]bool{} + for i := range menu.Choices { + o := &menu.Choices[i] + if o.method != "" && (!safeHelp || commandLabel(o.Label) != o.Label || commandText(o.Help) != strings.TrimSpace(o.Help)) { + o.method = "" + o.params = nil + o.Help = "Complete safe help unavailable. Use Codex for this option." + } + o.Label, o.Help = commandLabel(o.Label), commandText(o.Help) + o.ID = commandDigest([]any{o.Label, o.Help, o.Next, o.method, o.params}) + o.Action = o.method != "" + if !seen[o.ID] { + unique = append(unique, *o) + seen[o.ID] = true + } + } + menu.Choices = unique + return menu +} + +func (p *daemonStatusProvider) commandMenu(ctx context.Context, t commandThread, path string) (SessionCommandMenu, error) { + m := SessionCommandMenu{Title: "/" + path, Help: "Live options from the shared Codex app-server.", Choices: []SessionCommandChoice{}} + parts := strings.Split(path, "/") + if path == "" || path == "help" { + families := []string{"model", "plan", "permissions", "skills", "apps", "mcp", "hooks", "experimental"} + menus := make([]SessionCommandMenu, len(families)) + errs := make([]error, len(families)) + var wg sync.WaitGroup + for i, family := range families { + wg.Add(1) + go func() { defer wg.Done(); menus[i], errs[i] = p.commandMenu(ctx, t, family) }() + } + wg.Wait() + for i, family := range families { + if errs[i] == nil && len(menus[i].Choices) > 0 { + m.Choices = append(m.Choices, SessionCommandChoice{Label: "/" + family, Help: menus[i].Help, Next: family}) + } + } + if models, err := p.commandModels(ctx); err == nil { + if current, err := p.readQuotaSession(ctx, t.ID); err == nil { + for _, model := range models { + if model.Model == current.Model { + for _, tier := range model.ServiceTiers { + name := strings.ToLower(tier.Name) + if commandSafe(tier.ID) && commandSafe(name) && !strings.ContainsAny(name, " /\t\n") && !commandReserved(name) { + m.Choices = append(m.Choices, SessionCommandChoice{Label: "/" + name, Help: tier.Description, Next: "tier/" + url.PathEscape(tier.ID)}) + } + } + } + } + } + } + m.Title = "/ COMMANDS" + m.Help = "Choose a command. Options/help are discovered live. Settings changes target this session only, require confirmation and are available only while idle. Catalogues marked browse-only do not execute tools or change global configuration." + return m, nil + } + if parts[0] == "model" || parts[0] == "tier" || parts[0] == "plan" || parts[0] == "permissions" { + current, err := p.readQuotaSession(ctx, t.ID) + if err != nil { + return m, err + } + m.Revision = commandDigest(current) + setting := func(label, help string, params map[string]any) { + params["threadId"] = t.ID + if t.Status.Type != "idle" { + help += " — available when the session is idle" + m.Choices = append(m.Choices, SessionCommandChoice{Label: label, Help: help}) + return + } + m.Choices = append(m.Choices, SessionCommandChoice{Label: label, Help: help, method: "thread/settings/update", params: params}) + } + switch parts[0] { + case "model", "tier": + models, err := p.commandModels(ctx) + if err != nil { + return m, err + } + if path == "model" { + m.Help = "Select a model, then one of its advertised reasoning levels. Changes apply to subsequent turns in this session; automatic quota thresholds may later replace them." + for _, model := range models { + m.Choices = append(m.Choices, SessionCommandChoice{Label: model.Model, Help: model.Description, Next: "model/" + url.PathEscape(model.Model)}) + } + return m, nil + } + if len(parts) != 2 { + return m, ErrSessionCommand + } + target, err := url.PathUnescape(parts[1]) + if err != nil { + return m, err + } + for _, model := range models { + if parts[0] == "model" && model.Model == target { + m.Help = model.Description + "\nSelect reasoning effort. Existing speed will be reset to the server default to avoid carrying an unsupported tier to another model." + for _, effort := range model.SupportedReasoningEfforts { + if commandSafe(effort.ReasoningEffort) { + setting(effort.ReasoningEffort, effort.Description, map[string]any{"model": model.Model, "effort": effort.ReasoningEffort, "serviceTier": nil}) + } + } + if len(model.SupportedReasoningEfforts) == 0 && commandSafe(model.DefaultReasoningEffort) { + setting(model.DefaultReasoningEffort, "Server-advertised default reasoning effort.", map[string]any{"model": model.Model, "effort": model.DefaultReasoningEffort, "serviceTier": nil}) + } + return m, nil + } + if parts[0] == "tier" && model.Model == current.Model { + for _, tier := range model.ServiceTiers { + if tier.ID == target { + m.Title = "/" + strings.ToLower(tier.Name) + m.Help = tier.Description + setting("Enable "+tier.Name, tier.Description, map[string]any{"serviceTier": tier.ID}) + setting("Use default tier", "Clear this session's explicit speed override.", map[string]any{"serviceTier": nil}) + return m, nil + } + } + } + } + case "permissions": + m.Help = "Permission profiles allowed by the effective requirements for this directory. Review the scope carefully: this changes what Codex may do without asking in subsequent turns." + rows, err := p.commandRows(ctx, "permissionProfile/list", map[string]any{"cwd": t.Cwd}) + if err != nil { + return m, err + } + for _, raw := range rows { + var v struct { + ID, Description string + Allowed bool + } + if json.Unmarshal(raw, &v) == nil && v.Allowed && commandSafe(v.ID) && strings.TrimSpace(v.Description) != "" { + setting(v.ID, v.Description, map[string]any{"permissions": v.ID}) + } + } + return m, nil + case "plan": + m.Help = "Choose an advertised collaboration mode for subsequent turns. Uses Codex's built-in mode instructions, not a generated prompt." + rows, err := p.commandRows(ctx, "collaborationMode/list", map[string]any{}) + if err != nil { + return m, err + } + for _, raw := range rows { + var v struct { + Name, Mode string + Model *string + Effort *string `json:"reasoning_effort"` + } + if json.Unmarshal(raw, &v) != nil || !commandSafe(v.Mode) { + continue + } + model, effort := current.Model, current.Effort + if v.Model != nil { + model = *v.Model + } + if v.Effort != nil { + effort = *v.Effort + } + setting(v.Name, v.Mode+" // "+model+" // "+effort, map[string]any{"collaborationMode": map[string]any{"mode": v.Mode, "settings": map[string]any{"model": model, "reasoning_effort": effort, "developer_instructions": nil}}}) + } + return m, nil + } + return m, ErrSessionCommand + } + if len(parts) == 1 { + return p.commandCatalogue(ctx, t, path) + } + return m, ErrSessionCommand +} + +func commandReserved(s string) bool { + for _, name := range []string{"help", "model", "tier", "plan", "permissions", "skills", "apps", "mcp", "hooks", "experimental"} { + if s == name { + return true + } + } + return false +} + +func (p *daemonStatusProvider) ExecuteSessionCommand(ctx context.Context, id, path, revision, choice string) error { + p.settingsMu.Lock() + defer p.settingsMu.Unlock() + p.mu.Lock() + conn := p.connection + p.mu.Unlock() + if conn == nil || p.settingsClosed { + return ErrSessionCommand + } + m, err := p.SessionCommands(ctx, id, path) + if err != nil { + return err + } + if m.Revision != revision { + return ErrSessionCommand + } + for _, o := range m.Choices { + if o.ID == choice && o.Action { + p.mu.Lock() + valid := p.connection == conn && (p.contexts[id] == nil || len(p.contexts[id].requests) == 0) + p.mu.Unlock() + if !valid { + return ErrSessionCommand + } + return p.requestOn(ctx, conn, o.method, o.params, nil) + } + } + return ErrSessionCommand +} + +// Read-only inventory deliberately does not infer toggle, installation, OAuth +// or arbitrary tool-execution semantics from a discovered name. +func (p *daemonStatusProvider) commandCatalogue(ctx context.Context, t commandThread, path string) (SessionCommandMenu, error) { + m := SessionCommandMenu{Title: "/" + path, Help: "Browse-only catalogue from Codex. Select an entry to read its supplied help; use Codex to invoke or configure it.", Choices: []SessionCommandChoice{}} + method := map[string]string{"skills": "skills/list", "apps": "app/list", "mcp": "mcpServerStatus/list", "hooks": "hooks/list", "experimental": "experimentalFeature/list"}[path] + if method == "" { // Catalog tier aliases such as /fast, never assumed IDs. + root, err := p.commandMenu(ctx, t, "") + if err != nil { + return m, err + } + for _, o := range root.Choices { + if o.Label == "/"+path { + return p.commandMenu(ctx, t, o.Next) + } + } + return m, ErrSessionCommand + } + params := map[string]any{} + switch path { + case "skills", "hooks": + params["cwds"] = []string{t.Cwd} + case "apps", "mcp": + params["threadId"] = t.ID + } + rows, err := p.commandRows(ctx, method, params) + if err != nil { + return m, err + } + overflow := false + add := func(label, help string) { + if len(m.Choices) < 2048 { + m.Choices = append(m.Choices, SessionCommandChoice{Label: label, Help: help}) + } else { + overflow = true + } + } + for _, raw := range rows { + switch path { + case "skills": + var entry struct { + Cwd string + Skills []struct { + Name, Description, Path string + Enabled bool + } + } + if json.Unmarshal(raw, &entry) == nil && entry.Cwd == t.Cwd { + for _, s := range entry.Skills { + if s.Enabled { + add(s.Name, s.Description+"\n"+s.Path) + } + } + } + case "hooks": + var entry struct { + Cwd string + Hooks []struct { + Key, EventName, StatusMessage, Command, Server, Tool, HandlerType string + Enabled bool + } + } + if json.Unmarshal(raw, &entry) == nil && entry.Cwd == t.Cwd { + for _, h := range entry.Hooks { + add(h.Key, fmt.Sprintf("%s // %s // enabled=%t\n%s\n%s %s %s", h.EventName, h.HandlerType, h.Enabled, h.StatusMessage, h.Command, h.Server, h.Tool)) + } + } + case "apps": + var a struct { + Name, Description string + IsAccessible, IsEnabled bool + } + if json.Unmarshal(raw, &a) == nil && a.IsAccessible && a.IsEnabled { + add(a.Name, a.Description) + } + case "experimental": + var f struct { + Name, DisplayName, Description, Announcement, Stage string + Enabled bool + } + if json.Unmarshal(raw, &f) == nil { + add(f.Name, fmt.Sprintf("%s // %s // enabled=%t\n%s\n%s", f.DisplayName, f.Stage, f.Enabled, f.Description, f.Announcement)) + } + case "mcp": + var s struct { + Name, AuthStatus, ToolsError string + Tools map[string]struct { + Name string + Description string + } + } + if json.Unmarshal(raw, &s) == nil { + add(s.Name, "Authentication: "+s.AuthStatus+"\n"+s.ToolsError) + names := make([]string, 0, len(s.Tools)) + for name := range s.Tools { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + add(s.Name+" / "+name, s.Tools[name].Description) + } + } + } + } + if overflow { + return SessionCommandMenu{}, ErrSessionCommand + } + return m, nil +} diff --git a/internal/codex/session_commands_unix_test.go b/internal/codex/session_commands_unix_test.go new file mode 100644 index 0000000..2851e93 --- /dev/null +++ b/internal/codex/session_commands_unix_test.go @@ -0,0 +1,108 @@ +//go:build unix + +package codex + +import ( + "context" + "strings" + "testing" +) + +func TestSessionCommandsModelAndTier(t *testing.T) { + p, f := newQuotaDaemon(t) + ctx := context.Background() + m, err := p.SessionCommands(ctx, "one", "/model") + if err != nil || len(m.Choices) != 1 || m.Choices[0].Next != "model/small" { + t.Fatalf("models: %+v %v", m, err) + } + m, err = p.SessionCommands(ctx, "one", m.Choices[0].Next) + if err != nil || len(m.Choices) != 1 || !m.Choices[0].Action { + t.Fatalf("efforts: %+v %v", m, err) + } + if err = p.ExecuteSessionCommand(ctx, "one", m.Path, "stale", m.Choices[0].ID); err == nil { + t.Fatal("accepted stale revision") + } + if err = p.ExecuteSessionCommand(ctx, "one", m.Path, m.Revision, m.Choices[0].ID); err != nil { + t.Fatal(err) + } + f.mu.Lock() + if len(f.writes) != 1 || f.writes[0]["model"] != "small" || f.writes[0]["effort"] != "medium" || f.writes[0]["serviceTier"] != nil || f.sessions["two"].Model != "large" { + t.Errorf("wrong scoped mutation: %+v", f.writes) + } + f.mu.Unlock() + m, err = p.SessionCommands(ctx, "one", "fast") + if err != nil || len(m.Choices) != 2 { + t.Fatalf("tier alias: %+v %v", m, err) + } + if err = p.ExecuteSessionCommand(ctx, "one", m.Path, m.Revision, m.Choices[0].ID); err != nil { + t.Fatal(err) + } + f.mu.Lock() + if f.writes[1]["serviceTier"] != "flex" { + t.Errorf("used display name instead of advertised ID: %+v", f.writes[1]) + } + f.commandStatus = "active" + f.mu.Unlock() + if err = p.ExecuteSessionCommand(ctx, "one", m.Path, m.Revision, m.Choices[1].ID); err == nil { + t.Fatal("accepted action after turn started") + } + if _, err = p.SessionCommands(ctx, "closed", "model"); err == nil { + t.Fatal("opened unloaded session") + } +} + +func TestSessionCommandCataloguesAndSafeHelp(t *testing.T) { + p, f := newQuotaDaemon(t) + f.catalogues = map[string]map[string]any{ + "permissionProfile/list": {"data": []any{ + map[string]any{"id": "safe", "description": "Read-only workspace", "allowed": true}, + map[string]any{"id": "blocked", "description": "Not allowed", "allowed": false}, + map[string]any{"id": "bad-help", "description": "unsafe\x1b[31mhelp", "allowed": true}, + }}, + "skills/list": {"data": []any{map[string]any{"cwd": "/work", "skills": []any{map[string]any{"name": "review", "description": "Review help", "path": "/skills/review", "enabled": true}}}}}, + } + ctx := context.Background() + m, err := p.SessionCommands(ctx, "one", "permissions") + if err != nil || len(m.Choices) != 2 || !m.Choices[0].Action || m.Choices[1].Action { + t.Fatalf("unsafe permissions: %+v %v", m, err) + } + m, err = p.SessionCommands(ctx, "one", "skills") + if err != nil || len(m.Choices) != 1 || m.Choices[0].Action || !strings.Contains(m.Choices[0].Help, "Review help") { + t.Fatalf("skill help: %+v %v", m, err) + } + if err = p.ExecuteSessionCommand(ctx, "one", m.Path, m.Revision, m.Choices[0].ID); err == nil { + t.Fatal("executed browse-only choice") + } + f.mu.Lock() + f.catalogues["skills/list"] = map[string]any{"nextCursor": "repeat", "data": []any{}} + f.mu.Unlock() + if _, err = p.SessionCommands(ctx, "one", "skills"); err == nil { + t.Fatal("accepted repeating cursor") + } +} + +func TestCommandMenuSingleLineLabelsFailClosed(t *testing.T) { + for _, label := range []string{"Enable\nFast", "Enable\r\nFast", "Enable\tFast", "Enable Fast", " Enable Fast ", "Enable\u2028Fast", "Enable\x1b[31m Fast"} { + menu := normalizeCommandMenu(SessionCommandMenu{ + Title: "Choose\na mode", Help: "First help line\nSecond help line", + Choices: []SessionCommandChoice{ + {Label: label, Help: "Option help\nMore help", method: "thread/settings/update", params: map[string]any{"serviceTier": "flex"}}, + {Label: "Browse\nonly", Help: "First\nSecond"}, + {Label: "Unchanged", Help: "Safe\nmultiline help", method: "thread/settings/update", params: map[string]any{"serviceTier": nil}}, + }, + }) + if menu.Title != "Choose a mode" || menu.Help != "First help line\nSecond help line" { + t.Fatalf("title/help normalization: %+v", menu) + } + changed := menu.Choices[0] + if strings.ContainsAny(changed.Label, "\n\r\t\u2028\x1b") || changed.Action || changed.method != "" || changed.params != nil { + t.Fatalf("normalized action remained executable: %+v", changed) + } + if menu.Choices[1].Label != "Browse only" || menu.Choices[1].Help != "First\nSecond" { + t.Fatal("browse label must be single line while help retains paragraphs") + } + if !menu.Choices[2].Action || menu.Choices[2].Help != "Safe\nmultiline help" { + t.Fatal("safe action or multiline help was unnecessarily disabled") + } + } +} diff --git a/internal/codex/session_context.go b/internal/codex/session_context.go index 9cfad38..ee86860 100644 --- a/internal/codex/session_context.go +++ b/internal/codex/session_context.go @@ -25,10 +25,11 @@ const ( // SessionContext is a bounded, memory-only excerpt, never a generated summary. // It is deliberately separate from token accounting and persisted preferences. type SessionContext struct { - CurrentTask string - LatestGuidance string - Streaming bool - Activity SessionActivity + CurrentTask string + LatestGuidance string + LatestGuidanceID string + Streaming bool + Activity SessionActivity // ApprovalContext is preceding same-thread/turn prose captured when the // request arrived. Display-only: never part of the authorised action. ApprovalContext string diff --git a/internal/codex/session_context_daemon.go b/internal/codex/session_context_daemon.go index ded9bbf..330519d 100644 --- a/internal/codex/session_context_daemon.go +++ b/internal/codex/session_context_daemon.go @@ -13,6 +13,7 @@ type daemonContextState struct { turnObserved bool userItems map[string]bool task, guidance string + guidanceID string ended bool streamItem, streamTurn, streamText, streamPhase string activeTurn string @@ -380,6 +381,10 @@ func daemonContextSnapshot(states map[string]*daemonContextState, ids []string, } c.PendingApprovals = count c.CurrentTask, c.LatestGuidance = state.task, state.guidance + c.LatestGuidanceID = state.guidanceID + if c.ThreadID == "" && (c.CurrentTask != "" || c.LatestGuidance != "") { + c.ThreadID, c.TurnID, c.Source = id, state.activeTurn, "LIVE" + } c.PendingApprovalsLimited = state.approvalsLimited && statuses[id] == sessionRuntimeApproval if c.Text != "" || c.CurrentTask != "" || c.LatestGuidance != "" || c.PendingApprovalsLimited { out[id] = c @@ -424,6 +429,7 @@ func (s *daemonContextState) observeUserMessage(id, turn, text string) bool { s.task = text } else { s.guidance = text + s.guidanceID = id } return true } diff --git a/internal/codex/session_history.go b/internal/codex/session_history.go new file mode 100644 index 0000000..7c935b0 --- /dev/null +++ b/internal/codex/session_history.go @@ -0,0 +1,34 @@ +package codex + +import ( + "context" + "errors" +) + +const SessionHistoryLimit = 10 + +// Display-only completed turns. No approval/input capabilities are retained. +type SessionHistoryClient interface { + SessionHistory(context.Context, string) ([]SessionContext, error) +} + +var ErrSessionHistory = errors.New("session history unavailable") + +func (c Client) SessionHistory(ctx context.Context, id string) ([]SessionContext, error) { + if c.LiveUsage != nil { + if p, ok := c.LiveUsage.statusProvider.(SessionHistoryClient); ok { + return p.SessionHistory(ctx, id) + } + } + return nil, ErrSessionHistory +} + +// Restrict excerpts to display fields even when supplied by another reader. +func HistoryExcerpt(c SessionContext) SessionContext { + return SessionContext{ + ThreadID: c.ThreadID, TurnID: c.TurnID, ItemID: c.ItemID, + Kind: SessionContextReply, Source: c.Source, At: c.At, + Text: SanitizeSessionContext(c.Text), CurrentTask: SanitizeSessionContext(c.CurrentTask), + LatestGuidance: SanitizeSessionContext(c.LatestGuidance), + } +} diff --git a/internal/codex/session_history_unix.go b/internal/codex/session_history_unix.go new file mode 100644 index 0000000..3fe0991 --- /dev/null +++ b/internal/codex/session_history_unix.go @@ -0,0 +1,77 @@ +//go:build unix + +package codex + +import ( + "context" + "strings" + "time" +) + +type historyTurn struct { + ID, Status string + CompletedAt *int64 + Items []struct { + ID, Type, Text, Phase string + Content []struct{ Type, Text string } + } +} + +func (p *daemonStatusProvider) SessionHistory(ctx context.Context, id string) ([]SessionContext, error) { + if id == "" || len(id) > 512 { + return nil, ErrSessionHistory + } + if err := p.ensureConnected(ctx); err != nil { + return nil, err + } + // Bounded, newest-first history reads neither resume nor change a session. + // Do not fall back to an unbounded includeTurns read on older servers. + var response struct{ Data []historyTurn } + if err := p.request(ctx, "thread/turns/list", map[string]any{ + "threadId": id, "limit": SessionHistoryLimit + 2, "sortDirection": "desc", "itemsView": "summary", + }, &response); err != nil { + return nil, err + } + var result []SessionContext + for _, turn := range response.Data { + if turn.Status != "completed" || turn.ID == "" { + continue + } + c := SessionContext{ThreadID: id, TurnID: turn.ID, Kind: SessionContextReply, Source: "HISTORY"} + if turn.CompletedAt != nil { + c.At = time.Unix(*turn.CompletedAt, 0) + } + for _, item := range turn.Items { + switch item.Type { + case "userMessage": + var text []string + for _, content := range item.Content { + if content.Type == "text" { + text = append(text, content.Text) + } + } + if c.CurrentTask == "" { + c.CurrentTask = strings.Join(text, "\n") + } else { + c.LatestGuidance = strings.Join(text, "\n") + } + case "agentMessage": + if item.Phase == "final_answer" || item.Phase == "" { + c.Text, c.ItemID = item.Text, item.ID + } + } + } + c = HistoryExcerpt(c) + if c.Text != "" { + result = append(result, c) + } + if len(result) == SessionHistoryLimit { + break + } + } + // UI caches use oldest-first order, independent of completion timestamps. + for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 { + result[i], result[j] = result[j], result[i] + } + return result, nil +} diff --git a/internal/codex/session_history_unix_test.go b/internal/codex/session_history_unix_test.go new file mode 100644 index 0000000..0d5fa42 --- /dev/null +++ b/internal/codex/session_history_unix_test.go @@ -0,0 +1,49 @@ +//go:build unix + +package codex + +import ( + "context" + "fmt" + "strings" + "testing" +) + +func TestSessionHistoryBoundedReadOnlyCompletedTurns(t *testing.T) { + p, f := newQuotaDaemon(t) + rows := []any{map[string]any{"id": "active", "status": "inProgress", "items": []any{map[string]any{"type": "agentMessage", "text": "Not finished"}}}} + for i := 15; i >= 1; i-- { + rows = append(rows, map[string]any{"id": fmt.Sprint(i), "status": "completed", "completedAt": int64(i), "items": []any{ + map[string]any{"type": "userMessage", "content": []any{map[string]any{"type": "text", "text": fmt.Sprintf("Task %d", i)}}}, + map[string]any{"type": "agentMessage", "phase": "commentary", "text": "Planning"}, + map[string]any{"type": "agentMessage", "phase": "final_answer", "text": fmt.Sprintf("\x1b[31mAnswer %d\x00", i)}, + map[string]any{"type": "commandExecution", "command": "never run this"}, + }}) + } + f.catalogues = map[string]map[string]any{"thread/turns/list": {"data": rows}} + ctx := context.Background() + turns, err := p.SessionHistory(ctx, "one") + if err != nil || len(turns) != SessionHistoryLimit || turns[0].TurnID != "6" || turns[9].TurnID != "15" { + t.Fatalf("history ordering/cap: %+v %v", turns, err) + } + for _, c := range turns { + if c.CurrentTask != "Task "+c.TurnID || c.Text != "Answer "+c.TurnID || c.ApprovalToken != "" || c.InputToken != "" || strings.Contains(c.Text, "Planning") { + t.Fatalf("wrong excerpt: %+v", c) + } + } + f.mu.Lock() + if len(f.writes) != 0 || len(f.historyReads) != 1 || f.historyReads[0]["limit"] != float64(SessionHistoryLimit+2) || f.historyReads[0]["itemsView"] != "summary" || f.historyReads[0]["sortDirection"] != "desc" || f.historyReads[0]["threadId"] != "one" { + t.Errorf("unsafe/unbounded request: %+v", f.historyReads) + } + delete(f.catalogues, "thread/turns/list") + f.mu.Unlock() + if _, err = p.SessionHistory(ctx, "one"); err == nil { + t.Fatal("unsupported history reported success") + } + if _, err = p.SessionHistory(ctx, ""); err == nil { + t.Fatal("accepted empty session") + } + if _, err = (Client{}).SessionHistory(ctx, "one"); err == nil { + t.Fatal("unavailable provider reported success") + } +} diff --git a/internal/codex/session_prompt_unix.go b/internal/codex/session_prompt_unix.go index 1e46659..01f9c8d 100644 --- a/internal/codex/session_prompt_unix.go +++ b/internal/codex/session_prompt_unix.go @@ -141,6 +141,10 @@ func (p *daemonStatusProvider) SendSessionPrompt(ctx context.Context, token stri p.mu.Unlock() return errors.New("prompt expired or answer invalid; check Codex") } + if len(offer.Questions) == 0 && IsSessionCommand(answers[0]) { + p.mu.Unlock() + return errors.New("use the command menu; prefix with // to send literal slash text") + } // Consume the capability before any IO. Never automatically retry an // ambiguous write, or send it over a replacement connection. if requestID != "" { @@ -181,6 +185,6 @@ func (p *daemonStatusProvider) SendSessionPrompt(ctx context.Context, token stri } return p.requestOn(ctx, connection, "turn/start", map[string]any{ "threadId": offer.ThreadID, - "input": []any{map[string]any{"type": "text", "text": answers[0]}}, + "input": []any{map[string]any{"type": "text", "text": literalSessionText(answers[0])}}, }, nil) } diff --git a/internal/codex/session_queue_unix.go b/internal/codex/session_queue_unix.go index a1d682b..e293243 100644 --- a/internal/codex/session_queue_unix.go +++ b/internal/codex/session_queue_unix.go @@ -107,6 +107,10 @@ func (p *daemonStatusProvider) SessionQueue(ctx context.Context, thread string) } func (p *daemonStatusProvider) ChangeSessionQueue(ctx context.Context, thread string, item SessionQueuedMessage, text string, remove bool) error { + if !remove && IsSessionCommand(text) { + return errors.New("slash commands cannot be queued; use the command menu") + } + text = literalSessionText(text) p.mu.Lock() cap, ok := p.queueCapabilities[item.Token] if !ok || cap.connection != p.connection || cap.thread != thread || cap.item.ID != item.ID || (!remove && !cap.item.Editable) { diff --git a/internal/codex/session_settings_unix_test.go b/internal/codex/session_settings_unix_test.go index 02b2550..909e370 100644 --- a/internal/codex/session_settings_unix_test.go +++ b/internal/codex/session_settings_unix_test.go @@ -17,14 +17,17 @@ import ( ) type quotaDaemonFixture struct { - mu sync.Mutex - sessions map[string]QuotaSession - writes []map[string]any - fail string - queued bool - notify bool - started chan struct{} - release chan struct{} + mu sync.Mutex + sessions map[string]QuotaSession + writes []map[string]any + fail string + queued bool + notify bool + started chan struct{} + release chan struct{} + commandStatus string + catalogues map[string]map[string]any + historyReads []map[string]any } func TestQuotaEffortDefaultFallback(t *testing.T) { @@ -104,6 +107,12 @@ func newQuotaDaemon(t *testing.T) (*daemonStatusProvider, *quotaDaemonFixture) { s, ok := fixture.sessions[id] failure = !ok || fixture.fail == id result = map[string]any{"model": s.Model, "reasoningEffort": s.Effort, "serviceTier": s.Tier, "thread": map[string]any{"id": id}} + case "thread/read": + status := fixture.commandStatus + if status == "" { + status = "idle" + } + result["thread"] = map[string]any{"id": id, "cwd": "/work", "status": map[string]any{"type": status}} case "thread/settings/update": fixture.writes = append(fixture.writes, req.Params) desired := fixture.sessions[id] @@ -139,7 +148,14 @@ func newQuotaDaemon(t *testing.T) (*daemonStatusProvider, *quotaDaemonFixture) { fixture.mu.Lock() } default: - failure = true + if req.Method == "thread/turns/list" { + fixture.historyReads = append(fixture.historyReads, req.Params) + } + if value, ok := fixture.catalogues[req.Method]; ok { + result = value + } else { + failure = true + } } fixture.mu.Unlock() response := map[string]any{"id": *req.ID, "result": result} diff --git a/internal/codex/session_stream_test.go b/internal/codex/session_stream_test.go index ecc00f7..a06d391 100644 --- a/internal/codex/session_stream_test.go +++ b/internal/codex/session_stream_test.go @@ -24,7 +24,7 @@ func TestLiveTaskGuidanceAndStream(t *testing.T) { } event("item/completed", `{"threadId":"root","turnId":"turn","item":{"type":"userMessage","id":"u2","content":[{"type":"text","text":"Use Go"}]}}`) event("item/completed", `{"threadId":"root","turnId":"turn","item":{"type":"userMessage","id":"u1","content":[{"type":"text","text":"Build it"}]}}`) - if c := snapshot(); c.CurrentTask != "Build it" || c.LatestGuidance != "Use Go" { + if c := snapshot(); c.CurrentTask != "Build it" || c.LatestGuidance != "Use Go" || c.LatestGuidanceID != "u2" || c.ThreadID != "root" || c.TurnID != "turn" { t.Fatal(c) } event("item/started", `{"threadId":"root","turnId":"turn","item":{"type":"agentMessage","id":"a","phase":"final_answer"}}`) @@ -44,7 +44,7 @@ func TestLiveTaskGuidanceAndStream(t *testing.T) { t.Fatal(c) } event("turn/started", `{"threadId":"root","turn":{"id":"next"}}`) - if c := snapshot(); c.Text != "" || c.CurrentTask != "" { + if c := snapshot(); c.Text != "" || c.CurrentTask != "" || c.LatestGuidanceID != "" { t.Fatal(c) } } diff --git a/internal/codex/session_turn_unix.go b/internal/codex/session_turn_unix.go index d8b5b14..a08ac1c 100644 --- a/internal/codex/session_turn_unix.go +++ b/internal/codex/session_turn_unix.go @@ -23,6 +23,10 @@ func (p *daemonStatusProvider) SessionTurn(thread string) SessionPromptOffer { } func (p *daemonStatusProvider) SendSessionTurn(ctx context.Context, offer SessionPromptOffer, action, text string) error { + if action != "interrupt" && IsSessionCommand(text) { + return errors.New("slash commands cannot be steered or queued; open the command menu") + } + text = literalSessionText(text) if err := ctx.Err(); err != nil { return err } diff --git a/internal/i18n/locales/da.json b/internal/i18n/locales/da.json index c7894d7..671372d 100644 --- a/internal/i18n/locales/da.json +++ b/internal/i18n/locales/da.json @@ -1,4 +1,12 @@ { + "PREVIOUS TURN": "FORRIGE TUR", + "LIVE SESSION": "LIVESESSION", + " // %d OF %d": " // %d AF %d", + "[ Alt+←: PREVIOUS ]": "[ Alt+←: FORRIGE ]", + "[ Alt+→: NEXT ]": "[ Alt+→: NÆSTE ]", + "[ LIVE ]": "[ LIVE ]", + "Slash commands cannot be scheduled. Use // for literal slash text.": "Skråstregskommandoer kan ikke planlægges. Brug // til almindelig tekst, der begynder med en skråstreg.", + "STEER SENT": "INSTRUKTION SENDT", "TRIGGER SET": "UDLØSER INDSTILLET", "Ctrl+S: EDIT": "Ctrl+S: REDIGER", "CURRENT TASK": "AKTUEL OPGAVE", diff --git a/internal/i18n/locales/de.json b/internal/i18n/locales/de.json index 1e31615..be2581e 100644 --- a/internal/i18n/locales/de.json +++ b/internal/i18n/locales/de.json @@ -1,4 +1,12 @@ { + "PREVIOUS TURN": "VORHERIGER DURCHGANG", + "LIVE SESSION": "LIVE-SITZUNG", + " // %d OF %d": " // %d VON %d", + "[ Alt+←: PREVIOUS ]": "[ Alt+←: ZURÜCK ]", + "[ Alt+→: NEXT ]": "[ Alt+→: WEITER ]", + "[ LIVE ]": "[ LIVE ]", + "Slash commands cannot be scheduled. Use // for literal slash text.": "Slash-Befehle können nicht geplant werden. Verwenden Sie // für wörtlichen Text mit Schrägstrich.", + "STEER SENT": "STEUERUNG GESENDET", "TRIGGER SET": "AUSLÖSER GESETZT", "Ctrl+S: EDIT": "Ctrl+S: BEARBEITEN", "CURRENT TASK": "AKTUELLE AUFGABE", diff --git a/internal/i18n/locales/en-GB.json b/internal/i18n/locales/en-GB.json index 08c307f..f6a743b 100644 --- a/internal/i18n/locales/en-GB.json +++ b/internal/i18n/locales/en-GB.json @@ -1,4 +1,12 @@ { + "PREVIOUS TURN": "PREVIOUS TURN", + "LIVE SESSION": "LIVE SESSION", + " // %d OF %d": " // %d OF %d", + "[ Alt+←: PREVIOUS ]": "[ Alt+←: PREVIOUS ]", + "[ Alt+→: NEXT ]": "[ Alt+→: NEXT ]", + "[ LIVE ]": "[ LIVE ]", + "Slash commands cannot be scheduled. Use // for literal slash text.": "Slash commands cannot be scheduled. Use // for literal slash text.", + "STEER SENT": "STEER SENT", "TRIGGER SET": "TRIGGER SET", "Ctrl+S: EDIT": "Ctrl+S: EDIT", "CURRENT TASK": "CURRENT TASK", diff --git a/internal/i18n/locales/es.json b/internal/i18n/locales/es.json index be71ef0..1b9d3a9 100644 --- a/internal/i18n/locales/es.json +++ b/internal/i18n/locales/es.json @@ -1,4 +1,12 @@ { + "PREVIOUS TURN": "TURNO ANTERIOR", + "LIVE SESSION": "SESIÓN EN DIRECTO", + " // %d OF %d": " // %d DE %d", + "[ Alt+←: PREVIOUS ]": "[ Alt+←: ANTERIOR ]", + "[ Alt+→: NEXT ]": "[ Alt+→: SIGUIENTE ]", + "[ LIVE ]": "[ DIRECTO ]", + "Slash commands cannot be scheduled. Use // for literal slash text.": "Los comandos con barra no se pueden programar. Usa // para texto literal que empiece por una barra.", + "STEER SENT": "INDICACIÓN ENVIADA", "TRIGGER SET": "ACTIVADOR CONFIGURADO", "Ctrl+S: EDIT": "Ctrl+S: EDITAR", "CURRENT TASK": "TAREA ACTUAL", diff --git a/internal/i18n/locales/et.json b/internal/i18n/locales/et.json index 0d8a765..690c152 100644 --- a/internal/i18n/locales/et.json +++ b/internal/i18n/locales/et.json @@ -1,4 +1,12 @@ { + "PREVIOUS TURN": "EELMINE VOOR", + "LIVE SESSION": "REAALAJASEANSS", + " // %d OF %d": " // %d / %d", + "[ Alt+←: PREVIOUS ]": "[ Alt+←: EELMINE ]", + "[ Alt+→: NEXT ]": "[ Alt+→: JÄRGMINE ]", + "[ LIVE ]": "[ OTSE ]", + "Slash commands cannot be scheduled. Use // for literal slash text.": "Kaldkriipsukäske ei saa ajastada. Kaldkriipsuga algava tavateksti jaoks kasutage //.", + "STEER SENT": "JUHIS SAADETUD", "TRIGGER SET": "PÄÄSTIK SEATUD", "Ctrl+S: EDIT": "Ctrl+S: MUUDA", "CURRENT TASK": "PRAEGUNE ÜLESANNE", diff --git a/internal/i18n/locales/fi.json b/internal/i18n/locales/fi.json index c5f68be..4addb03 100644 --- a/internal/i18n/locales/fi.json +++ b/internal/i18n/locales/fi.json @@ -1,4 +1,12 @@ { + "PREVIOUS TURN": "EDELLINEN VUORO", + "LIVE SESSION": "REAALIAIKAINEN ISTUNTO", + " // %d OF %d": " // %d / %d", + "[ Alt+←: PREVIOUS ]": "[ Alt+←: EDELLINEN ]", + "[ Alt+→: NEXT ]": "[ Alt+→: SEURAAVA ]", + "[ LIVE ]": "[ LIVE ]", + "Slash commands cannot be scheduled. Use // for literal slash text.": "Kauttaviivakomentoja ei voi ajastaa. Käytä // tavalliselle tekstille, joka alkaa kauttaviivalla.", + "STEER SENT": "OHJE LÄHETETTY", "TRIGGER SET": "EHTO ASETETTU", "Ctrl+S: EDIT": "Ctrl+S: MUOKKAA", "CURRENT TASK": "NYKYINEN TEHTÄVÄ", diff --git a/internal/i18n/locales/fr.json b/internal/i18n/locales/fr.json index 9816442..8aef134 100644 --- a/internal/i18n/locales/fr.json +++ b/internal/i18n/locales/fr.json @@ -1,4 +1,12 @@ { + "PREVIOUS TURN": "TOUR PRÉCÉDENT", + "LIVE SESSION": "SESSION EN DIRECT", + " // %d OF %d": " // %d SUR %d", + "[ Alt+←: PREVIOUS ]": "[ Alt+←: PRÉCÉDENT ]", + "[ Alt+→: NEXT ]": "[ Alt+→: SUIVANT ]", + "[ LIVE ]": "[ DIRECT ]", + "Slash commands cannot be scheduled. Use // for literal slash text.": "Les commandes slash ne peuvent pas être planifiées. Utilisez // pour du texte commençant littéralement par une barre oblique.", + "STEER SENT": "DIRECTIVE ENVOYÉE", "TRIGGER SET": "DÉCLENCHEUR DÉFINI", "Ctrl+S: EDIT": "Ctrl+S: MODIFIER", "CURRENT TASK": "TÂCHE EN COURS", diff --git a/internal/i18n/locales/it.json b/internal/i18n/locales/it.json index fbbae70..ab0ee1e 100644 --- a/internal/i18n/locales/it.json +++ b/internal/i18n/locales/it.json @@ -1,4 +1,12 @@ { + "PREVIOUS TURN": "TURNO PRECEDENTE", + "LIVE SESSION": "SESSIONE IN DIRETTA", + " // %d OF %d": " // %d DI %d", + "[ Alt+←: PREVIOUS ]": "[ Alt+←: PRECEDENTE ]", + "[ Alt+→: NEXT ]": "[ Alt+→: SUCCESSIVO ]", + "[ LIVE ]": "[ DIRETTA ]", + "Slash commands cannot be scheduled. Use // for literal slash text.": "I comandi slash non possono essere programmati. Usa // per testo letterale che inizia con una barra.", + "STEER SENT": "INDICAZIONE INVIATA", "TRIGGER SET": "TRIGGER IMPOSTATO", "Ctrl+S: EDIT": "Ctrl+S: MODIFICA", "CURRENT TASK": "ATTIVITÀ CORRENTE", diff --git a/internal/i18n/locales/ja.json b/internal/i18n/locales/ja.json index eb578cf..f922ae2 100644 --- a/internal/i18n/locales/ja.json +++ b/internal/i18n/locales/ja.json @@ -1,4 +1,12 @@ { + "PREVIOUS TURN": "前のターン", + "LIVE SESSION": "ライブセッション", + " // %d OF %d": " // %d / %d", + "[ Alt+←: PREVIOUS ]": "[ Alt+←: 前へ ]", + "[ Alt+→: NEXT ]": "[ Alt+→: 次へ ]", + "[ LIVE ]": "[ ライブ ]", + "Slash commands cannot be scheduled. Use // for literal slash text.": "スラッシュコマンドは予約できません。スラッシュで始まる通常のテキストには // を使用してください。", + "STEER SENT": "追加指示を送信済み", "TRIGGER SET": "トリガー設定済み", "Ctrl+S: EDIT": "Ctrl+S: 編集", "CURRENT TASK": "現在のタスク", diff --git a/internal/i18n/locales/nb.json b/internal/i18n/locales/nb.json index 959981a..461ef32 100644 --- a/internal/i18n/locales/nb.json +++ b/internal/i18n/locales/nb.json @@ -1,4 +1,12 @@ { + "PREVIOUS TURN": "FORRIGE TUR", + "LIVE SESSION": "DIREKTEØKT", + " // %d OF %d": " // %d AV %d", + "[ Alt+←: PREVIOUS ]": "[ Alt+←: FORRIGE ]", + "[ Alt+→: NEXT ]": "[ Alt+→: NESTE ]", + "[ LIVE ]": "[ DIREKTE ]", + "Slash commands cannot be scheduled. Use // for literal slash text.": "Skråstrekkommandoer kan ikke planlegges. Bruk // for vanlig tekst som begynner med en skråstrek.", + "STEER SENT": "INSTRUKSJON SENDT", "TRIGGER SET": "UTLØSER ANGITT", "Ctrl+S: EDIT": "Ctrl+S: REDIGER", "CURRENT TASK": "GJELDENDE OPPGAVE", diff --git a/internal/i18n/locales/nl.json b/internal/i18n/locales/nl.json index b63a2aa..93fd9f0 100644 --- a/internal/i18n/locales/nl.json +++ b/internal/i18n/locales/nl.json @@ -1,4 +1,12 @@ { + "PREVIOUS TURN": "VORIGE BEURT", + "LIVE SESSION": "LIVESESSIE", + " // %d OF %d": " // %d VAN %d", + "[ Alt+←: PREVIOUS ]": "[ Alt+←: VORIGE ]", + "[ Alt+→: NEXT ]": "[ Alt+→: VOLGENDE ]", + "[ LIVE ]": "[ LIVE ]", + "Slash commands cannot be scheduled. Use // for literal slash text.": "Slashcommando's kunnen niet worden ingepland. Gebruik // voor letterlijke tekst met een slash.", + "STEER SENT": "BIJSTURING VERZONDEN", "TRIGGER SET": "TRIGGER INGESTELD", "Ctrl+S: EDIT": "Ctrl+S: BEWERKEN", "CURRENT TASK": "HUIDIGE TAAK", diff --git a/internal/i18n/locales/pt-BR.json b/internal/i18n/locales/pt-BR.json index a5df4eb..e37b3c6 100644 --- a/internal/i18n/locales/pt-BR.json +++ b/internal/i18n/locales/pt-BR.json @@ -1,4 +1,12 @@ { + "PREVIOUS TURN": "TURNO ANTERIOR", + "LIVE SESSION": "SESSÃO AO VIVO", + " // %d OF %d": " // %d DE %d", + "[ Alt+←: PREVIOUS ]": "[ Alt+←: ANTERIOR ]", + "[ Alt+→: NEXT ]": "[ Alt+→: PRÓXIMO ]", + "[ LIVE ]": "[ AO VIVO ]", + "Slash commands cannot be scheduled. Use // for literal slash text.": "Comandos de barra não podem ser agendados. Use // para texto literal que comece com uma barra.", + "STEER SENT": "ORIENTAÇÃO ENVIADA", "TRIGGER SET": "GATILHO DEFINIDO", "Ctrl+S: EDIT": "Ctrl+S: EDITAR", "CURRENT TASK": "TAREFA ATUAL", diff --git a/internal/i18n/locales/pt-PT.json b/internal/i18n/locales/pt-PT.json index 2a50e53..f4b3ceb 100644 --- a/internal/i18n/locales/pt-PT.json +++ b/internal/i18n/locales/pt-PT.json @@ -1,4 +1,12 @@ { + "PREVIOUS TURN": "TURNO ANTERIOR", + "LIVE SESSION": "SESSÃO EM DIRETO", + " // %d OF %d": " // %d DE %d", + "[ Alt+←: PREVIOUS ]": "[ Alt+←: ANTERIOR ]", + "[ Alt+→: NEXT ]": "[ Alt+→: SEGUINTE ]", + "[ LIVE ]": "[ DIRETO ]", + "Slash commands cannot be scheduled. Use // for literal slash text.": "Os comandos de barra não podem ser agendados. Use // para texto literal que comece por uma barra.", + "STEER SENT": "ORIENTAÇÃO ENVIADA", "TRIGGER SET": "ACIONADOR DEFINIDO", "Ctrl+S: EDIT": "Ctrl+S: EDITAR", "CURRENT TASK": "TAREFA ATUAL", diff --git a/internal/i18n/locales/ru.json b/internal/i18n/locales/ru.json index 0095b28..65b9566 100644 --- a/internal/i18n/locales/ru.json +++ b/internal/i18n/locales/ru.json @@ -1,4 +1,12 @@ { + "PREVIOUS TURN": "ПРЕДЫДУЩИЙ ХОД", + "LIVE SESSION": "ТЕКУЩИЙ СЕАНС", + " // %d OF %d": " // %d ИЗ %d", + "[ Alt+←: PREVIOUS ]": "[ Alt+←: НАЗАД ]", + "[ Alt+→: NEXT ]": "[ Alt+→: ДАЛЕЕ ]", + "[ LIVE ]": "[ СЕЙЧАС ]", + "Slash commands cannot be scheduled. Use // for literal slash text.": "Слеш-команды нельзя запланировать. Используйте // для обычного текста, начинающегося с косой черты.", + "STEER SENT": "УКАЗАНИЕ ОТПРАВЛЕНО", "TRIGGER SET": "ТРИГГЕР ЗАДАН", "Ctrl+S: EDIT": "Ctrl+S: ИЗМЕНИТЬ", "CURRENT TASK": "ТЕКУЩАЯ ЗАДАЧА", diff --git a/internal/i18n/locales/sv.json b/internal/i18n/locales/sv.json index e1d7cb0..d592398 100644 --- a/internal/i18n/locales/sv.json +++ b/internal/i18n/locales/sv.json @@ -1,4 +1,12 @@ { + "PREVIOUS TURN": "FÖREGÅENDE TUR", + "LIVE SESSION": "LIVESESSION", + " // %d OF %d": " // %d AV %d", + "[ Alt+←: PREVIOUS ]": "[ Alt+←: FÖREGÅENDE ]", + "[ Alt+→: NEXT ]": "[ Alt+→: NÄSTA ]", + "[ LIVE ]": "[ LIVE ]", + "Slash commands cannot be scheduled. Use // for literal slash text.": "Snedstreckskommandon kan inte schemaläggas. Använd // för vanlig text som börjar med ett snedstreck.", + "STEER SENT": "INSTRUKTION SKICKAD", "TRIGGER SET": "UTLÖSARE INSTÄLLD", "Ctrl+S: EDIT": "Ctrl+S: REDIGERA", "CURRENT TASK": "AKTUELL UPPGIFT", diff --git a/internal/i18n/locales/tr.json b/internal/i18n/locales/tr.json index 5aa4b33..3a62bdb 100644 --- a/internal/i18n/locales/tr.json +++ b/internal/i18n/locales/tr.json @@ -1,4 +1,12 @@ { + "PREVIOUS TURN": "ÖNCEKİ TUR", + "LIVE SESSION": "CANLI OTURUM", + " // %d OF %d": " // %d / %d", + "[ Alt+←: PREVIOUS ]": "[ Alt+←: ÖNCEKİ ]", + "[ Alt+→: NEXT ]": "[ Alt+→: SONRAKİ ]", + "[ LIVE ]": "[ CANLI ]", + "Slash commands cannot be scheduled. Use // for literal slash text.": "Eğik çizgi komutları zamanlanamaz. Eğik çizgiyle başlayan düz metin için // kullanın.", + "STEER SENT": "YÖNLENDİRME GÖNDERİLDİ", "TRIGGER SET": "TETİKLEYİCİ AYARLANDI", "Ctrl+S: EDIT": "Ctrl+S: DÜZENLE", "CURRENT TASK": "GEÇERLİ GÖREV", diff --git a/internal/i18n/locales/zh-Hans.json b/internal/i18n/locales/zh-Hans.json index 403cfc1..6cdd0a7 100644 --- a/internal/i18n/locales/zh-Hans.json +++ b/internal/i18n/locales/zh-Hans.json @@ -1,4 +1,12 @@ { + "PREVIOUS TURN": "上一轮", + "LIVE SESSION": "实时会话", + " // %d OF %d": " // %d / %d", + "[ Alt+←: PREVIOUS ]": "[ Alt+←: 上一轮 ]", + "[ Alt+→: NEXT ]": "[ Alt+→: 下一轮 ]", + "[ LIVE ]": "[ 实时 ]", + "Slash commands cannot be scheduled. Use // for literal slash text.": "无法安排斜杠命令。对于以斜杠开头的普通文本,请使用 //。", + "STEER SENT": "追加指令已发送", "TRIGGER SET": "已设触发器", "Ctrl+S: EDIT": "Ctrl+S: 编辑", "CURRENT TASK": "当前任务", diff --git a/internal/statusline/statusline.go b/internal/statusline/statusline.go new file mode 100644 index 0000000..f3e16fa --- /dev/null +++ b/internal/statusline/statusline.go @@ -0,0 +1,99 @@ +// Package statusline defines Codexometer's display-only session footer fields. +// Codex CLI's private picker enum is not an app-server capability catalogue. +package statusline + +import ( + "fmt" + "strings" + + "github.com/merefield/codexometer/internal/codex" +) + +type Field struct { + ID string `json:"id"` + Label string `json:"label"` + Help string `json:"help"` + Default bool `json:"default"` +} + +func Fields() []Field { + return []Field{ + {"model-with-reasoning", "Model and reasoning", "Observed model and reasoning level for this session.", true}, + {"fast-mode", "Speed", "Observed service tier, when available.", true}, + {"used-tokens", "Tokens", "Total observed tokens for this session, including linked agents.", true}, + {"current-dir", "Directory", "Working directory for this session.", true}, + {"model", "Model", "Observed model name without reasoning.", false}, + {"reasoning", "Reasoning", "Observed reasoning level.", false}, + {"thread-name", "Session name", "Session name, when available.", false}, + {"thread-id", "Session ID", "Full session identifier.", false}, + {"run-state", "State", "Observed state, including stale or attention signals.", false}, + {"agents", "Agents", "Number of linked agents, when reported.", false}, + {"source", "Source", "Source of the observed session context.", false}, + } +} + +// Nil means defaults; an explicit empty selection hides the footer. +func Normalize(ids []string) []string { + if ids == nil { + ids = []string{} + for _, f := range Fields() { + if f.Default { + ids = append(ids, f.ID) + } + } + } + valid := map[string]bool{} + for _, f := range Fields() { + valid[f.ID] = true + } + out := make([]string, 0, len(valid)) + seen := map[string]bool{} + for _, id := range ids { + if valid[id] && !seen[id] { + out = append(out, id) + seen[id] = true + } + } + return out +} + +type Data struct { + Model, Effort, Speed, Directory, Name, ID, State, Source string + Tokens int64 + Agents int +} + +func Values(d Data) map[string]string { + combined := strings.TrimSpace(d.Model + " " + d.Effort) + values := map[string]string{"model-with-reasoning": combined, "model": d.Model, "reasoning": d.Effort, "fast-mode": d.Speed, "current-dir": d.Directory, "thread-name": d.Name, "thread-id": d.ID, "run-state": d.State, "source": d.Source} + if d.Tokens >= 0 { + values["used-tokens"] = fmt.Sprintf("%s tokens", count(d.Tokens)) + } + if d.Agents > 0 { + values["agents"] = fmt.Sprintf("%d agents", d.Agents) + } + for k, v := range values { + values[k] = strings.Join(strings.Fields(codex.SanitizeSessionContext(v)), " ") + } + return values +} + +func Text(ids []string, values map[string]string) string { + var parts []string + for _, id := range Normalize(ids) { + if v := strings.Join(strings.Fields(codex.SanitizeSessionContext(values[id])), " "); v != "" { + parts = append(parts, v) + } + } + return strings.Join(parts, " · ") +} + +func count(n int64) string { + if n >= 1000000 { + return fmt.Sprintf("%.1fM", float64(n)/1000000) + } + if n >= 1000 { + return fmt.Sprintf("%.1fK", float64(n)/1000) + } + return fmt.Sprint(n) +} diff --git a/internal/statusline/statusline_test.go b/internal/statusline/statusline_test.go new file mode 100644 index 0000000..97ddec9 --- /dev/null +++ b/internal/statusline/statusline_test.go @@ -0,0 +1,23 @@ +package statusline + +import ( + "reflect" + "strings" + "testing" +) + +func TestSelectionAndSanitizedOneLineValues(t *testing.T) { + values := Values(Data{Model: "model\x1b[31m", Effort: "high", Speed: "fast", Tokens: 1200, Directory: "/work\nproject"}) + if got := Text(nil, values); got != "model high · fast · 1.2K tokens · /work project" { + t.Fatal(got) + } + if Text([]string{}, values) != "" { + t.Fatal("empty selection must hide footer") + } + if got := Normalize([]string{"used-tokens", "invalid", "model", "used-tokens"}); !reflect.DeepEqual(got, []string{"used-tokens", "model"}) { + t.Fatal(got) + } + if got := Text([]string{"thread-name", "reasoning", "model"}, values); got != "high · model" || strings.ContainsAny(got, "\n\x1b") { + t.Fatal(got) + } +} diff --git a/internal/ui/localisation_test.go b/internal/ui/localisation_test.go index 5581745..f4f8bab 100644 --- a/internal/ui/localisation_test.go +++ b/internal/ui/localisation_test.go @@ -60,6 +60,7 @@ func TestLocalisedScreensHelper(t *testing.T) { t.Run("monitor_pill_compression", TestMonitorAttentionProgressiveCompression) t.Run("monitor_pill_selection", TestMonitorSelectedPillMarkersPreserveLayout) t.Run("monitor_context_surfaces", TestMonitorContextResponsiveHitTargets) + t.Run("monitor_history_surfaces", TestMonitorHistoryRenderedControlsAndLiveApproval) t.Run("monitor_privacy_surfaces", TestMonitorContextPrivacyAndDismissRenderedTargets) t.Run("monitor_approval_surfaces", TestMonitorApprovalRenderedTargets) t.Run("monitor_expanded_surfaces", TestExpandedContextResponsiveHitTargets) diff --git a/internal/ui/model.go b/internal/ui/model.go index 02dbcd5..152ba9a 100644 --- a/internal/ui/model.go +++ b/internal/ui/model.go @@ -48,13 +48,20 @@ type Model struct { geometry *monitorGeometryCache scheduleUI scheduleUI monitorQueue monitorQueueState + monitorSteers []monitorSteerReceipt + monitorSteerSequence uint64 monitorPrompt monitorPromptState + monitorCommands monitorCommandsState + monitorSuggestions monitorSuggestionState + monitorStatusLine []string monitorDrafts map[string]string monitorContextHidden bool monitorContextDetail string monitorContextExpanded string monitorContextRows map[string]rowContextState monitorContextScroll int + monitorHistory map[string]monitorTurnHistory + monitorHistorySequence uint64 monitorAttentionPage int monitorApprovalOrder []string monitorApprovalOrderCandidate string @@ -464,6 +471,21 @@ func (m Model) Init() tea.Cmd { } func (m Model) Update(message tea.Msg) (tea.Model, tea.Cmd) { + if next, cmd, handled := m.updateMonitorHistory(message); handled { + return next, cmd + } else { + m = next + } + if next, cmd, handled := m.updateMonitorSuggestions(message); handled { + return next, cmd + } else { + m = next + } + if next, cmd, handled := m.updateMonitorCommands(message); handled { + return next, cmd + } else { + m = next + } colors := paletteFor(m.theme) m.monitorPrompt.input.style(colors) m.monitorQueue.input.style(colors) @@ -1092,6 +1114,7 @@ func (m Model) Update(message tea.Msg) (tea.Model, tea.Cmd) { } m.phase++ commands := []tea.Cmd{secondTick()} + commands = append(commands, m.pollMonitorHistory(time.Time(message))) commands = append(commands, m.pollMonitorQueue()) commands = append(commands, m.dispatchSchedules()) if m.monitorState == monitorRunning { @@ -1526,6 +1549,7 @@ type dashboardGeometry struct { meterY int footerY int headerSpacer bool + footerSpacer bool } func (m Model) dashboardLayout() dashboardGeometry { @@ -1576,9 +1600,15 @@ func (m Model) dashboardLayout() dashboardGeometry { } meterY := tabsY + tabsHeight + extraHeight meterHeight := max(contentHeight-headerHeight-statusHeight-tabsHeight-extraHeight-footerHeight, 1) + footerSpacing := 0 + // Keep the minimum eight-row detail/composer layout on short terminals. + if meterHeight >= 9 && m.monitorStatusLineVisible(contentWidth) { + footerSpacing = 1 + meterHeight-- + } footerY := meterY if m.meterView == viewUsage || m.meterView == viewResets || m.meterView == viewThresholds || m.meterView == viewMonitor || m.meterView == viewBenchmark || len(m.snapshot.Meters()) > 0 { - footerY += meterHeight + footerY += meterHeight + footerSpacing } return dashboardGeometry{ contentWidth: contentWidth, @@ -1588,6 +1618,7 @@ func (m Model) dashboardLayout() dashboardGeometry { meterY: meterY, footerY: footerY, headerSpacer: statusHeight > 0, + footerSpacer: footerSpacing > 0, } } @@ -2362,6 +2393,9 @@ func (m Model) applyMonitorFetch(message monitorFetchedMsg) (tea.Model, tea.Cmd, m.monitorNextFetch = time.Time{} } } + if accepted { + m.reconcileMonitorSteers() + } if accepted && message.kind != monitorFetchSample && message.kind != monitorFetchBoundary { m.refreshMonitorRates(message.at, true) } @@ -2660,6 +2694,7 @@ func sameOptionalInt64(left, right *int64) bool { } func (m *Model) startMonitorSessions(usage codex.LiveUsageSnapshot, observedAt time.Time) { + m.monitorHistory = nil m.monitorSessionData = nil m.monitorDismissed = nil for _, session := range usage.Sessions { @@ -2683,6 +2718,7 @@ func (m *Model) startMonitorSessions(usage codex.LiveUsageSnapshot, observedAt t active: true, displayed: true, unattributed: true, }) } + m.observeMonitorHistory() } func (m *Model) syncMonitorSessions(usage codex.LiveUsageSnapshot, observedAt time.Time) { @@ -2698,7 +2734,8 @@ func (m *Model) syncMonitorSessions(usage codex.LiveUsageSnapshot, observedAt ti } } defer func() { - if follow { + m.observeMonitorHistory() + if _, historical := m.historicalContext(); follow && !historical { m.monitorContextScroll = m.monitorContextScrollLimit() } }() diff --git a/internal/ui/monitor_approval.go b/internal/ui/monitor_approval.go index eff1a16..a5b1323 100644 --- a/internal/ui/monitor_approval.go +++ b/internal/ui/monitor_approval.go @@ -245,6 +245,7 @@ func (m Model) renderMonitorApprovalControls(width, height int, colors palette) } func (m Model) monitorApprovalAction(action string) (Model, tea.Cmd, bool) { + m.showLiveMonitorHistory() token := m.monitorApprovalToken() if token == "" { m.monitorApprovalConfirm = "" diff --git a/internal/ui/monitor_approval_shortcut_test.go b/internal/ui/monitor_approval_shortcut_test.go index 99b997b..8486be6 100644 --- a/internal/ui/monitor_approval_shortcut_test.go +++ b/internal/ui/monitor_approval_shortcut_test.go @@ -7,8 +7,131 @@ import ( tea "charm.land/bubbletea/v2" "github.com/charmbracelet/x/ansi" + "github.com/merefield/codexometer/internal/codex" ) +func TestApprovalShortcutAfterFocusedComposer(t *testing.T) { + for _, wide := range []bool{false, true} { + for _, enhanced := range []bool{false, true} { + m := approvalTestModel() + m.keyboardEventTypes = enhanced + if wide { + m.width, m.height = 180, 60 + m.monitorSessionData = m.monitorSessionData[:1] + m.setRowContext("root-one", contextWide) + } + m.monitorPrompt = monitorPromptState{session: "root-one", offer: codex.SessionPromptOffer{Token: "previous", TurnID: "turn"}, input: newMonitorEditor()} + m.monitorPrompt.input.SetValue("Keep my unsent draft") + m.monitorPrompt.input.Focus() + // Even an old armed confirmation must not make the transition + // key grant permission immediately. + m.monitorApprovalConfirm = "live/decision:0" + m.monitorApprovalConfirmUntil = time.Now().Add(time.Minute) + m.monitorApprovalNumberReleased = true + m, cmd := approvalEvent(m, key('1')) + if cmd != nil || m.monitorApprovalBusy || m.monitorApprovalConfirm != "live/decision:0" || m.monitorPrompt.input.Focused() || m.monitorApprovalNumberReleased { + t.Fatalf("wide=%v enhanced=%v: first number must only select approval", wide, enhanced) + } + if m.monitorDrafts["root-one"] != "Keep my unsent draft" { + t.Fatal("approval transition lost ordinary draft") + } + if enhanced { + m, _ = approvalEvent(m, tea.KeyReleaseMsg{Code: '1'}) + m, cmd = approvalEvent(m, key('1')) + } else { + m, cmd = approvalEvent(m, key('c')) + } + if cmd == nil || !m.monitorApprovalBusy { + t.Fatal("separate confirmation did not submit") + } + cmd() + if m.fetcher.(*approvalTestClient).decision != "accept" { + t.Fatal("wrong decision sent") + } + } + } +} + +func TestApprovalShortcutAfterComposerTelemetryTransition(t *testing.T) { + for _, wide := range []bool{false, true} { + for _, enhanced := range []bool{false, true} { + m := approvalTestModel() + m.keyboardEventTypes = enhanced + if wide { + m.width, m.height = 180, 60 + m.monitorSessionData = m.monitorSessionData[:1] + m.setRowContext("root-one", contextWide) + } + m.monitorPrompt = monitorPromptState{session: "root-one", offer: codex.SessionPromptOffer{Token: "previous", TurnID: "turn"}, input: newMonitorEditor()} + m.monitorPrompt.input.SetValue("Keep my draft") + m.monitorPrompt.input.Focus() + // An intervening redraw/telemetry message blurs the composer before + // the user's first approval key, unlike the direct key handoff test. + m, _ = approvalEvent(m, tea.MouseMotionMsg{X: 0, Y: 0}) + if m.monitorPrompt.input.Focused() || m.monitorPrompt.offer.Token != "" { + t.Fatal("approval did not retire the old composer capability") + } + m, cmd := approvalEvent(m, key('1')) + if cmd != nil || m.monitorApprovalConfirm != "live/decision:0" { + t.Fatalf("wide=%v enhanced=%v: first approval number swallowed after telemetry", wide, enhanced) + } + if m.monitorDrafts["root-one"] != "Keep my draft" { + t.Fatal("telemetry transition lost ordinary draft") + } + m, _ = approvalEvent(m, tea.MouseMotionMsg{X: 1, Y: 0}) + if m.monitorApprovalConfirm != "live/decision:0" { + t.Fatal("subsequent redraw cleared the armed approval") + } + if enhanced { + m, _ = approvalEvent(m, tea.KeyReleaseMsg{Code: '1'}) + m, cmd = approvalEvent(m, key('1')) + } else { + m, cmd = approvalEvent(m, key('c')) + } + if cmd == nil || !m.monitorApprovalBusy { + t.Fatal("confirmation swallowed after telemetry transition") + } + } + } +} + +func TestApprovalComposerTransitionDoesNotReinterpretOtherInput(t *testing.T) { + for _, scenario := range []string{"decline", "cancel", "confirm", "repeat", "different-session", "clipped", "paste", "secret"} { + t.Run(scenario, func(t *testing.T) { + m := approvalTestModel() + m.monitorPrompt = monitorPromptState{session: "root-one", offer: codex.SessionPromptOffer{Token: "previous", TurnID: "turn"}, input: newMonitorEditor()} + m.monitorPrompt.input.SetValue("draft") + m.monitorPrompt.input.Focus() + var msg tea.Msg = key('1') + switch scenario { + case "decline": + msg = key('2') + case "cancel": + msg = key('3') + case "confirm": + msg = key('c') + case "repeat": + msg = tea.KeyPressMsg{Code: '1', IsRepeat: true} + case "different-session": + m.monitorPrompt.session = "root-two" + case "clipped": + m.width, m.height = 40, 12 + case "paste": + msg = tea.PasteMsg{Content: "1"} + case "secret": + m.monitorPrompt.input.secret = true + } + m, cmd := approvalEvent(m, msg) + if cmd != nil || m.monitorApprovalBusy || m.monitorApprovalConfirm != "" { + t.Fatal("unrelated input became an approval action") + } + if scenario == "secret" && len(m.monitorDrafts) != 0 { + t.Fatal("secret answer persisted as an ordinary draft") + } + }) + } +} + func approvalEvent(m Model, msg tea.Msg) (Model, tea.Cmd) { n, cmd := m.Update(msg) return n.(Model), cmd diff --git a/internal/ui/monitor_commands.go b/internal/ui/monitor_commands.go new file mode 100644 index 0000000..19afa50 --- /dev/null +++ b/internal/ui/monitor_commands.go @@ -0,0 +1,459 @@ +package ui + +import ( + "context" + "fmt" + "strings" + "time" + + tea "charm.land/bubbletea/v2" + "github.com/charmbracelet/x/ansi" + "github.com/merefield/codexometer/internal/codex" + "github.com/merefield/codexometer/internal/statusline" +) + +type monitorCommandsState struct { + open, busy, detail bool + session string + request uint64 + menu codex.SessionCommandMenu + selected, scroll, hover int + notice string + until time.Time + footerHover string +} +type monitorCommandsResult struct { + session string + path string + request uint64 + menu codex.SessionCommandMenu + applied bool + err error +} + +func (m *Model) loadMonitorCommands(path string) tea.Cmd { + p := &m.monitorCommands + path = strings.TrimPrefix(strings.TrimSpace(path), "/") + if strings.TrimPrefix(strings.TrimSpace(path), "/") == "statusline" { + p.busy = false + p.detail = false + p.notice = "" + p.selected = 0 + p.scroll = 0 + p.request++ + p.menu = m.statusLineMenu() + return nil + } + c, ok := m.fetcher.(codex.SessionCommandsClient) + if !ok { + if path == "" || path == "help" { + p.menu = localCommandsMenu() + p.busy = false + p.notice = "" + return nil + } + p.notice = "Shared app-server commands unavailable." + return nil + } + p.busy = true + p.detail = false + p.notice = "" + p.selected = 0 + p.scroll = 0 + p.hover = -1 + p.request++ + id, seq := p.session, p.request + return func() tea.Msg { + ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second) + defer cancel() + menu, err := c.SessionCommands(ctx, id, path) + return monitorCommandsResult{session: id, path: path, request: seq, menu: menu, err: err} + } +} + +func (m Model) openMonitorCommands(path string) (Model, tea.Cmd, bool) { + path = strings.TrimPrefix(strings.TrimSpace(path), "/") + if _, ok := m.fetcher.(codex.SessionCommandsClient); !ok && path != "statusline" && path != "" && path != "help" { + m.monitorPrompt.notice = "Slash commands require the shared app-server." + return m, nil, true + } + id := m.monitorContextTarget() + if id == "" { + return m, nil, true + } + m.monitorPrompt.input.Blur() + m.monitorContextDetail = id + m.monitorCommands = monitorCommandsState{open: true, session: id, request: m.monitorCommands.request, hover: -1} + cmd := m.loadMonitorCommands(path) + return m, cmd, true +} + +func (m *Model) closeMonitorCommands() { + m.monitorCommands.open = false + m.monitorCommands.request++ + m.monitorCommands.busy = false +} + +func (m Model) commandChoice() (codex.SessionCommandChoice, bool) { + p := m.monitorCommands + if p.selected < 0 || p.selected >= len(p.menu.Choices) { + return codex.SessionCommandChoice{}, false + } + return p.menu.Choices[p.selected], true +} + +func (m Model) chooseMonitorCommand() (Model, tea.Cmd, bool) { + o, ok := m.commandChoice() + if !ok || m.monitorCommands.busy { + return m, nil, true + } + if o.Next != "" { + cmd := m.loadMonitorCommands(o.Next) + return m, cmd, true + } + if m.monitorCommands.menu.Multiple { + p := &m.monitorCommands + p.menu.Choices = append([]codex.SessionCommandChoice(nil), p.menu.Choices...) + p.menu.Choices[p.selected].Selected = !o.Selected + return m, nil, true + } + m.monitorCommands.detail = true + m.monitorCommands.scroll = 0 + m.monitorCommands.until = time.Now().Add(30 * time.Second) + return m, nil, true +} + +func (m Model) confirmMonitorCommand() (Model, tea.Cmd, bool) { + p := &m.monitorCommands + if p.open && p.menu.Multiple && p.menu.Path == "statusline" { + m.monitorStatusLine = statusline.Normalize(statusLineSelection(p.menu)) + if codex.IsSessionCommand(m.monitorPrompt.input.Value()) { + m.monitorPrompt.input.Reset() + } + m.persistPreferences() + m.closeMonitorCommands() + return m, nil, true + } + o, ok := m.commandChoice() + if !p.detail || !ok || !o.Action || p.busy || p.notice != "" { + return m, nil, true + } + if time.Now().After(p.until) { + p.notice = "Confirmation expired; reopen the option." + return m, nil, true + } + c, ok := m.fetcher.(codex.SessionCommandsClient) + if !ok { + return m, nil, true + } + p.busy = true + p.request++ + id, seq, path, rev := p.session, p.request, p.menu.Path, p.menu.Revision + return m, func() tea.Msg { + ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second) + defer cancel() + err := c.ExecuteSessionCommand(ctx, id, path, rev, o.ID) + return monitorCommandsResult{session: id, request: seq, applied: true, err: err} + }, true +} + +type commandDisplayRow struct { + text string + choice int + action string +} + +func (m Model) monitorCommandRows(width, height int) []commandDisplayRow { + p := m.monitorCommands + w := max(width-4, 1) + capacity := max(height-5, 1) + if p.menu.Multiple { + capacity = max(height-6, 1) + } + var rows []commandDisplayRow + if p.busy { + return []commandDisplayRow{{text: "Loading / sending…", choice: -1}} + } + if p.notice != "" { + for _, line := range strings.Split(ansi.Hardwrap(p.notice, w, true), "\n") { + rows = append(rows, commandDisplayRow{text: line, choice: -1}) + } + return rows + } + if p.detail { + o, ok := m.commandChoice() + if !ok { + return nil + } + text := o.Label + "\n\n" + p.menu.Help + "\n\n" + o.Help + if o.Action { + text += "\n\nTARGET // " + p.session + "\nChanges apply to subsequent turns of this session. They are not global defaults. Confirm only after reviewing the option. Automatic quota thresholds may later supersede model settings." + } + lines := strings.Split(ansi.Hardwrap(text, w, true), "\n") + start := min(p.scroll, max(len(lines)-capacity, 0)) + for _, line := range lines[start:min(start+capacity, len(lines))] { + rows = append(rows, commandDisplayRow{text: line, choice: -1}) + } + return rows + } + start := max(p.selected-capacity+1, 0) + for i := start; i < min(start+capacity, len(p.menu.Choices)); i++ { + o := p.menu.Choices[i] + label := o.Label + if p.menu.Multiple { + mark := "[ ] " + if o.Selected { + mark = "[x] " + } + label = mark + label + " // " + o.Help + } else if o.Next != "" { + label += " →" + } else if !o.Action { + label += " // HELP" + } + rows = append(rows, commandDisplayRow{text: ansi.Truncate(label, w, "…"), choice: i, action: "choose"}) + } + if len(rows) == 0 { + rows = append(rows, commandDisplayRow{text: "No available options. Use Codex for unsupported commands.", choice: -1}) + } + if p.menu.Multiple { + preview := statusline.Text(statusLineSelection(p.menu), m.monitorStatusValues()) + if preview == "" { + preview = "—" + } + rows = append(rows, commandDisplayRow{text: ansi.Truncate("Preview // "+preview, w, "…"), choice: -1}) + } + return rows +} + +func (m Model) commandFooter(width int) []commandDisplayRow { + p := m.monitorCommands + var buttons []commandDisplayRow + if p.menu.Multiple { + buttons = append(buttons, commandDisplayRow{text: "[ C APPLY ]", action: "confirm"}) + } else if o, ok := m.commandChoice(); ok && p.detail && o.Action && !p.busy && p.notice == "" { + buttons = append(buttons, commandDisplayRow{text: "[ C CONFIRM ]", action: "confirm"}) + } + buttons = append(buttons, commandDisplayRow{text: "[ ← BACK ]", action: "back"}, commandDisplayRow{text: "[ ESC CLOSE ]", action: "close"}) + used := 0 + for i, b := range buttons { + if used+ansi.StringWidth(b.text) > max(width-4, 0) { + return buttons[:i] + } + used += ansi.StringWidth(b.text) + 2 + } + return buttons +} + +func (m Model) renderMonitorCommands(width, height int, colors palette) string { + p := m.monitorCommands + rows := m.monitorCommandRows(width, height) + body := make([]string, max(height-2, 1)) + for i, r := range rows { + if i >= len(body)-2 { + break + } + style := colors.label() + if r.action != "" && (r.choice == p.selected || r.choice == p.hover) { + style = style.Foreground(colors.primary).Bold(true) + } + body[i] = style.Render(ansi.Truncate(r.text, max(width-4, 1), "")) + } + var footer []string + for _, b := range m.commandFooter(width) { + style := colors.label() + if p.footerHover == b.action { + style = style.Foreground(colors.primary).Bold(true).Reverse(true) + } + footer = append(footer, style.Render(b.text)) + } + if len(body) > 1 { + hint := "↑/↓ select or scroll • Enter open • / root" + if p.menu.Multiple { + hint = "↑/↓ select • Space toggle • ←/→ order • C apply" + } + body[len(body)-2] = colors.dimmed().Render(ansi.Truncate(hint, max(width-4, 1), "")) + body[len(body)-1] = strings.Join(footer, " ") + } + identity := shortSessionID(p.session) + if s, ok := m.contextDetailSession(); ok { + identity = monitorSessionIdentity(s) + } + title := fmt.Sprintf("%s // %s", p.menu.Title, identity) + return frameSizedWithActions(width, max(height-2, 1), title, "", "", strings.Join(body, "\n"), colors.primary, colors) +} + +func (m Model) updateMonitorCommands(msg tea.Msg) (Model, tea.Cmd, bool) { + p := &m.monitorCommands + if r, ok := msg.(monitorCommandsResult); ok { + if p.open && p.session == r.session && p.request == r.request { + if m.meterView != viewMonitor || m.monitorContextTarget() != r.session { + m.closeMonitorCommands() + return m, nil, true + } + p.busy = false + p.notice = "" + if r.err != nil { + if !r.applied && (r.path == "" || r.path == "help") { + p.menu = localCommandsMenu() + } else { + p.notice = "Command unavailable or unconfirmed. Check Codex before retrying." + } + } else if r.applied { + // Applying settings invalidates both the option revision and + // the root suggestion catalogue. Reopen only from a fresh read. + m.monitorCommands = monitorCommandsState{request: p.request + 1} + m.monitorSuggestions = monitorSuggestionState{request: m.monitorSuggestions.request + 1} + if codex.IsSessionCommand(m.monitorPrompt.input.Value()) { + m.monitorPrompt.input.Reset() + } + if offer := m.monitorPromptOffer(); offer.Token != "" && len(offer.Questions) == 0 && len(m.monitorPrompt.offer.Questions) == 0 { + m.monitorPrompt.offer = offer + m.monitorPrompt.session = r.session + } + cmd := m.focusMonitorPrompt() + m.monitorPrompt.session = r.session + m.monitorPrompt.notice = "Change requested. Codex will apply it to subsequent turns." + return m, cmd, true + } else { + p.menu = r.menu + if p.menu.Path == "" || p.menu.Path == "help" { + p.menu.Choices = append(p.menu.Choices, localStatusLineChoice()) + } + } + } + return m, nil, true + } + if !p.open { + if key, ok := msg.(tea.KeyPressMsg); ok && key.String() == "/" && m.meterView == viewMonitor && m.monitorContextDetail != "" && !m.monitorPrompt.input.Focused() && !m.scheduleUI.open && !m.monitorQueue.open { + return m.openMonitorCommands("") + } + return m, nil, false + } + if m.meterView != viewMonitor || m.monitorContextTarget() != p.session { + m.closeMonitorCommands() + return m, nil, false + } + back := func() tea.Cmd { + if p.detail { + p.detail = false + p.scroll = 0 + p.notice = "" + return nil + } + path := p.menu.Path + if i := strings.LastIndex(path, "/"); i >= 0 { + path = path[:i] + } else { + path = "" + } + return m.loadMonitorCommands(path) + } + if key, ok := msg.(tea.KeyPressMsg); ok { + if p.menu.Multiple { + switch key.String() { + case "space": + return m.chooseMonitorCommand() + case "left", "right": + d := 1 + if key.String() == "left" { + d = -1 + } + target := p.selected + d + if target >= 0 && target < len(p.menu.Choices) { + p.menu.Choices = append([]codex.SessionCommandChoice(nil), p.menu.Choices...) + p.menu.Choices[p.selected], p.menu.Choices[target] = p.menu.Choices[target], p.menu.Choices[p.selected] + p.selected = target + } + return m, nil, true + } + } + switch key.String() { + case "esc": + m.closeMonitorCommands() + case "q", "ctrl+c", "tab", "shift+tab": + m.closeMonitorCommands() + return m, nil, false + case "t", "r": + return m, nil, false + case "left", "backspace": + return m, back(), true + case "/": + cmd := m.loadMonitorCommands("") + return m, cmd, true + case "enter", "right": + if !p.detail { + return m.chooseMonitorCommand() + } + case "c", "C": + return m.confirmMonitorCommand() + case "up", "down", "pgup", "pgdown": + d := 1 + if key.String() == "up" || key.String() == "pgup" { + d = -1 + } + if strings.HasPrefix(key.String(), "pg") { + d *= 10 + } + if p.detail { + p.scroll = max(0, p.scroll+d) + } else { + p.selected = min(max(0, p.selected+d), max(len(p.menu.Choices)-1, 0)) + } + } + return m, nil, true + } + if mouse, ok := msg.(tea.MouseMsg); ok { + g := m.monitorDashboardLayout() + v := mouse.Mouse() + x, y := v.X-4, v.Y-g.meterY-1 + if x < 0 || x >= g.contentWidth-4 || y < 0 || y >= g.meterHeight-2 { + return m, nil, false + } + if v.Button == tea.MouseWheelUp || v.Button == tea.MouseWheelDown { + d := 3 + if v.Button == tea.MouseWheelUp { + d = -3 + } + if p.detail { + p.scroll = max(0, p.scroll+d) + } else { + p.selected = min(max(0, p.selected+d), max(len(p.menu.Choices)-1, 0)) + } + return m, nil, true + } + rows := m.monitorCommandRows(g.contentWidth, g.meterHeight) + p.hover = -1 + p.footerHover = "" + _, click := msg.(tea.MouseClickMsg) + if y < len(rows) && rows[y].action == "choose" { + p.hover = rows[y].choice + if click && v.Button == tea.MouseLeft { + p.selected = p.hover + return m.chooseMonitorCommand() + } + } + if y == g.meterHeight-3 { + for _, b := range m.commandFooter(g.contentWidth) { + w := ansi.StringWidth(b.text) + if x >= 0 && x < w { + p.footerHover = b.action + if click && v.Button == tea.MouseLeft { + switch b.action { + case "confirm": + return m.confirmMonitorCommand() + case "back": + return m, back(), true + case "close": + m.closeMonitorCommands() + } + } + break + } + x -= w + 2 + } + } + return m, nil, true + } + return m, nil, false +} diff --git a/internal/ui/monitor_commands_test.go b/internal/ui/monitor_commands_test.go new file mode 100644 index 0000000..04daaa9 --- /dev/null +++ b/internal/ui/monitor_commands_test.go @@ -0,0 +1,207 @@ +package ui + +import ( + "context" + "errors" + "strconv" + "strings" + "testing" + "time" + + tea "charm.land/bubbletea/v2" + "github.com/charmbracelet/x/ansi" + "github.com/merefield/codexometer/internal/codex" +) + +type commandTestClient struct { + *promptTestClient + calls int +} + +func (f *commandTestClient) SessionCommands(context.Context, string, string) (codex.SessionCommandMenu, error) { + return codex.SessionCommandMenu{Title: "/ COMMANDS", Path: "model/test", Revision: "r", Choices: []codex.SessionCommandChoice{{ID: "choice", Label: "Medium", Help: "Live effort help", Action: true}}}, nil +} +func (f *commandTestClient) ExecuteSessionCommand(context.Context, string, string, string, string) error { + f.calls++ + return nil +} + +func commandTestModel(t *testing.T) (Model, *commandTestClient) { + t.Helper() + m, p := promptTestModel() + f := &commandTestClient{promptTestClient: p} + m.fetcher = f + m.monitorPrompt.input = newMonitorEditor() + m, cmd, _ := m.openMonitorCommands("") + if cmd == nil { + t.Fatal("no catalogue request") + } + n, _ := m.Update(cmd()) + return n.(Model), f +} +func TestMonitorCommandsConfirmationAndDraft(t *testing.T) { + m, f := commandTestModel(t) + m.monitorPrompt.input.SetValue("keep draft") + m, _, _ = m.chooseMonitorCommand() + if !m.monitorCommands.detail || f.calls != 0 { + t.Fatal("selection should show help, not execute") + } + n, cmd := m.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) + m = n.(Model) + if cmd != nil || f.calls != 0 { + t.Fatal("Enter bypassed explicit confirmation") + } + n, cmd = m.Update(key('c')) + m = n.(Model) + if cmd == nil { + t.Fatal("missing confirmation command") + } + msg := cmd() + n, _ = m.Update(msg) + m = n.(Model) + if f.calls != 1 || m.monitorPrompt.input.Value() != "keep draft" { + t.Fatal("wrong command/draft handling") + } + _, again, _ := m.confirmMonitorCommand() + if again != nil { + t.Fatal("success notice allowed a second send") + } + n, _ = m.Update(tea.KeyPressMsg{Code: tea.KeyEscape}) + m = n.(Model) + if m.monitorCommands.open || m.monitorContextDetail != "root-one" { + t.Fatal("Escape failed to restore detail") + } +} +func TestMonitorCommandsRenderedClickTargets(t *testing.T) { + for _, width := range []int{60, 120, 180} { + m, f := commandTestModel(t) + m.width = width + x, y := renderedTextStart(t, m, "Medium") + n, _ := m.Update(tea.MouseClickMsg{X: x, Y: y, Button: tea.MouseLeft}) + m = n.(Model) + if !m.monitorCommands.detail { + t.Fatalf("width %d choice missed: %s", width, ansi.Strip(m.render())) + } + x, y = renderedTextStart(t, m, "[ C CONFIRM ]") + n, cmd := m.Update(tea.MouseClickMsg{X: x, Y: y, Button: tea.MouseLeft}) + m = n.(Model) + if cmd == nil { + t.Fatalf("width %d confirm missed", width) + } + cmd() + if f.calls != 1 { + t.Fatal("wrong click action") + } + } +} +func TestMonitorCommandsIgnoreStaleResultsAndExpiredConfirmation(t *testing.T) { + m, f := commandTestModel(t) + m, _, _ = m.chooseMonitorCommand() + m.monitorCommands.until = time.Now().Add(-time.Second) + _, cmd, _ := m.confirmMonitorCommand() + if cmd != nil || f.calls != 0 { + t.Fatal("expired confirmation sent") + } + m.closeMonitorCommands() + n, _ := m.Update(monitorCommandsResult{session: "root-one", request: m.monitorCommands.request - 1, menu: codex.SessionCommandMenu{Title: "stale"}}) + if n.(Model).monitorCommands.open { + t.Fatal("late result reopened menu") + } +} + +type revisionCommandClient struct { + *commandTestClient + revision, reads int + reject bool +} + +func (f *revisionCommandClient) SessionCommands(ctx context.Context, id, path string) (codex.SessionCommandMenu, error) { + f.reads++ + menu, err := f.commandTestClient.SessionCommands(ctx, id, path) + menu.Revision = strconv.Itoa(f.revision) + return menu, err +} + +func (f *revisionCommandClient) ExecuteSessionCommand(_ context.Context, _, _, revision, _ string) error { + f.calls++ + if f.reject || revision != strconv.Itoa(f.revision) { + return errors.New("stale or rejected command") + } + f.revision++ + return nil +} + +func TestMonitorCommandsSuccessfulChangesDiscardOldCatalogues(t *testing.T) { + m, base := commandTestModel(t) + f := &revisionCommandClient{commandTestClient: base} + m.fetcher = f + for attempt := 0; attempt < 2; attempt++ { + var cmd tea.Cmd + m, cmd, _ = m.openMonitorCommands("model/test") + m, _ = approvalEvent(m, cmd()) + if m.monitorCommands.menu.Revision != strconv.Itoa(attempt) { + t.Fatal("reopened commands did not fetch a fresh revision") + } + m.monitorPrompt.input.SetValue("/model") + m.monitorSuggestions = monitorSuggestionState{active: true, busy: true, session: "root-one", request: 42, menu: m.monitorCommands.menu} + m, _, _ = m.chooseMonitorCommand() + m, cmd, _ = m.confirmMonitorCommand() + if cmd == nil { + t.Fatal("missing confirmed change") + } + m, _ = approvalEvent(m, cmd()) + if m.monitorCommands.open || m.monitorCommands.detail || m.monitorCommands.menu.Revision != "" || len(m.monitorCommands.menu.Choices) > 0 || m.monitorSuggestions.active || len(m.monitorSuggestions.menu.Choices) > 0 { + t.Fatal("successful change retained old menus") + } + if m.monitorPrompt.input.Value() != "" || !m.monitorPrompt.input.Focused() || !strings.Contains(ansi.Strip(m.render()), "Change requested.") { + t.Fatal("success did not clear and focus composer with a visible notice") + } + m, _ = approvalEvent(m, monitorSuggestionResult{session: "root-one", request: 42, menu: codex.SessionCommandMenu{Revision: "stale"}}) + if m.monitorSuggestions.menu.Revision != "" { + t.Fatal("late suggestions restored an invalidated revision") + } + m.monitorPrompt.input.SetValue("/") + cmd = m.syncMonitorSuggestions() + if cmd == nil { + t.Fatal("typing slash reused the invalidated catalogue") + } + m, _ = approvalEvent(m, cmd()) + if m.monitorSuggestions.menu.Revision != strconv.Itoa(attempt+1) { + t.Fatal("suggestions did not fetch the new revision") + } + } + if f.calls != 2 || f.reads != 4 { + t.Fatalf("expected two independent confirmations and fresh reads: calls=%d reads=%d", f.calls, f.reads) + } +} + +func TestMonitorCommandsFailedChangeRetainsDraftAndError(t *testing.T) { + m, base := commandTestModel(t) + f := &revisionCommandClient{commandTestClient: base, reject: true} + m.fetcher = f + m.monitorPrompt.input.SetValue("/model") + m, _, _ = m.chooseMonitorCommand() + m, cmd, _ := m.confirmMonitorCommand() + m, _ = approvalEvent(m, cmd()) + if !m.monitorCommands.open || !m.monitorCommands.detail || m.monitorPrompt.input.Value() != "/model" || !strings.Contains(ansi.Strip(m.render()), "Command unavailable or unconfirmed") { + t.Fatal("failed change lost its draft or error context") + } + _, cmd, _ = m.confirmMonitorCommand() + if cmd != nil || f.calls != 1 { + t.Fatal("failed change allowed an automatic retry") + } +} + +func TestMonitorCommandsLateSuccessCannotClearAnotherSessionDraft(t *testing.T) { + m, _ := commandTestModel(t) + m, _, _ = m.chooseMonitorCommand() + m, cmd, _ := m.confirmMonitorCommand() + result := cmd() + m.monitorContextDetail = "root-two" + m.monitorPrompt.session = "root-two" + m.monitorPrompt.input.SetValue("/keep this other draft") + m, cmd = approvalEvent(m, result) + if cmd != nil || m.monitorCommands.open || m.monitorPrompt.session != "root-two" || m.monitorPrompt.input.Value() != "/keep this other draft" || m.monitorPrompt.notice != "" { + t.Fatal("late success modified the newly selected session") + } +} diff --git a/internal/ui/monitor_context.go b/internal/ui/monitor_context.go index d601ccf..774e23a 100644 --- a/internal/ui/monitor_context.go +++ b/internal/ui/monitor_context.go @@ -184,11 +184,18 @@ func (m Model) contextDetailSession() (monitorSession, bool) { } func (m Model) renderMonitorContextDetail(width, height int, colors palette) string { + if m.monitorCommands.open { + return m.renderMonitorCommands(width, height, colors) + } if m.monitorQueue.open { return m.renderQueueEditor() } if m.scheduleUI.open { - return m.renderScheduleDetail(width, height, colors) + panel := m.renderScheduleDetail(width, height, colors) + if m.dashboardLayout().footerSpacer { + return m.withMonitorStatusLine(panel, width, "", colors) + } + return panel } document := m.contextDetailDocument(max(width-4, 1)) lines := make([]string, len(document)) @@ -198,12 +205,15 @@ func (m Model) renderMonitorContextDetail(width, height int, colors palette) str rows := max(height-2, 1) layout := m.layoutDetailControls(width, height) controls := layout.render(m, width, height, colors) - textRows, gap, _ := monitorContextBodyLayout(height, layout.rows) + textRows, gap, _ := m.detailBodyLayout(width, height, layout.rows) start := min(max(m.monitorContextScroll, 0), max(len(lines)-textRows, 0)) end := min(start+textRows, len(lines)) bodyLines := append([]string(nil), lines[start:end]...) + if m.historyNavigationRows(width, height) > 0 { + bodyLines = append([]string{m.renderMonitorNavigationButtons(m.historyNavigationButtons(width, height), colors)}, bodyLines...) + } if controls != "" { - for len(bodyLines) < textRows+gap { + for len(bodyLines) < textRows+gap+m.historyNavigationRows(width, height) { bodyLines = append(bodyLines, "") } bodyLines = append(bodyLines, controls) @@ -211,7 +221,12 @@ func (m Model) renderMonitorContextDetail(width, height int, colors palette) str body := strings.Join(bodyLines, "\n") action := m.renderMonitorNavigation(width, m.monitorContextDetail, true, colors) title := m.monitorDetailTitle(width, colors) - return frameSizedWithActions(width, rows, title, action, m.renderMonitorCopy(width, m.monitorContextDetail, colors), body, colors.primary, colors) + copyLabel := m.renderMonitorCopy(width, m.monitorContextDetail, colors) + panel := frameSizedWithActions(width, rows, title, action, copyLabel, body, colors.primary, colors) + if m.dashboardLayout().footerSpacer { + return m.withMonitorStatusLine(panel, width, copyLabel, colors) + } + return panel } func (m Model) monitorDetailTitle(width int, colors palette) string { @@ -230,6 +245,9 @@ func (m Model) monitorDetailTitle(width int, colors palette) string { if m.hasSessionProfile(s) { title = i18n.Text("QUOTA THRESHOLD") } + if _, historical := m.historicalContext(); historical { + title = i18n.Text("PREVIOUS TURN") + " // " + i18n.Text("LIVE SESSION") + ": " + title + } title += " // " + monitorSessionIdentity(s) } return title @@ -279,7 +297,7 @@ func (m *Model) scrollMonitorContext(delta int) { func (m Model) monitorContextScrollLimit() int { g := m.monitorDashboardLayout() layout := m.layoutDetailControls(g.contentWidth, g.meterHeight) - rows, _, _ := monitorContextBodyLayout(g.meterHeight, layout.rows) + rows, _, _ := m.detailBodyLayout(g.contentWidth, g.meterHeight, layout.rows) limit := max(len(m.contextDetailLines(max(g.contentWidth-4, 1)))-rows, 0) return limit } @@ -386,12 +404,17 @@ func (m Model) monitorContextAt(x, y int) string { return hit } if m.monitorContextDetail != "" && !m.contextTargetHidden() { + if !m.monitorCommands.open && !m.monitorQueue.open && !m.scheduleUI.open { + if hit := monitorNavigationButtonsHit(m.historyNavigationButtons(g.contentWidth, g.meterHeight), x, y); hit != "" { + return hit + } + } if hit := m.monitorNavigationHit(g.contentWidth, m.monitorContextDetail, true, x, y); hit != "" { return hit } if rows := m.monitorPromptRows(g.contentWidth, g.meterHeight); rows > 0 && m.monitorPromptOffer().Token != "" { _, _, controlY := monitorContextBodyLayout(g.meterHeight, rows) - inEditor := y >= controlY+1 && y < controlY+rows-1 && x >= 2 && x < g.contentWidth-2 + inEditor := y >= controlY+m.monitorPromptHeaderRows() && y < controlY+rows-1 && x >= 2 && x < g.contentWidth-2 // Include the full-width bottom three rows of the detail frame, // without taking clicks from the global footer below it. inBottom := y >= g.meterHeight-3 && y < g.meterHeight && x >= 0 && x < g.contentWidth @@ -505,12 +528,19 @@ func (m Model) updateMonitorContextMouse(msg tea.MouseMsg) (Model, tea.Cmd, bool case "session-down": m.selectMonitorSession(1) case "prompt": + m.showLiveMonitorHistory() cmd := m.focusMonitorPrompt() return m, cmd, true case "privacy": m.toggleMonitorContext() case "close": m.stepBackMonitorContext() + case "history:previous": + m.moveMonitorHistory(-1) + case "history:next": + m.moveMonitorHistory(1) + case "history:live": + m.showLiveMonitorHistory() default: if page, ok := strings.CutPrefix(m.monitorContextHover, "attention-next:"); ok { m.monitorAttentionPage, _ = strconv.Atoi(page) @@ -518,6 +548,7 @@ func (m Model) updateMonitorContextMouse(msg tea.MouseMsg) (Model, tea.Cmd, bool m.openMonitorAttention(m.monitorContextHover) } else if id, ok := strings.CutPrefix(m.monitorContextHover, "badge:"); ok { m.setRowContext(id, contextFull) + m.showLiveMonitorHistory() row := m.monitorContextRows[id] row.review = "context" m.monitorContextRows[id] = row diff --git a/internal/ui/monitor_context_modes.go b/internal/ui/monitor_context_modes.go index f3b37ec..fe0bf00 100644 --- a/internal/ui/monitor_context_modes.go +++ b/internal/ui/monitor_context_modes.go @@ -209,11 +209,7 @@ func (m Model) renderExpandedContext(width, height int, s monitorSession, colors if s.id == m.monitorContextTarget() { if queueRows := m.monitorQueueRows(width, height, n); queueRows > 0 { queue := m.renderMonitorQueue(width, queueRows, colors) - if controls != "" { - controls = queue + "\n" + controls - } else { - controls = queue - } + controls = stackMonitorQueue(queue, controls, m.monitorQueueActivityFirst(width, height, n)) n += queueRows } } @@ -324,7 +320,7 @@ func (m Model) expandedContextAt(x, y int) string { if s.id == m.monitorContextTarget() && m.monitorPromptOffer().Token != "" { if rows := m.monitorPromptRows(cw, heights[i]); rows > 0 { _, _, cy := monitorContextBodyLayout(heights[i], rows) - if y >= cy+1 && y < cy+rows-1 && x >= 2 && x < cw-2 { + if y >= cy+m.monitorPromptHeaderRows() && y < cy+rows-1 && x >= 2 && x < cw-2 { return "prompt" } } diff --git a/internal/ui/monitor_copy.go b/internal/ui/monitor_copy.go index f151a4d..6dabdc0 100644 --- a/internal/ui/monitor_copy.go +++ b/internal/ui/monitor_copy.go @@ -9,6 +9,11 @@ import ( // Copy the complete observed reply, not its wrapped/truncated presentation. func (m Model) monitorCopyText(id string) string { + if id == m.monitorContextDetail { + if c, ok := m.historicalContext(); ok { + return codex.SanitizeSessionContext(c.Text) + } + } for _, s := range m.monitorSessionData { if s.id == id && m.monitorSessionVisible(s) && !m.hasSessionProfile(s) && (s.preview.Kind == codex.SessionContextReply || s.preview.Kind == codex.SessionContextActivity) { diff --git a/internal/ui/monitor_detail.go b/internal/ui/monitor_detail.go index a48264a..7164a58 100644 --- a/internal/ui/monitor_detail.go +++ b/internal/ui/monitor_detail.go @@ -46,6 +46,9 @@ func (m Model) contextDetailDocument(width int) (document []detailLine) { if !ok { return []detailLine{{i18n.Text("NO CONTEXT"), "metadata"}} } + if c, historical := m.historicalContext(); historical { + return m.historyDocument(width, s, c) + } if doc := m.profileDocument(s, width); doc != nil { return doc } diff --git a/internal/ui/monitor_detail_activity.go b/internal/ui/monitor_detail_activity.go index d59beea..a6736d6 100644 --- a/internal/ui/monitor_detail_activity.go +++ b/internal/ui/monitor_detail_activity.go @@ -138,10 +138,7 @@ func (layout detailControlLayout) render(m Model, width, height int, colors pale body := layout.renderBase(m, width, height, colors) if layout.queueRows > 0 { queue := m.renderMonitorQueue(width, layout.queueRows, colors) - if body != "" { - return queue + "\n" + body - } - return queue + return stackMonitorQueue(queue, body, m.monitorQueueActivityFirst(width, height, layout.rows-layout.queueRows)) } return body } diff --git a/internal/ui/monitor_history.go b/internal/ui/monitor_history.go new file mode 100644 index 0000000..dc1f49a --- /dev/null +++ b/internal/ui/monitor_history.go @@ -0,0 +1,334 @@ +package ui + +import ( + "context" + "maps" + "slices" + "strings" + "time" + + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" + "github.com/charmbracelet/x/ansi" + "github.com/merefield/codexometer/internal/codex" + "github.com/merefield/codexometer/internal/i18n" +) + +type monitorTurnHistory struct { + turns []codex.SessionContext + // Pin an immutable excerpt rather than an index: incoming turns and cache + // eviction must never change the answer somebody is currently reading. + selected *codex.SessionContext + requestedAt time.Time + // Successful server reads own the order of replies observed before the + // request. Lagging telemetry may update those entries, but not reappend + // an older reply that the bounded server page has already evicted. + observedThrough time.Time + busy bool + request uint64 +} + +type monitorHistoryResult struct { + id string + request uint64 + turns []codex.SessionContext + err error +} + +func sameHistoryTurn(a, b codex.SessionContext) bool { + if a.ThreadID != b.ThreadID { + return false + } + if a.TurnID != "" && b.TurnID != "" { + return a.TurnID == b.TurnID + } + return a.ItemID == b.ItemID && a.At.Equal(b.At) && a.Text == b.Text +} + +func (m *Model) saveMonitorHistory(id string, h monitorTurnHistory) { + m.monitorHistory = maps.Clone(m.monitorHistory) + if m.monitorHistory == nil { + m.monitorHistory = map[string]monitorTurnHistory{} + } + m.monitorHistory[id] = h +} + +func addHistoryTurn(h monitorTurnHistory, c codex.SessionContext) monitorTurnHistory { + if c.Kind != codex.SessionContextReply || c.Streaming || strings.TrimSpace(c.Text) == "" { + return h + } + c = codex.HistoryExcerpt(c) + for i, old := range h.turns { + if sameHistoryTurn(old, c) { + if old == c { + return h + } + h.turns = slices.Clone(h.turns) + h.turns[i] = c + return h + } + } + h.turns = slices.Clone(h.turns) + h.turns = append(h.turns, c) + if len(h.turns) > codex.SessionHistoryLimit { + h.turns = slices.Clone(h.turns[len(h.turns)-codex.SessionHistoryLimit:]) + } + return h +} + +func (m *Model) observeMonitorHistory() { + cloned := false + for _, s := range m.monitorSessionData { + if s.preview.Kind == codex.SessionContextReply && !s.preview.Streaming && s.preview.Text != "" { + h := m.monitorHistory[s.id] + if !h.observedThrough.IsZero() && !s.preview.At.After(h.observedThrough) && + !slices.ContainsFunc(h.turns, func(c codex.SessionContext) bool { return sameHistoryTurn(c, s.preview) }) { + continue + } + next := addHistoryTurn(h, s.preview) + if slices.Equal(h.turns, next.turns) { + continue + } + if !cloned { + m.monitorHistory = maps.Clone(m.monitorHistory) + if m.monitorHistory == nil { + m.monitorHistory = map[string]monitorTurnHistory{} + } + cloned = true + } + m.monitorHistory[s.id] = next + } + } +} + +func (m Model) historicalContext() (codex.SessionContext, bool) { + if m.monitorContextDetail == "" || m.contextTargetHidden() { + return codex.SessionContext{}, false + } + h := m.monitorHistory[m.monitorContextDetail] + if h.selected == nil { + return codex.SessionContext{}, false + } + return *h.selected, true +} + +func (m *Model) moveMonitorHistory(delta int) { + id := m.monitorContextDetail + if id == "" { + return + } + m.observeMonitorHistory() + h := m.monitorHistory[id] + index := len(h.turns) + if h.selected != nil { + index = -1 // pinned entry may already have fallen outside the cache + for i, c := range h.turns { + if sameHistoryTurn(c, *h.selected) { + index = i + break + } + } + } else if s, ok := m.contextDetailSession(); ok && len(h.turns) > 0 && sameHistoryTurn(h.turns[len(h.turns)-1], s.preview) { + // Live already displays this completed answer; Previous means the + // answer before it, not a duplicate of the current screen. + index-- + } + index += delta + if index < 0 { + return + } + if index >= len(h.turns) { + h.selected = nil + } else { + c := h.turns[index] + h.selected = &c + } + m.saveMonitorHistory(id, h) + m.monitorContextScroll = 0 + m.monitorPrompt.input.Blur() + m.monitorApprovalConfirm = "" + m.monitorApprovalNumberReleased = false +} + +func (m *Model) pollMonitorHistory(now time.Time) tea.Cmd { + id := m.monitorContextDetail + if m.meterView != viewMonitor || id == "" || m.contextTargetHidden() || m.scheduleUI.open || m.monitorCommands.open || m.monitorQueue.open { + return nil + } + c, ok := m.fetcher.(codex.SessionHistoryClient) + if !ok { + return nil + } + h := m.monitorHistory[id] + if h.busy || !h.requestedAt.IsZero() && now.Sub(h.requestedAt) < 5*time.Second { + return nil + } + h.busy = true + h.requestedAt = now + m.monitorHistorySequence++ + h.request = m.monitorHistorySequence + m.saveMonitorHistory(id, h) + request := h.request + return func() tea.Msg { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + turns, err := c.SessionHistory(ctx, id) + return monitorHistoryResult{id, request, turns, err} + } +} + +func (m Model) updateMonitorHistory(msg tea.Msg) (Model, tea.Cmd, bool) { + if result, ok := msg.(monitorHistoryResult); ok { + h, exists := m.monitorHistory[result.id] + if !exists || h.request != result.request { + return m, nil, true + } + h.busy = false + if result.err == nil && len(result.turns) > 0 { + h.observedThrough = h.requestedAt + // Server order is authoritative. Merge only observed turns not in + // that page (e.g. a completion newer than the asynchronous read). + old := h.turns + h.turns = nil + for _, c := range result.turns { + for _, previous := range old { + if sameHistoryTurn(c, previous) && c.LatestGuidance == "" { + c.LatestGuidance = previous.LatestGuidance + break + } + } + h = addHistoryTurn(h, c) + } + for _, c := range old { + found := false + for _, v := range h.turns { + if sameHistoryTurn(c, v) { + found = true + break + } + } + if !found && c.At.After(h.requestedAt) { + h = addHistoryTurn(h, c) + } + } + } + m.saveMonitorHistory(result.id, h) + return m, nil, true + } + if key, ok := msg.(tea.KeyPressMsg); ok && (key.String() == "alt+left" || key.String() == "alt+right") && + m.meterView == viewMonitor && m.monitorContextDetail != "" && !m.contextTargetHidden() && !m.monitorCommands.open && !m.scheduleUI.open && !m.monitorQueue.open { + delta := -1 + if key.String() == "alt+right" { + delta = 1 + } + m.moveMonitorHistory(delta) + return m, nil, true + } + return m, nil, false +} + +func (m Model) historyNavigationButtons(width, height int) []monitorNavigationButton { + textRows, _, _ := monitorContextBodyLayout(height, m.layoutDetailControls(width, height).rows) + if textRows < 2 { + return nil + } + h := m.monitorHistory[m.monitorContextDetail] + if len(h.turns) == 0 && h.selected == nil { + return nil + } + older := len(h.turns) > 0 + if h.selected != nil { + older = false + for i, c := range h.turns { + if sameHistoryTurn(c, *h.selected) { + older = i > 0 + break + } + } + } else if s, ok := m.contextDetailSession(); ok && len(h.turns) == 1 && sameHistoryTurn(h.turns[0], s.preview) { + older = false + } + if !older && h.selected == nil { + return nil + } + buttons := []monitorNavigationButton{ + {label: i18n.Text("[ Alt+←: PREVIOUS ]"), action: "history:previous", enabled: older}, + {label: i18n.Text("[ Alt+→: NEXT ]"), action: "history:next", enabled: h.selected != nil}, + {label: i18n.Text("[ LIVE ]"), action: "history:live", enabled: h.selected != nil}, + } + x := 2 + for i := range buttons { + buttons[i].rect = monitorRect{x: x, y: 1, width: lipgloss.Width(buttons[i].label), height: 1} + x += buttons[i].rect.width + 1 + } + if x > width-2 { + for i, label := range []string{"[Alt+←]", "[Alt+→]", i18n.Text("[ LIVE ]")} { + buttons[i].label = label + } + x = 2 + for i := range buttons { + buttons[i].rect = monitorRect{x: x, y: 1, width: lipgloss.Width(buttons[i].label), height: 1} + x += buttons[i].rect.width + 1 + } + } + if x > width-2 { + return nil + } + return buttons +} + +func (m Model) historyNavigationRows(width, height int) int { + if len(m.historyNavigationButtons(width, height)) > 0 { + return 1 + } + return 0 +} + +func (m Model) detailBodyLayout(width, height, controls int) (textRows, gap, controlY int) { + textRows, gap, controlY = monitorContextBodyLayout(height, controls) + textRows = max(textRows-m.historyNavigationRows(width, height), 0) + return +} + +func (m *Model) showLiveMonitorHistory() { + id := m.monitorContextDetail + h := m.monitorHistory[id] + if h.selected != nil { + h.selected = nil + m.saveMonitorHistory(id, h) + m.monitorContextScroll = 0 + } +} + +func (m Model) historyDocument(width int, s monitorSession, c codex.SessionContext) []detailLine { + h := m.monitorHistory[s.id] + heading := i18n.Text("PREVIOUS TURN") + for i, v := range h.turns { + if sameHistoryTurn(c, v) { + heading += i18n.Format(" // %d OF %d", i+1, len(h.turns)) + break + } + } + lines := []detailLine{{ansi.Truncate(heading, width, "…"), "heading"}} + appendSection := func(title, text string) { + if text == "" { + return + } + lines = append(lines, detailLine{}, detailLine{ansi.Truncate(title, width, "…"), "heading"}) + for _, line := range strings.Split(ansi.Hardwrap(codex.SanitizeSessionContext(text), width, true), "\n") { + lines = append(lines, detailLine{ansi.Truncate(line, width, ""), "body"}) + } + } + appendSection(i18n.Text("TASK"), c.CurrentTask) + appendSection(i18n.Text("LATEST GUIDANCE"), c.LatestGuidance) + appendSection(i18n.Text("LAST REPLY"), c.Text) + // Current requests are still live; never render controls beside a saved + // approval or present historical text as the justification for a new one. + if s.preview.Kind == codex.SessionContextApproval || s.preview.Kind == codex.SessionContextQuestion || m.hasSessionProfile(s) { + lines = append(lines, detailLine{}, detailLine{ansi.Truncate(i18n.Text("LIVE SESSION"), width, "…"), "warning"}) + live := m + live.monitorHistory = nil + lines = append(lines, live.contextDetailDocument(width)...) + } + return lines +} diff --git a/internal/ui/monitor_history_test.go b/internal/ui/monitor_history_test.go new file mode 100644 index 0000000..605083a --- /dev/null +++ b/internal/ui/monitor_history_test.go @@ -0,0 +1,277 @@ +package ui + +import ( + "context" + "fmt" + "strings" + "testing" + "time" + + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" + "github.com/charmbracelet/x/ansi" + "github.com/merefield/codexometer/internal/codex" + "github.com/merefield/codexometer/internal/i18n" +) + +func historyContext(turn int) codex.SessionContext { + return codex.SessionContext{ThreadID: "root-one", TurnID: fmt.Sprint(turn), Kind: codex.SessionContextReply, CurrentTask: fmt.Sprintf("Task %d", turn), Text: fmt.Sprintf("Answer %d\n", turn) + strings.Repeat("More detail\n", 40), At: time.Unix(int64(turn), 0)} +} + +func historyTestModel() Model { + m, _ := promptTestModel() + for i := 1; i <= 3; i++ { + m.monitorSessionData[0].preview = historyContext(i) + m.observeMonitorHistory() + } + return m +} + +func TestMonitorHistoryNavigationPinningAndCopy(t *testing.T) { + m := historyTestModel() + next, _ := m.Update(tea.KeyPressMsg{Code: tea.KeyLeft, Mod: tea.ModAlt}) + m = next.(Model) + c, ok := m.historicalContext() + if !ok || c.TurnID != "2" || !strings.Contains(strings.Join(m.contextDetailLines(100), "\n"), "Task 2") || m.monitorCopyText("root-one") != c.Text { + t.Fatal("Previous did not select/copy the previous completed turn") + } + m.scrollMonitorContext(4) + scroll := m.monitorContextScroll + for i := 4; i <= 18; i++ { + u := codex.LiveUsageSession{ID: "root-one", Active: true, Context: historyContext(i)} + m.syncMonitorSessions(codex.LiveUsageSnapshot{Sessions: []codex.LiveUsageSession{u}}, time.Now()) + } + if pinned, _ := m.historicalContext(); pinned != c || m.monitorContextScroll != scroll || len(m.monitorHistory["root-one"].turns) != codex.SessionHistoryLimit { + t.Fatal("incoming turns replaced pinned answer, scroll, or exceeded cache cap") + } + if strings.Contains(strings.Join(m.contextDetailLines(100), "\n"), "Answer 18") { + t.Fatal("live reply leaked into pinned document") + } + m.moveMonitorHistory(1) // evicted pinned answer advances to oldest retained turn + if c, _ := m.historicalContext(); c.TurnID != "9" { + t.Fatal("evicted selection cannot advance") + } + m.showLiveMonitorHistory() + if _, ok := m.historicalContext(); ok || !strings.Contains(m.monitorCopyText("root-one"), "Answer 18") { + t.Fatal("Live did not restore current reply") + } + // Cache and selected excerpt belong to the session, not the detail viewport. + m.moveMonitorHistory(-1) + m.setRowContext("root-two", contextFull) + if _, ok := m.historicalContext(); ok { + t.Fatal("selection leaked to another session") + } + m.setRowContext("root-one", contextFull) + if c, ok := m.historicalContext(); !ok || c.TurnID != "17" { + t.Fatal("selection lost across navigation") + } +} + +func TestMonitorHistoryRejectsStreamingAndRemovesCapabilities(t *testing.T) { + m := historyTestModel() + c := historyContext(4) + c.Streaming = true + m.monitorSessionData[0].preview = c + m.observeMonitorHistory() + if len(m.monitorHistory["root-one"].turns) != 3 { + t.Fatal("saved incomplete stream") + } + c.Streaming = false + c.ApprovalToken = "never-retain" + c.InputToken = "never-retain" + c.Text = "\x1b[31manswer\x00" + m.monitorSessionData[0].preview = c + m.observeMonitorHistory() + h := m.monitorHistory["root-one"] + got := h.turns[len(h.turns)-1] + if got.Text != "answer" || got.ApprovalToken != "" || got.InputToken != "" { + t.Fatal("history retained controls or unsafe text") + } + m.observeMonitorHistory() + if len(m.monitorHistory["root-one"].turns) != 4 { + t.Fatal("duplicate turn retained") + } +} + +func TestMonitorHistoryRenderedControlsAndLiveApproval(t *testing.T) { + for _, width := range []int{28, 40, 80, 160} { + m := historyTestModel() + m.width = width + m.height = 45 + m.moveMonitorHistory(-1) + g := m.monitorDashboardLayout() + out := m.renderMonitorContextDetail(g.contentWidth, g.meterHeight, paletteFor(m.theme)) + if lipgloss.Width(out) > g.contentWidth || lipgloss.Height(out) > g.meterHeight { + t.Fatal("history overflows detail") + } + lines := strings.Split(ansi.Strip(out), "\n") + for _, b := range m.historyNavigationButtons(g.contentWidth, g.meterHeight) { + if got := ansi.Cut(lines[b.rect.y], b.rect.x, b.rect.x+b.rect.width); got != b.label { + t.Fatalf("render/hit mismatch: %q vs %q", got, b.label) + } + for x := b.rect.x; x < b.rect.x+b.rect.width; x++ { + want := b.action + if !b.enabled { + want = "navigation-disabled" + } + if got := m.monitorContextAt(x+2, g.meterY+b.rect.y); got != want { + t.Fatalf("history click %d,%d: %q != %q", x, b.rect.y, got, want) + } + } + if b.enabled { + m.monitorContextHover = b.action + hover := m.renderMonitorContextDetail(g.contentWidth, g.meterHeight, paletteFor(m.theme)) + if hover == out || ansi.Strip(hover) != ansi.Strip(out) { + t.Fatal("history hover altered layout or was missing") + } + m.monitorContextHover = "" + } + } + } + m := historyTestModel() + m.moveMonitorHistory(-1) + live := approvalTestModel() + approval := live.monitorSessionData[0].preview + m.fetcher = live.fetcher + m.monitorSessionData[0].preview = approval + doc := strings.Join(m.contextDetailLines(100), "\n") + if !strings.Contains(doc, i18n.Text("LIVE SESSION")) || !strings.Contains(doc, "Answer 2") || !strings.Contains(doc, approval.CommandDetails.Command) { + t.Fatal("live request missing or history replaced") + } + if m.monitorApprovalToken() != approval.ApprovalToken { + t.Fatal("historical turn replaced live capability") + } + m, cmd, _ := m.monitorApprovalAction("decision:0") + if _, historical := m.historicalContext(); historical || cmd != nil || m.monitorApprovalConfirm == "" { + t.Fatal("first approval choice must show live request and still require confirmation") + } + m.moveMonitorHistory(-1) + m.monitorSessionData[0].attention = codex.SessionAttentionApproval + m.openMonitorAttention("attention:root-one") + if _, historical := m.historicalContext(); historical { + t.Fatal("approval pill reopened historical reply instead of current request") + } +} + +func TestMonitorHistoryShortLayoutsAndDrafts(t *testing.T) { + for _, size := range [][2]int{{28, 12}, {40, 16}, {80, 20}, {160, 40}} { + m := historyTestModel() + m.width, m.height = size[0], size[1] + m.monitorPrompt.input = newMonitorEditor() + m.focusMonitorPrompt() + m.monitorPrompt.input.SetValue("Keep this unsent draft") + next, _ := m.Update(tea.KeyPressMsg{Code: tea.KeyLeft, Mod: tea.ModAlt}) + m = next.(Model) + if m.monitorPrompt.input.Value() != "Keep this unsent draft" || m.monitorPrompt.input.Focused() { + t.Fatal("history navigation changed draft or retained focus") + } + g := m.monitorDashboardLayout() + out := m.renderMonitorContextDetail(g.contentWidth, g.meterHeight, paletteFor(m.theme)) + if lipgloss.Height(out) > g.meterHeight || lipgloss.Width(out) > g.contentWidth { + t.Fatalf("history overflow at %v", size) + } + next, _ = m.Update(tea.KeyPressMsg{Code: tea.KeyPgDown}) + m = next.(Model) + if m.monitorContextScroll == 0 { + t.Fatal("Page Down no longer scrolls historical reply") + } + m.focusMonitorPrompt() + if _, historical := m.historicalContext(); historical || m.monitorPrompt.input.Value() != "Keep this unsent draft" { + t.Fatal("return to live discarded draft") + } + } +} + +type historyTestClient struct { + *promptTestClient + turns []codex.SessionContext + err error +} + +func TestMonitorHistoryRefreshRejectsStalePreview(t *testing.T) { + m := historyTestModel() + h := m.monitorHistory["root-one"] + h.request = 1 + h.requestedAt = time.Now() + m.monitorHistory["root-one"] = h + var turns []codex.SessionContext + for i := 4; i <= 13; i++ { + turns = append(turns, historyContext(i)) + } + m, _, _ = m.updateMonitorHistory(monitorHistoryResult{id: "root-one", request: 1, turns: turns}) + // Telemetry still displays turn 3, which is outside the server's page. + // Observing it repeatedly (including during navigation) must not evict + // turn 4 or append turn 3 as the newest answer. + m.observeMonitorHistory() + m.moveMonitorHistory(-1) + got := m.monitorHistory["root-one"].turns + if len(got) != len(turns) || got[0].TurnID != "4" || got[len(got)-1].TurnID != "13" { + t.Fatalf("stale preview reordered history: first=%s last=%s", got[0].TurnID, got[len(got)-1].TurnID) + } + if c, ok := m.historicalContext(); !ok || c.TurnID != "13" { + t.Fatal("Previous did not select the newest recovered answer") + } + // Existing entries can still gain observed guidance without reordering. + c := historyContext(7) + c.LatestGuidance = "Preserve this guidance" + m.monitorSessionData[0].preview = c + m.observeMonitorHistory() + if got := m.monitorHistory["root-one"].turns[3]; got.TurnID != "7" || got.LatestGuidance != c.LatestGuidance { + t.Fatal("stale-preview guard prevented an existing entry update") + } + // New completions remain observable even if later history reads fail. + c = historyContext(14) + c.At = h.requestedAt.Add(time.Second) + m.monitorSessionData[0].preview = c + m.observeMonitorHistory() + got = m.monitorHistory["root-one"].turns + if len(got) != codex.SessionHistoryLimit || got[0].TurnID != "5" || got[len(got)-1].TurnID != "14" { + t.Fatal("new completion was lost after a successful server refresh") + } + if c, ok := m.historicalContext(); !ok || c.TurnID != "13" { + t.Fatal("new completion replaced the pinned answer") + } +} + +func (c *historyTestClient) SessionHistory(context.Context, string) ([]codex.SessionContext, error) { + return c.turns, c.err +} + +func TestMonitorHistoryAsyncRecoveryAndFallback(t *testing.T) { + m := historyTestModel() + _, promptClient := promptTestModel() + client := &historyTestClient{promptTestClient: promptClient, turns: []codex.SessionContext{historyContext(1), historyContext(2), historyContext(3), historyContext(4)}} + m.fetcher = client + now := time.Now() + cmd := m.pollMonitorHistory(now) + if cmd == nil || m.pollMonitorHistory(now) != nil { + t.Fatal("missing fetch or overlapping fetch") + } + m.moveMonitorHistory(-1) + next, _, handled := m.updateMonitorHistory(cmd()) + if !handled || len(next.monitorHistory["root-one"].turns) != 4 { + t.Fatal("server did not recover missed completion") + } + m = next + if c, _ := m.historicalContext(); c.TurnID != "2" { + t.Fatal("async completion stole pinned view") + } + if m.pollMonitorHistory(now.Add(time.Second)) != nil { + t.Fatal("history polled on every paint") + } + client.err = codex.ErrSessionHistory + cmd = m.pollMonitorHistory(now.Add(6 * time.Second)) + m, _, _ = m.updateMonitorHistory(cmd()) + if len(m.monitorHistory["root-one"].turns) != 4 { + t.Fatal("unsupported server discarded observed history") + } + stale := monitorHistoryResult{id: "root-one", request: 1, turns: []codex.SessionContext{historyContext(99)}} + m, _, _ = m.updateMonitorHistory(stale) + if len(m.monitorHistory["root-one"].turns) != 4 { + t.Fatal("stale result replaced history") + } + m.focusMonitorPrompt() + if _, ok := m.historicalContext(); ok { + t.Fatal("composer focus did not return to live session") + } +} diff --git a/internal/ui/monitor_inline_prompt_test.go b/internal/ui/monitor_inline_prompt_test.go index 027897d..a9dd515 100644 --- a/internal/ui/monitor_inline_prompt_test.go +++ b/internal/ui/monitor_inline_prompt_test.go @@ -25,14 +25,15 @@ func TestInlineMonitorPromptClicksAndSend(t *testing.T) { m.width = width w, h := m.monitorPromptSize() rows := m.monitorPromptRows(w, h) - if rows != 3 || !strings.Contains(ansi.Strip(m.render()), i18n.Text("FOLLOW-UP")) { + placeholder := ansi.Truncate(i18n.Text("[ Click here or press Enter to write ]"), max(w-4, 1), "") + if rows != 2 || !strings.Contains(ansi.Strip(m.render()), placeholder) { t.Fatalf("missing inline composer at width %d", width) } g := m.dashboardLayout() a := m.monitorArea(g.contentWidth, g.meterHeight) mw, _, _ := monitorSessionColumnWidths(a.width) _, _, cy := monitorContextBodyLayout(h, rows) - y := g.meterY + a.topHeight + a.gap - 1 + cy + 1 + y := g.meterY + a.topHeight + a.gap - 1 + cy for x := mw + 5; x < mw+3+w-2; x++ { if got := m.monitorContextAt(x, y); got != "prompt" { t.Fatalf("width %d click %d,%d = %q", width, x, y, got) @@ -122,7 +123,7 @@ func TestInlineMonitorPromptSingleOwner(t *testing.T) { } id := m.monitorSessionData[1].id c.offer.ThreadID = id - if got := strings.Count(ansi.Strip(m.render()), i18n.Text("FOLLOW-UP")); got != 1 { + if got := strings.Count(ansi.Strip(m.render()), i18n.Text("[ Click here or press Enter to write ]")); got != 1 { t.Fatalf("rendered %d composers, want only selected row", got) } m, _ = promptKey(m, tea.KeyEnter, "") diff --git a/internal/ui/monitor_name_test.go b/internal/ui/monitor_name_test.go index 03273f3..13a83f2 100644 --- a/internal/ui/monitor_name_test.go +++ b/internal/ui/monitor_name_test.go @@ -31,7 +31,7 @@ func TestMonitorSessionNameDisplayAndRename(t *testing.T) { } } -func TestFullDetailAndComposerIdentifySession(t *testing.T) { +func TestFullDetailIdentifiesSessionWithoutRepeatingComposerHeading(t *testing.T) { for _, working := range []bool{false, true} { m, _ := promptTestModel() if working { @@ -46,10 +46,13 @@ func TestFullDetailAndComposerIdentifySession(t *testing.T) { identity = shortSessionID(m.monitorSessionData[0].id) + " // dashboard" } frame := strings.Split(ansi.Strip(m.renderMonitorContextDetail(160, 40, paletteFor(m.theme))), "\n")[0] - composer := strings.Split(ansi.Strip(m.renderMonitorPrompt(160, 40, paletteFor(m.theme))), "\n")[0] - if !strings.Contains(frame, identity) || !strings.Contains(composer, identity) { + composer := ansi.Strip(m.renderMonitorPrompt(160, 40, paletteFor(m.theme))) + if !strings.Contains(frame, identity) { t.Fatalf("missing session identity: frame=%q composer=%q", frame, composer) } + if strings.Contains(composer, identity) || strings.Contains(composer, "FOLLOW-UP") { + t.Fatalf("redundant composer heading: %q", composer) + } } } } diff --git a/internal/ui/monitor_profile.go b/internal/ui/monitor_profile.go index 9ee50b5..fccc8a9 100644 --- a/internal/ui/monitor_profile.go +++ b/internal/ui/monitor_profile.go @@ -241,6 +241,7 @@ func (m Model) updateProfileKey(key string) (Model, tea.Cmd, bool) { } func (m Model) profileAction(action string, confirm bool) (Model, tea.Cmd, bool) { + m.showLiveMonitorHistory() id, skip := strings.CutPrefix(action, "profile-skip:") if !skip { id = strings.TrimPrefix(action, "profile-apply:") diff --git a/internal/ui/monitor_prompt.go b/internal/ui/monitor_prompt.go index e163b9e..a89d9b4 100644 --- a/internal/ui/monitor_prompt.go +++ b/internal/ui/monitor_prompt.go @@ -71,6 +71,7 @@ type monitorTurnResult struct { session, token, action string text string err error + steer monitorSteerReceipt } func (m Model) monitorPromptOffer() codex.SessionPromptOffer { @@ -134,10 +135,11 @@ func (m Model) monitorPromptRows(width, height int) int { } if m.monitorPromptOffer().Token != "" || m.monitorPrompt.session == m.monitorContextTarget() && (m.monitorPrompt.busy || m.monitorPrompt.notice != "" && !sentNotice(m.monitorPrompt.notice)) { p := m.monitorPrompt - rows := 3 + headerRows := m.monitorPromptHeaderRows() + rows := 2 + headerRows if (p.input.Focused() || p.input.Value() != "") && !p.busy { p.input.configure(width, m.monitorPromptEditorHeight(width, height)) - rows = p.input.Height() + 2 + rows = p.input.Height() + 1 + headerRows } if m.monitorContextDetail == "" { s, ok := m.contextDetailSession() @@ -151,15 +153,35 @@ func (m Model) monitorPromptRows(width, height int) int { return 0 } +func (m Model) monitorPromptHeaderRows() int { + if len(m.monitorPromptOffer().Questions) > 0 { + return 1 + } + if m.monitorComposerActivity() != "" { + return 1 + } + return 0 +} + +func (m Model) monitorComposerActivity() string { + if m.monitorPromptOffer().TurnID != "" { + if s, ok := m.contextDetailSession(); ok { + return m.sessionActivityDots(s) + } + } + return "" +} + // Inline drafts grow only into spare space, then scroll inside the editor; // they never displace the source context. Full detail keeps its usual viewport. func (m Model) monitorPromptEditorHeight(width, height int) int { + extra := 1 - m.monitorPromptHeaderRows() if m.monitorContextDetail == "" { if s, ok := m.contextDetailSession(); ok { - return min(height, max(8, height+3-len(expandedContextLines(width, s)))) + return min(height+extra, max(8, height+3+extra-len(expandedContextLines(width, s)))) } } - return height + return height + extra } func (m Model) monitorPromptSize() (int, int) { @@ -179,6 +201,7 @@ func (m Model) monitorPromptSize() (int, int) { } func (m *Model) focusMonitorPrompt() tea.Cmd { + m.showLiveMonitorHistory() m.restoreMonitorDraft() o := m.monitorPromptOffer() w, h := m.monitorPromptSize() @@ -200,19 +223,19 @@ func (m *Model) focusMonitorPrompt() tea.Cmd { m.monitorPrompt.input.setSecret(len(o.Questions) > 0 && o.Questions[m.monitorPrompt.question].Secret) m.monitorPrompt.input.configure(w, m.monitorPromptEditorHeight(w, h)) m.monitorPrompt.notice = "" - return m.monitorPrompt.input.Focus() + return tea.Batch(m.monitorPrompt.input.Focus(), m.syncMonitorSuggestions()) } func (m Model) renderMonitorPrompt(width, height int, colors palette) string { p := m.monitorPrompt o := m.monitorPromptOffer() - identity := shortSessionID(o.ThreadID) - if s, ok := m.contextDetailSession(); ok { - identity = monitorSessionIdentity(s) - } - header := i18n.Text("FOLLOW-UP") + " // " + identity + header := m.monitorComposerActivity() scheduling := m.monitorContextDetail != "" && o.Token != "" && len(o.Questions) == 0 && o.TurnID == "" if len(o.Questions) > 0 { + identity := shortSessionID(o.ThreadID) + if s, ok := m.contextDetailSession(); ok { + identity = monitorSessionIdentity(s) + } n := 0 if p.offer.Token == o.Token { n = p.question @@ -223,14 +246,8 @@ func (m Model) renderMonitorPrompt(width, height int, colors palette) string { line := colors.dimmed().Background(monitorComposerBackground(colors)). Width(max(width-4, 1)).Render(ansi.Truncate(i18n.Text("[ Click here or press Enter to write ]"), max(width-4, 1), "")) hint := i18n.Text("Enter: send / next answer • Esc: leave editor • ↑/↓: choices") + hintStyle := colors.dimmed() if o.TurnID != "" { - header = i18n.Text("WORKING") - if s, ok := m.contextDetailSession(); ok { - if dots := m.sessionActivityDots(s); dots != "" { - header += " " + dots - } - } - header += " // " + i18n.Text("FOLLOW-UP") + " // " + identity hint = "Enter: steer • Tab: queue next turn • Esc: interrupt" } if p.offer.Token == o.Token && (p.input.Focused() || p.input.Value() != "") && !p.busy { @@ -244,24 +261,37 @@ func (m Model) renderMonitorPrompt(width, height int, colors palette) string { if p.notice != "" { if !sentNotice(p.notice) { hint = p.notice + hintStyle = colors.label() } else if time.Now().Before(p.noticeUntil) || m.monitorDetailSent.session == m.monitorContextTarget() && time.Now().Before(m.monitorDetailSent.visibleUntil) { hint = p.notice + hintStyle = colors.label() } } inputLines := strings.Split(line, "\n") for i := range inputLines { inputLines[i] = ansi.Truncate(inputLines[i], max(width-4, 1), "") } - heading := colors.label().Render(ansi.Truncate(header, max(width-4, 1), "…")) + if popup, _ := m.monitorSuggestionPopup(); popup.height > 0 { + hint = "↑/↓: select • Tab: complete • Enter: options • Esc: dismiss" + hintStyle = colors.dimmed() + } + footer := hintStyle.Render(ansi.Truncate(hint, max(width-4, 1), "…")) if scheduling { - heading = m.renderScheduleToggleHeader(width, identity, false, colors) + footer = m.renderScheduleToggleHint(width, hint, false, hintStyle, colors) + } + body := strings.Join(inputLines, "\n") + "\n" + footer + if header != "" { + body = colors.label().Render(ansi.Truncate(header, max(width-4, 1), "…")) + "\n" + body } - return heading + "\n" + strings.Join(inputLines, "\n") + "\n" + colors.label().Render(ansi.Truncate(hint, max(width-4, 1), "…")) + return body } func (m Model) updateMonitorPrompt(msg tea.Msg) (Model, tea.Cmd, bool) { p := &m.monitorPrompt if result, ok := msg.(monitorTurnResult); ok { + if result.err == nil && result.action == "steer" { + m.recordMonitorSteer(result.steer) + } if p.session == result.session && p.offer.Token == result.token { p.busy = false if result.err != nil { @@ -316,6 +346,28 @@ func (m Model) updateMonitorPrompt(msg tea.Msg) (Model, tea.Cmd, bool) { if wasFocused && layout.monitorPromptRows(w, h) == 0 { p.input.Blur() } + // A newly visible approval may replace the focused composer. Let an + // explicit grant shortcut select it, but never reinterpret that first key + // as confirmation, a decline, or an action on a different session. + if wasFocused && !p.input.Focused() && !p.busy && !p.input.secret && len(p.offer.Questions) == 0 && p.session == m.monitorContextTarget() { + if key, ok := msg.(tea.KeyPressMsg); ok && !key.IsRepeat { + name := key.String() + if len(name) == 1 && name[0] >= '1' && name[0] <= '8' { + index := int(name[0] - '1') + if s, ok := m.contextDetailSession(); ok && s.preview.ApprovalOptions[index].GrantsPermission() { + for _, b := range m.visibleMonitorApprovalButtons() { + if b.action == fmt.Sprintf("decision:%d", index) { + m.stashMonitorDraft() + *p = monitorPromptState{session: p.session} + m.monitorApprovalConfirm = "" + m.monitorApprovalNumberReleased = false + return m.updateMonitorApprovalKey(name) + } + } + } + } + } + } if p.session != "" && (m.meterView != viewMonitor || m.monitorContextTarget() != p.session || m.contextTargetHidden() || m.monitorContextDetail == "" && m.monitorSelectedID != p.session) { m.stashMonitorDraft() *p = monitorPromptState{} @@ -328,7 +380,13 @@ func (m Model) updateMonitorPrompt(msg tea.Msg) (Model, tea.Cmd, bool) { if next.Token != "" { p.offer = next } else { - p.input.Blur() + // Retire the obsolete working capability, not just its focus. + // Otherwise every later approval key re-enters this transition + // and is swallowed forever after an intervening telemetry tick. + m.stashMonitorDraft() + *p = monitorPromptState{session: p.session} + m.monitorApprovalConfirm = "" + m.monitorApprovalNumberReleased = false } if _, key := msg.(tea.KeyPressMsg); key { return m, nil, true @@ -362,12 +420,18 @@ func (m Model) updateMonitorPrompt(msg tea.Msg) (Model, tea.Cmd, bool) { p.input.Blur() return m, nil, true case "enter": + if len(p.offer.Questions) == 0 && codex.IsSessionCommand(p.input.Value()) { + return m.openMonitorCommands(strings.TrimSpace(p.input.Value())) + } if p.offer.TurnID != "" { return m.submitMonitorTurn("steer") } return m.submitMonitorPrompt() case "tab": if p.offer.TurnID != "" { + if codex.IsSessionCommand(p.input.Value()) { + return m.openMonitorCommands(strings.TrimSpace(p.input.Value())) + } return m.submitMonitorTurn("queue") } case "up", "down": @@ -392,7 +456,8 @@ func (m Model) updateMonitorPrompt(msg tea.Msg) (Model, tea.Cmd, bool) { case tea.KeyPressMsg, tea.PasteMsg: var cmd tea.Cmd p.input, cmd = p.input.Update(msg) - return m, cmd, true + m.monitorSuggestions.selected = 0 + return m, tea.Batch(cmd, m.syncMonitorSuggestions()), true } before := p.input.Value() var cmd tea.Cmd @@ -413,10 +478,15 @@ func (m Model) submitMonitorTurn(action string) (Model, tea.Cmd, bool) { } p.busy, p.notice = true, "" session := p.session + steer := monitorSteerReceipt{session: session, thread: o.ThreadID, turn: o.TurnID, text: codex.SanitizeSessionContext(text), sentAt: time.Now()} + if s, ok := m.contextDetailSession(); ok { + steer.baselineID = s.preview.LatestGuidanceID + steer.baselineText = s.preview.LatestGuidance + } return m, func() tea.Msg { ctx, cancel := context.WithTimeout(context.Background(), 6*time.Second) defer cancel() - return monitorTurnResult{session: session, token: o.Token, action: action, text: text, err: c.SendSessionTurn(ctx, o, action, text)} + return monitorTurnResult{session: session, token: o.Token, action: action, text: text, steer: steer, err: c.SendSessionTurn(ctx, o, action, text)} }, true } diff --git a/internal/ui/monitor_prompt_test.go b/internal/ui/monitor_prompt_test.go index 3748b65..a0f72b0 100644 --- a/internal/ui/monitor_prompt_test.go +++ b/internal/ui/monitor_prompt_test.go @@ -34,7 +34,7 @@ func TestMonitorPromptPlaceholderComposerBackground(t *testing.T) { t.Fatal("composer has no background") } for x := 0; x < width-4; x++ { - cell := cells.CellAt(x, 1) + cell := cells.CellAt(x, 0) if cell == nil || cell.Style.Bg == nil { t.Fatalf("theme %d width %d: missing placeholder background at %d", theme, width, x) } @@ -143,6 +143,10 @@ func TestMonitorPromptQuestionsAndSecret(t *testing.T) { m.monitorSessionData[0].preview.Kind = codex.SessionContextQuestion m.monitorSessionData[0].preview.InputToken = c.offer.Token m.focusMonitorPrompt() + question := strings.Split(ansi.Strip(m.renderMonitorPrompt(120, 30, paletteFor(m.theme))), "\n") + if len(question) != 3 || !strings.Contains(question[0], "REPLY 1/2") || !strings.Contains(question[0], "Pick one") || m.monitorPromptHeaderRows() != 1 { + t.Fatal("structured question lost its heading or answer progress") + } m.monitorPrompt.input.SetValue("not offered") m, cmd, _ := m.submitMonitorPrompt() if cmd != nil || m.monitorPrompt.question != 0 { @@ -270,7 +274,7 @@ func TestMonitorPromptWrapsAndGrowsUpward(t *testing.T) { if lipgloss.Width(out) > m.width || lipgloss.Height(out) > m.height { t.Fatalf("overflow %v\n%s", size, ansi.Strip(out)) } - for row := y + 1; row < y+rows-1; row++ { + for row := y; row < y+rows-1; row++ { for x := 4; x < g.contentWidth; x++ { if got := m.monitorContextAt(x, g.meterY+row); got != "prompt" { t.Fatalf("wrapped input miss %v %d,%d: %s", size, x, row, got) @@ -284,7 +288,7 @@ func TestMonitorPromptWrapsAndGrowsUpward(t *testing.T) { t.Fatal("wrapping altered the submitted text") } m.monitorPrompt.input.SetValue("short") - if m.monitorPromptRows(g.contentWidth, g.meterHeight) != 3 { + if m.monitorPromptRows(g.contentWidth, g.meterHeight) != 2 { t.Fatal("editor failed to shrink") } } diff --git a/internal/ui/monitor_queue.go b/internal/ui/monitor_queue.go index 23e699d..0b1a2c7 100644 --- a/internal/ui/monitor_queue.go +++ b/internal/ui/monitor_queue.go @@ -17,6 +17,7 @@ import ( type followupEntry struct { label, text string receipt string + readOnly bool native *codex.SessionQueuedMessage trigger *schedule.Job editable, removable, now bool @@ -42,6 +43,11 @@ func (m Model) followupEntries() []followupEntry { entries = append(entries, followupEntry{label: i18n.Text("ACCEPTED // CHECKING QUEUE"), text: text, receipt: "receipt:" + strconv.Itoa(i)}) } } + for _, steer := range m.monitorSteers { + if steer.session == m.monitorContextTarget() { + entries = append(entries, followupEntry{label: i18n.Text("STEER SENT"), text: steer.text, receipt: "steer:" + strconv.FormatUint(steer.id, 10), readOnly: true}) + } + } if j, ok := m.pendingTrigger(); ok { label := i18n.Text("AFTER QUOTA REFRESH") if j.Trigger == "at" { @@ -57,6 +63,9 @@ func (m Model) followupEntries() []followupEntry { } func followupButtons(entry followupEntry, width int) []triggerButton { + if entry.readOnly { + return nil + } var buttons []triggerButton if entry.trigger != nil { buttons = append(buttons, triggerButton{text: i18n.Text("[NOW]"), key: "now", enabled: entry.now}) @@ -195,7 +204,9 @@ func (m Model) renderMonitorQueue(width, rows int, colors palette) string { if m.queueUnavailable() { header = i18n.Text("FOLLOW-UPS // UNAVAILABLE") } - if q.focused { + if q.focused && q.offset < len(entries) && entries[q.offset].readOnly { + header += " // ↑↓ · Esc" + } else if q.focused { header += i18n.Text(" // ↑↓ · Enter: edit · X: delete · Esc") } else { header += " // Alt+Q" @@ -211,22 +222,25 @@ func (m Model) renderMonitorQueue(width, rows int, colors palette) string { if spare := inner - ansi.StringWidth(header); spare > 1 { header += " " + strings.Repeat("─", spare-1) } - lines := []string{colors.label().Bold(true).Foreground(colors.primary).Background(tint).Width(inner).Render(header)} + lines := []string{colors.label().Bold(false).Foreground(colors.primary).Background(tint).Width(inner).Render(header)} for i := start; i < min(start+rows-1, len(entries)); i++ { entry := entries[i] buttons := followupButtons(entry, width) selected := q.focused && i == q.offset - marker := " " + marker := "" background := tint if selected { marker = "› " background = monitorTintBackground(colors, 18) } text := marker + entry.label + " // " + strings.Join(strings.Fields(entry.text), " ") - space := max(buttons[0].x-1, 1) + space := inner + if len(buttons) > 0 { + space = max(buttons[0].x-1, 1) + } text = ansi.Truncate(text, space, "…") text += strings.Repeat(" ", max(space-ansi.StringWidth(text), 0)) - prefix := colors.label().Background(background) + prefix := colors.label().Bold(false).Background(background) if selected { prefix = prefix.Foreground(colors.primary).Bold(true) } @@ -238,9 +252,9 @@ func (m Model) renderMonitorQueue(width, rows int, colors palette) string { } else if m.monitorContextHover == "queue:"+b.key+":"+strconv.Itoa(i) { style = style.Foreground(colors.background).Background(colors.primary) } - line += colors.label().Background(background).Render(" ") + style.Render(b.text) + line += colors.label().Bold(false).Background(background).Render(" ") + style.Render(b.text) } - lines = append(lines, colors.label().Background(background).Width(inner).Render(line)) + lines = append(lines, colors.label().Bold(false).Background(background).Width(inner).Render(line)) } return strings.Join(lines, "\n") } @@ -251,6 +265,9 @@ func (m Model) monitorQueueAction(width, height, controls, x, y int) string { return "" } _, _, top := monitorContextBodyLayout(height, controls+rows) + if m.monitorQueueActivityFirst(width, height, controls) { + top++ // the task's existing activity row now precedes the queue + } if x < 2 || x >= width-2 || y < top || y >= top+rows { return "" } @@ -271,6 +288,43 @@ func (m Model) monitorQueueAction(width, height, controls, x, y int) string { return "queue:focus" } +// Move the existing activity row, rather than adding a second animation or +// moving the composer. Rendering and queue hit testing share this decision. +func (m Model) monitorQueueActivityFirst(width, height, controls int) bool { + s, ok := m.contextDetailSession() + if !ok || m.sessionActivityDots(s) == "" || controls == 0 { + return false + } + if m.monitorContextDetail != "" { + base := m.layoutDetailControlsBase(width, height) + if base.kind == "notice" { + return base.notice == m.detailActivity() + } + return base.kind == "prompt" && m.monitorComposerActivity() != "" && len(m.monitorPromptOffer().Questions) == 0 + } + if len(m.expandedApprovalButtons(width, height, s)) > 0 || m.monitorApprovalHasOutcome() { + return false + } + if m.monitorPromptRows(width, height) > 0 { + return m.monitorComposerActivity() != "" && len(m.monitorPromptOffer().Questions) == 0 + } + return height >= 5 && width >= 7 +} + +func stackMonitorQueue(queue, controls string, activityFirst bool) string { + if controls == "" { + return queue + } + if activityFirst { + activity, rest, more := strings.Cut(controls, "\n") + if more { + return activity + "\n" + queue + "\n" + rest + } + return activity + "\n" + queue + } + return queue + "\n" + controls +} + func (m Model) monitorQueueAt(x, y int) string { if m.meterView != viewMonitor || m.contextTargetHidden() || len(m.followupEntries()) == 0 && !m.queueUnavailable() { return "" diff --git a/internal/ui/monitor_queue_style_test.go b/internal/ui/monitor_queue_style_test.go index 5dfb462..c0c38e7 100644 --- a/internal/ui/monitor_queue_style_test.go +++ b/internal/ui/monitor_queue_style_test.go @@ -45,6 +45,24 @@ func TestQueuePanelThemeAndGeometry(t *testing.T) { t.Fatalf("wrong background theme %d at %d,%d", theme, x, y) } } + // The queue stays readable at full theme colour without giving + // every pending item the weight of the selected row. + cell := cells.CellAt(0, y) + if bold := cell.Style.Attrs&uv.AttrBold != 0; bold != (y == 1) { + t.Fatalf("wrong queue emphasis theme %d row %d", theme, y) + } + foreground := colors.accent + if y < 2 { + foreground = colors.primary + } + if cell.Style.Fg == nil { + t.Fatal("queue text lost its theme colour") + } + r, g, b, a := cell.Style.Fg.RGBA() + wr, wg, wb, wa := foreground.RGBA() + if r != wr || g != wg || b != wb || a != wa { + t.Fatalf("queue text was dimmed theme %d row %d", theme, y) + } } entry := m.followupEntries()[0] buttons := followupButtons(entry, width) diff --git a/internal/ui/monitor_queue_test.go b/internal/ui/monitor_queue_test.go index 57a14b5..6cdd0da 100644 --- a/internal/ui/monitor_queue_test.go +++ b/internal/ui/monitor_queue_test.go @@ -174,40 +174,108 @@ func TestComposerDoesNotStoreInFlightOrQuestionDrafts(t *testing.T) { } } +func TestFollowupRowsWithoutIndent(t *testing.T) { + m, _ := queueTestModel(false) + colors := paletteFor(m.theme) + lines := strings.Split(ansi.Strip(m.renderMonitorQueue(120, 3, colors)), "\n") + if !strings.HasPrefix(lines[1], "QUEUED // Check the tests") || !strings.HasPrefix(lines[2], "QUEUED // Update documentation") { + t.Fatalf("queue rows have unnecessary indentation: %q", lines) + } + buttonX := ansi.StringWidth(lines[1][:strings.Index(lines[1], "[EDIT]")]) + m.monitorQueue.focused = true + selected := strings.Split(ansi.Strip(m.renderMonitorQueue(120, 3, colors)), "\n") + if !strings.HasPrefix(selected[1], "› QUEUED // Check the tests") || ansi.StringWidth(selected[1][:strings.Index(selected[1], "[EDIT]")]) != buttonX { + t.Fatal("selection marker was lost or moved the queue controls") + } +} + func TestNativeQueuePlacementAndClickSurfaces(t *testing.T) { for _, inline := range []bool{false, true} { - m, _ := queueTestModel(inline) - lines := strings.Split(ansi.Strip(m.render()), "\n") - queueY, composerY := -1, -1 - for y, line := range lines { - if strings.Contains(line, "FOLLOW-UPS // 2") { - queueY = y - } - if strings.Contains(line, "WORKING") && strings.Contains(line, "FOLLOW-UP") { - composerY = y - } - for label, action := range map[string]string{"[EDIT]": "queue:edit:0", "[×]": "queue:delete:0"} { - if x := strings.Index(line, label); x >= 0 && strings.Contains(line, "Check the tests") { - x = ansi.StringWidth(line[:x]) - for i := 0; i < ansi.StringWidth(label); i++ { - if got := m.monitorContextAt(x+i, y); got != action { - t.Fatalf("inline %v %s at %d,%d = %q", inline, label, x+i, y, got) + for _, size := range [][2]int{{80, 32}, {120, 40}, {200, 55}} { + m, _ := queueTestModel(inline) + m.width, m.height = size[0], size[1] + lines := strings.Split(ansi.Strip(m.render()), "\n") + queueY, composerY, activityY := -1, -1, -1 + for y, line := range lines { + if strings.Contains(line, monitorDotWave(m.phase)) { + activityY = y + } + if strings.Contains(line, "FOLLOW-UPS // 2") { + queueY = y + } + if strings.Contains(line, "Click here or press Enter to write") || strings.Contains(line, "Keep this draft") { + composerY = y + } + for label, action := range map[string]string{"[EDIT]": "queue:edit:0", "[×]": "queue:delete:0"} { + if x := strings.Index(line, label); x >= 0 && strings.Contains(line, "Check the tests") { + x = ansi.StringWidth(line[:x]) + for i := 0; i < ansi.StringWidth(label); i++ { + if got := m.monitorContextAt(x+i, y); got != action { + t.Fatalf("inline %v %s at %d,%d = %q", inline, label, x+i, y, got) + } } } } } + if queueY < 0 || composerY <= queueY { + t.Fatalf("queue not above composer: inline %v", inline) + } + if activityY != queueY-1 || strings.Count(strings.Join(lines, "\n"), monitorDotWave(m.phase)) != 1 { + t.Fatal("task activity must appear exactly once immediately above follow-ups") + } + if inline && !strings.Contains(lines[queueY], "1–1") { + t.Fatal("wide row didn't collapse queue") + } + // Composer remains anchored at its existing position. + w, h := m.monitorPromptSize() + rows := m.monitorPromptRows(w, h) + if rows != 3 && !m.monitorPrompt.input.Focused() { + t.Fatal("queue changed composer size") + } + m.monitorQueue.items = nil + withoutQueue := strings.Split(ansi.Strip(m.render()), "\n") + if withoutQueue[composerY] != lines[composerY] || !strings.Contains(withoutQueue[composerY-1], monitorDotWave(m.phase)) { + t.Fatal("moving animation shifted composer or lost its no-queue placement") + } } - if queueY < 0 || composerY <= queueY { - t.Fatalf("queue not above composer: inline %v", inline) + } +} + +func TestQueueActivityWithoutComposerAndSuppressedStates(t *testing.T) { + for _, inline := range []bool{false, true} { + m, c := queueTestModel(inline) + c.active = codex.SessionPromptOffer{} + m.monitorPrompt = monitorPromptState{} + view := ansi.Strip(m.render()) + lines := strings.Split(view, "\n") + found := false + for y, line := range lines { + if strings.Contains(line, "FOLLOW-UPS // 2") { + found = true + if y == 0 || !strings.Contains(lines[y-1], monitorDotWave(m.phase)) { + t.Fatal("standalone task animation remained below queue") + } + } } - if inline && !strings.Contains(lines[queueY], "1–1") { - t.Fatal("wide row didn't collapse queue") + if !found || strings.Count(view, monitorDotWave(m.phase)) != 1 { + t.Fatal("missing queue or duplicated task animation") } - // Composer remains anchored at its existing position. - w, h := m.monitorPromptSize() - rows := m.monitorPromptRows(w, h) - if rows != 3 && !m.monitorPrompt.input.Focused() { - t.Fatal("queue changed composer size") + for _, state := range []string{"idle", "paused", "error", "attention"} { + next := m + next.monitorSessionData = append([]monitorSession(nil), m.monitorSessionData...) + switch state { + case "idle": + next.monitorSessionData[0].working = false + case "paused": + next.monitorState = monitorPaused + case "error": + next.monitorError = "stale" + case "attention": + next.monitorSessionData[0].attention = codex.SessionAttentionInput + } + if strings.Contains(ansi.Strip(next.render()), monitorDotWave(m.phase)) { + t.Fatalf("queue animated %s session", state) + } } } } diff --git a/internal/ui/monitor_schedule.go b/internal/ui/monitor_schedule.go index 0e3a33c..560e35b 100644 --- a/internal/ui/monitor_schedule.go +++ b/internal/ui/monitor_schedule.go @@ -65,13 +65,14 @@ func (m *Model) openSchedule() tea.Cmd { if !m.hasSchedule(id) && (o.Token == "" || o.TurnID != "" || o.ThreadID != id || len(o.Questions) > 0) { return nil } + m.showLiveMonitorHistory() q := m.scheduleUI.queue if q == nil { q = schedule.New() } p := scheduleUI{queue: q, polling: m.scheduleUI.polling, drafts: m.scheduleUI.drafts, open: true, session: id, input: newMonitorEditor(), hours: 1, date: time.Now().Add(time.Hour).Truncate(time.Minute)} g := m.monitorDashboardLayout() - p.composerRows = max(3, m.monitorPromptRows(g.contentWidth, g.meterHeight)) + p.composerRows = max(2, m.monitorPromptRows(g.contentWidth, g.meterHeight)) if m.monitorPrompt.session == id { p.input = m.monitorPrompt.input } @@ -214,6 +215,7 @@ func (m Model) updateSchedule(msg tea.Msg) (Model, tea.Cmd, bool) { } m.monitorSessions = m.visibleMonitorSessionCount() m.setRowContext(id, contextFull) + m.showLiveMonitorHistory() row := m.monitorContextRows[id] row.review = "context" m.monitorContextRows[id] = row @@ -244,7 +246,7 @@ func (m Model) updateSchedule(msg tea.Msg) (Model, tea.Cmd, bool) { return m, nil, true } g := m.monitorDashboardLayout() - p.input.configure(g.contentWidth, m.schedulePaneLayout(g.contentWidth, g.meterHeight).composerRows+6) + p.input.configure(g.contentWidth, m.schedulePaneLayout(g.contentWidth, g.meterHeight).composerRows+7) if wheel, ok := msg.(tea.MouseWheelMsg); ok { layout := m.schedulePaneLayout(g.contentWidth, g.meterHeight) if wheel.Y >= g.meterY+1 && wheel.Y < g.meterY+1+layout.textRows { @@ -590,6 +592,8 @@ func (p scheduleUI) validation(now time.Time) string { switch { case text == "": return i18n.Text("Enter a message to schedule.") + case codex.IsSessionCommand(text): + return i18n.Text("Slash commands cannot be scheduled. Use // for literal slash text.") case len([]rune(text)) > 4096: return i18n.Text("Keep the message within 4096 characters.") case codex.SanitizeSessionContext(text) != text: diff --git a/internal/ui/monitor_schedule_pane.go b/internal/ui/monitor_schedule_pane.go index 42fddac..7d93e45 100644 --- a/internal/ui/monitor_schedule_pane.go +++ b/internal/ui/monitor_schedule_pane.go @@ -19,7 +19,7 @@ func (m Model) schedulePaneLayout(width, height int) schedulePaneLayout { for row := 9; row <= min(p.actionRow()+1, 23); row++ { l.indices = append(l.indices, row) } - l.composerRows = min(max(p.composerRows, 3), max(height-5, 1)) + l.composerRows = min(max(p.composerRows, 2), max(height-5, 1)) l.textRows, _, l.composerY = monitorContextBodyLayout(height, l.composerRows) l.scroll = min(max(p.scroll, 0), max(len(l.indices)-l.textRows, 0)) return l @@ -93,31 +93,28 @@ func (m Model) renderScheduleDetail(width, height int, colors palette) string { body[row] = document[l.indices[row+l.scroll]] } editor := m.scheduleUI.input - editor.configure(width, l.composerRows+6) + editor.configure(width, l.composerRows+7) composer := make([]string, l.composerRows) - identity := m.scheduleUI.session - if s, ok := m.contextDetailSession(); ok { - identity = monitorSessionIdentity(s) - } - composer[0] = m.renderScheduleToggleHeader(width, identity, true, colors) input := strings.Split(editor.View(colors), "\n") inputEnd := l.composerRows - if l.composerRows >= 3 { + if l.composerRows >= 2 { inputEnd-- } - for row := 1; row < inputEnd; row++ { - if row-1 < len(input) { - composer[row] = input[row-1] + for row := 0; row < inputEnd; row++ { + if row < len(input) { + composer[row] = input[row] } else { composer[row] = colors.label().Background(monitorComposerBackground(colors)).Width(max(width-4, 1)).Render("") } } hint := m.scheduleUI.focusHint() + hintStyle := colors.dimmed() if m.scheduleUI.notice != "" { hint = m.scheduleUI.notice + hintStyle = colors.label() } - if l.composerRows >= 3 { - composer[l.composerRows-1] = colors.label().Render(ansi.Truncate(hint, max(width-4, 1), "…")) + if l.composerRows >= 2 { + composer[l.composerRows-1] = m.renderScheduleToggleHint(width, hint, true, hintStyle, colors) } else if l.composerRows == 1 && len(input) > 0 { composer[0] = input[0] } diff --git a/internal/ui/monitor_schedule_test.go b/internal/ui/monitor_schedule_test.go index cd6de9c..50daa8c 100644 --- a/internal/ui/monitor_schedule_test.go +++ b/internal/ui/monitor_schedule_test.go @@ -61,6 +61,8 @@ func TestScheduleFormRefinements(t *testing.T) { want string }{ {"", 0, 0, 0, now, "Enter a message"}, + {"/model", 0, 0, 0, now, "Slash commands cannot be scheduled"}, + {"//literal", 0, 0, 0, now, ""}, {"Test", 1, 0, 0, now, "Choose a future"}, {"Test", 2, 0, 0, now.Add(-time.Minute), "Choose a future"}, {"Test", 2, 0, 0, now.AddDate(2, 0, 0), "within one year"}, @@ -498,7 +500,7 @@ func TestScheduledIdleDetailTitleHasNoDuplicateStatus(t *testing.T) { if !strings.Contains(lines[0], "● TRIGGER SET") || strings.Count(out, "TRIGGER SET") != 1 || strings.Contains(out, "SESSION CONTEXT") { t.Fatal("idle trigger state missing, blinking, or repeated", out) } - if !strings.Contains(out, "FOLLOW-UP //") || !strings.Contains(out, "The saved trigger prompt") { + if strings.Contains(out, "FOLLOW-UP //") || !strings.Contains(out, "The saved trigger prompt") { t.Fatal("trigger information lost", out) } } diff --git a/internal/ui/monitor_schedule_toggle.go b/internal/ui/monitor_schedule_toggle.go index 3043c39..1b453e6 100644 --- a/internal/ui/monitor_schedule_toggle.go +++ b/internal/ui/monitor_schedule_toggle.go @@ -1,36 +1,36 @@ package ui import ( + "strings" + + "charm.land/lipgloss/v2" "github.com/charmbracelet/x/ansi" "github.com/merefield/codexometer/internal/i18n" ) -// Reserve the whole button before truncating the session heading, and share +// Reserve the whole button before truncating the key hints, and share // those exact display columns with hover/click handling. -func scheduleToggleParts(width int, identity string, open bool) (prefix, button string) { - prefix = i18n.Text("FOLLOW-UP") + " // " + identity +func scheduleToggleParts(width int, hint string, open bool) (prefix, button string) { button = i18n.Text("[ Ctrl+S: SCHEDULE ]") if open { button = i18n.Text("[ Ctrl+S: CLOSE SCHEDULE ]") } inner := max(width-4, 1) if inner < ansi.StringWidth(button) { - return ansi.Truncate(prefix, inner, "…"), "" - } - room := inner - ansi.StringWidth(button) - 4 - if room <= 0 { - return "", button + return ansi.Truncate(hint, inner, "…"), "" } - return ansi.Truncate(prefix, room, "…") + " // ", button + room := inner - ansi.StringWidth(button) + prefix = ansi.Truncate(hint, max(room-2, 0), "…") + return prefix + strings.Repeat(" ", max(room-ansi.StringWidth(prefix), 0)), button } -func (m Model) renderScheduleToggleHeader(width int, identity string, open bool, colors palette) string { - prefix, button := scheduleToggleParts(width, identity, open) +func (m Model) renderScheduleToggleHint(width int, hint string, open bool, hintStyle lipgloss.Style, colors palette) string { + prefix, button := scheduleToggleParts(width, hint, open) style := colors.label().Foreground(colors.primary) if m.scheduleUI.toggleHover { style = style.Foreground(colors.background).Background(colors.primary) } - return colors.label().Render(prefix) + style.Render(button) + return hintStyle.Render(prefix) + style.Render(button) } func (m Model) scheduleToggleAt(x, y int) bool { @@ -38,10 +38,7 @@ func (m Model) scheduleToggleAt(x, y int) bool { return false } g := m.monitorDashboardLayout() - identity := "" - if s, ok := m.contextDetailSession(); ok { - identity = monitorSessionIdentity(s) - } else { + if _, ok := m.contextDetailSession(); !ok { return false } var row int @@ -50,7 +47,7 @@ func (m Model) scheduleToggleAt(x, y int) bool { if layout.composerRows < 2 { return false } - row = layout.composerY + row = layout.composerY + layout.composerRows - 1 } else { o := m.monitorPromptOffer() if o.Token == "" || o.TurnID != "" || len(o.Questions) > 0 { @@ -61,8 +58,9 @@ func (m Model) scheduleToggleAt(x, y int) bool { return false } _, _, row = monitorContextBodyLayout(g.meterHeight, rows) + row += rows - 1 } - prefix, button := scheduleToggleParts(g.contentWidth, identity, m.scheduleUI.open) + prefix, button := scheduleToggleParts(g.contentWidth, "", m.scheduleUI.open) start := 4 + ansi.StringWidth(prefix) return button != "" && y == g.meterY+row && x >= start && x < start+ansi.StringWidth(button) } diff --git a/internal/ui/monitor_schedule_toggle_test.go b/internal/ui/monitor_schedule_toggle_test.go index 169d2d3..6282bd7 100644 --- a/internal/ui/monitor_schedule_toggle_test.go +++ b/internal/ui/monitor_schedule_toggle_test.go @@ -6,10 +6,11 @@ import ( tea "charm.land/bubbletea/v2" "github.com/charmbracelet/x/ansi" + "github.com/merefield/codexometer/internal/i18n" ) func TestScheduleToggleRenderedHoverAndClicks(t *testing.T) { - for _, width := range []int{60, 100, 160} { + for _, width := range []int{28, 40, 60, 100, 160} { m, _ := promptTestModel() m.width = width m.monitorSessionData[0].name = strings.Repeat("Long session 日本語 ", 8) @@ -28,8 +29,30 @@ func TestScheduleToggleRenderedHoverAndClicks(t *testing.T) { } } if x < 0 { + g := m.monitorDashboardLayout() + if g.contentWidth-4 < ansi.StringWidth(label) { + next, _ := m.Update(tea.KeyPressMsg{Code: 's', Mod: tea.ModCtrl}) + m = next.(Model) + if m.scheduleUI.open == open { + t.Fatal("hidden schedule button lost its keyboard shortcut") + } + continue + } t.Fatalf("width %d missing complete button %q", width, label) } + g := m.monitorDashboardLayout() + if open { + layout := m.schedulePaneLayout(g.contentWidth, g.meterHeight) + if y != g.meterY+layout.composerY+layout.composerRows-1 { + t.Fatal("close schedule button is not on the hint row") + } + } else { + rows := m.monitorPromptRows(g.contentWidth, g.meterHeight) + _, _, cy := monitorContextBodyLayout(g.meterHeight, rows) + if y != g.meterY+cy+rows-1 || rows != 2 { + t.Fatal("idle composer retained a heading or misplaced schedule button") + } + } for column := x; column < x+ansi.StringWidth(label); column++ { if !m.scheduleToggleAt(column, y) { t.Fatalf("button cell not clickable: %d,%d", column, y) @@ -60,3 +83,21 @@ func TestScheduleToggleRenderedHoverAndClicks(t *testing.T) { } } } + +func TestScheduleToggleHintReservesWholeButton(t *testing.T) { + for _, open := range []bool{false, true} { + label := i18n.Text("[ Ctrl+S: SCHEDULE ]") + if open { + label = i18n.Text("[ Ctrl+S: CLOSE SCHEDULE ]") + } + for _, width := range []int{12, ansi.StringWidth(label) + 4, 40, 100} { + prefix, button := scheduleToggleParts(width, strings.Repeat("Long hint ", 30), open) + if ansi.StringWidth(prefix+button) > max(width-4, 1) { + t.Fatal("hint/button overflow") + } + if width-4 >= ansi.StringWidth(label) && button != label || width-4 < ansi.StringWidth(label) && button != "" { + t.Fatal("button was clipped or partially rendered") + } + } + } +} diff --git a/internal/ui/monitor_statusline.go b/internal/ui/monitor_statusline.go new file mode 100644 index 0000000..b7a14cd --- /dev/null +++ b/internal/ui/monitor_statusline.go @@ -0,0 +1,106 @@ +package ui + +import ( + "strings" + + "github.com/charmbracelet/x/ansi" + "github.com/merefield/codexometer/internal/codex" + "github.com/merefield/codexometer/internal/statusline" +) + +func localStatusLineChoice() codex.SessionCommandChoice { + return codex.SessionCommandChoice{ID: "local-statusline", Label: "/statusline", Help: "Choose multiple fields for Codexometer's detail footer. Saved display preferences only.", Next: "statusline"} +} + +func localCommandsMenu() codex.SessionCommandMenu { + return codex.SessionCommandMenu{Title: "/ COMMANDS", Help: "Codex catalogue unavailable. Local display settings remain available.", Choices: []codex.SessionCommandChoice{localStatusLineChoice()}} +} + +func (m Model) statusLineMenu() codex.SessionCommandMenu { + menu := codex.SessionCommandMenu{Title: "/statusline", Path: "statusline", Multiple: true, Help: "Choose fields for Codexometer's detail footer. Space/Enter toggles; C applies; Escape cancels. Left/Right reorders. This does not change Codex CLI configuration. Missing telemetry is omitted."} + selected := statusline.Normalize(m.monitorStatusLine) + fields := statusline.Fields() + // Keep selected fields in display order, followed by the remaining fields. + for _, id := range selected { + for _, f := range fields { + if f.ID == id { + menu.Choices = append(menu.Choices, codex.SessionCommandChoice{ID: f.ID, Label: f.Label, Help: f.Help, Selected: true}) + } + } + } + for _, f := range fields { + found := false + for _, id := range selected { + found = found || id == f.ID + } + if !found { + menu.Choices = append(menu.Choices, codex.SessionCommandChoice{ID: f.ID, Label: f.Label, Help: f.Help}) + } + } + return menu +} + +func statusLineSelection(menu codex.SessionCommandMenu) []string { + selected := []string{} + for _, o := range menu.Choices { + if o.Selected { + selected = append(selected, o.ID) + } + } + return selected +} + +func (m Model) monitorStatusValues() map[string]string { + s, ok := m.contextDetailSession() + if !ok { + return nil + } + state := monitorSessionAttentionLabel(s) + if state == "" { + if s.working { + state = "WORKING" + } else { + state = "IDLE" + } + } + if m.monitorError != "" || m.monitorState != monitorRunning { + state = "STALE" + } + return statusline.Values(statusline.Data{Model: s.modelSettings.Model, Effort: s.modelSettings.ReasoningEffort, Speed: s.modelSettings.ServiceTier, Directory: s.workingDirectory, Name: s.name, ID: s.id, State: state, Source: s.preview.Source, Tokens: s.latest, Agents: s.agentCount}) +} + +func (m Model) monitorStatusLineText(width int, copyLabel string) string { + end := width - 2 + if copyLabel != "" { + end = width - ansi.StringWidth(copyLabel) - 3 + } + if end < 12 { + return "" + } + text := statusline.Text(m.monitorStatusLine, m.monitorStatusValues()) + if text == "" { + return "" + } + return ansi.Truncate(text, max(end-3, 1), "…") +} + +func (m Model) monitorStatusLineVisible(width int) bool { + if m.meterView != viewMonitor || m.monitorContextDetail == "" || m.contextTargetHidden() || m.monitorCommands.open || m.monitorQueue.open { + return false + } + return m.monitorStatusLineText(width, m.renderMonitorCopy(width, m.monitorContextDetail, paletteFor(m.theme))) != "" +} + +// Write into the existing bottom border; dashboard geometry reserves a blank +// row below it only when this text is actually visible. +func (m Model) withMonitorStatusLine(panel string, width int, copyLabel string, colors palette) string { + text := m.monitorStatusLineText(width, copyLabel) + if text == "" { + return panel + } + lines := strings.Split(panel, "\n") + last := lines[len(lines)-1] + value := colors.label().Foreground(colors.primary).Background(colors.background).Render(" " + text + " ") + lines[len(lines)-1] = ansi.Cut(last, 0, 1) + value + ansi.Cut(last, 1+ansi.StringWidth(value), width) + return strings.Join(lines, "\n") +} diff --git a/internal/ui/monitor_statusline_test.go b/internal/ui/monitor_statusline_test.go new file mode 100644 index 0000000..b748f2f --- /dev/null +++ b/internal/ui/monitor_statusline_test.go @@ -0,0 +1,187 @@ +package ui + +import ( + "image" + "reflect" + "strings" + "testing" + + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" + uv "github.com/charmbracelet/ultraviolet" + "github.com/charmbracelet/x/ansi" + "github.com/merefield/codexometer/internal/codex" +) + +func TestStatusLineMultiSelectReorderCancelAndPersist(t *testing.T) { + m := completedCopyModel() + m.setRowContext("root-one", contextFull) + store := &memoryPreferenceStore{} + m.preferenceStore = store + m, _, _ = m.openMonitorCommands("/statusline") + if !m.monitorCommands.menu.Multiple { + t.Fatal("not a multi-select picker") + } + before := m.monitorCommands.menu.Choices[0].Selected + n, _ := m.Update(tea.KeyPressMsg{Code: ' ', Text: " "}) + m = n.(Model) + if m.monitorCommands.menu.Choices[0].Selected == before { + t.Fatal("Space did not toggle") + } + n, _ = m.Update(tea.KeyPressMsg{Code: tea.KeyRight}) + m = n.(Model) + if m.monitorCommands.selected != 1 || m.monitorCommands.menu.Choices[1].ID != "model-with-reasoning" { + t.Fatal("Right did not reorder") + } + n, _ = m.Update(tea.KeyPressMsg{Code: tea.KeyEscape}) + m = n.(Model) + if len(store.saves) != 0 || m.monitorStatusLine != nil { + t.Fatal("Cancel changed preferences") + } + m, _, _ = m.openMonitorCommands("statusline") + x, y := renderedTextStart(t, m, "[x] Tokens") + n, _ = m.Update(tea.MouseClickMsg{X: x, Y: y, Button: tea.MouseLeft}) + m = n.(Model) + for _, o := range m.monitorCommands.menu.Choices { + if o.ID == "used-tokens" && o.Selected { + t.Fatal("checkbox click missed") + } + } + x, y = renderedTextStart(t, m, "[ C APPLY ]") + n, _ = m.Update(tea.MouseClickMsg{X: x, Y: y, Button: tea.MouseLeft}) + m = n.(Model) + if m.monitorCommands.open || len(store.saves) != 1 { + t.Fatal("Apply did not save and close") + } + if !reflect.DeepEqual(m.monitorStatusLine, []string{"model-with-reasoning", "fast-mode", "current-dir"}) { + t.Fatal(m.monitorStatusLine) + } + fresh := Model{} + fresh.applyPreferences(store.preferences) + if !reflect.DeepEqual(fresh.monitorStatusLine, m.monitorStatusLine) { + t.Fatal("preference not restored") + } +} + +func TestStatusLineFooterPreservesCopyGeometry(t *testing.T) { + for _, width := range []int{24, 60, 120, 180} { + m := completedCopyModel() + m.width = width + m.setRowContext("root-one", contextFull) + m.monitorSessionData[0].modelSettings = codex.SessionModelSettings{Model: "model", ReasoningEffort: "high", ServiceTier: "fast"} + m.monitorSessionData[0].latest = 1200 + m.monitorSessionData[0].workingDirectory = "/work\nproject" + g := m.monitorDashboardLayout() + colors := paletteFor(m.theme) + panel := m.renderMonitorContextDetail(g.contentWidth, g.meterHeight, colors) + lines := strings.Split(ansi.Strip(panel), "\n") + if len(lines) != g.meterHeight { + t.Fatalf("width %d changed frame height: %d vs %d", width, len(lines), g.meterHeight) + } + for _, line := range lines { + if lipgloss.Width(line) > g.contentWidth { + t.Fatalf("footer overflow at width %d", width) + } + } + if width >= 60 && !strings.Contains(lines[len(lines)-1], "model high") { + t.Fatal("footer not at bottom", lines[len(lines)-1]) + } + if strings.Contains(ansi.Strip(m.render()), benchmarkDetailCopyLabel) { + x, y := renderedTextStart(t, m, benchmarkDetailCopyLabel) + if m.monitorContextAt(x, y) != "copy:root-one" { + t.Fatal("footer moved Copy target") + } + } + m.monitorStatusLine = []string{} + if strings.Contains(ansi.Strip(m.renderMonitorContextDetail(g.contentWidth, g.meterHeight, colors)), "model high") { + t.Fatal("empty selection retained footer") + } + } +} + +func TestStatusLineReservesFooterGapOnlyWhenDisplayed(t *testing.T) { + for _, size := range [][2]int{{60, 12}, {60, 16}, {60, 24}, {120, 24}, {180, 65}} { + for _, scenario := range []string{"shown", "hidden", "missing", "commands", "scheduler", "queue", "wide"} { + if scenario == "wide" && size[1] < 24 { + continue // overview rows require more height than full detail + } + m := completedCopyModel() + m.width, m.height = size[0], size[1] + m.setRowContext("root-one", contextFull) + m.monitorSessionData[0].modelSettings = codex.SessionModelSettings{Model: "model", ReasoningEffort: "high"} + baseline := m + baseline.monitorStatusLine = []string{} + base := baseline.dashboardLayout() + switch scenario { + case "hidden": + m.monitorStatusLine = []string{} + case "missing": + m.monitorStatusLine = []string{"fast-mode"} + case "commands": + m.monitorCommands.open = true + case "scheduler": + m.scheduleUI.open = true + case "queue": + m.monitorQueue.open = true + case "wide": + m.setRowContext("root-one", contextWide) + } + g := m.dashboardLayout() + wantGap := (scenario == "shown" || scenario == "scheduler") && base.meterHeight >= 9 + if g.footerSpacer != wantGap || g.footerY != base.footerY { + t.Fatalf("%v %s: incorrect footer geometry: %+v", size, scenario, g) + } + // Modal editors have their own setup and rendering tests; the + // scheduler retains the status line to keep its composer anchored. + if scenario == "commands" || scenario == "scheduler" || scenario == "queue" { + continue + } + view := ansi.Strip(m.render()) + lines := strings.Split(view, "\n") + if lipgloss.Height(view) > m.height { + t.Fatalf("%v %s: footer gap overflowed terminal", size, scenario) + } + if wantGap && (strings.TrimSpace(lines[g.footerY-1]) != "" || !strings.Contains(lines[g.footerY-2], "model high")) { + t.Fatalf("%v: status line missing its separating blank row", size) + } + if !wantGap && strings.Contains(lines[g.footerY-1], "model high") { + t.Fatal("status line shown without space for its gap") + } + x, y := renderedTextStart(t, m, "[ (T)HEME ]") + if m.footerButtonAt(x, y) != footerButtonTheme { + t.Fatalf("%v %s: footer gap moved the theme click target (%d,%d) expected row %d", size, scenario, x, y, g.footerY+1) + } + } + } +} + +func TestStatusLineAndComposerTipEmphasis(t *testing.T) { + m, _ := promptTestModel() + m.width, m.height = 120, 40 + m.monitorSessionData[0].modelSettings.Model = "my-model" + m.focusMonitorPrompt() + colors := paletteFor(m.theme) + foreground := func(text string) any { + cells := uv.NewScreenBuffer(120, 1) + uv.NewStyledString(text).Draw(&cells, image.Rect(0, 0, 120, 1)) + return cells.CellAt(0, 0).Style.Fg + } + g := m.monitorDashboardLayout() + panel := m.renderMonitorContextDetail(g.contentWidth, g.meterHeight, colors) + lines := strings.Split(panel, "\n") + // Skip the left border cell; the status-line colour starts with its padding. + if !reflect.DeepEqual(foreground(ansi.Cut(lines[len(lines)-1], 1, 2)), foreground(colors.label().Foreground(colors.primary).Render("x"))) { + t.Fatal("status line remains subdued") + } + for _, notice := range []string{"", "Change requested."} { + m.monitorPrompt.notice = notice + prompt := strings.Split(m.renderMonitorPrompt(g.contentWidth, g.meterHeight, colors), "\n") + want := colors.dimmed().Render("x") + if notice != "" { + want = colors.label().Render("x") + } + if !reflect.DeepEqual(foreground(prompt[len(prompt)-1]), foreground(want)) { + t.Fatal("tips must be subdued while notices retain emphasis") + } + } +} diff --git a/internal/ui/monitor_steer.go b/internal/ui/monitor_steer.go new file mode 100644 index 0000000..bdef364 --- /dev/null +++ b/internal/ui/monitor_steer.go @@ -0,0 +1,65 @@ +package ui + +import ( + "time" + + "github.com/merefield/codexometer/internal/codex" +) + +// A steer is already sent, not a queued message that can be edited or cancelled. +// Retain its receipt across selection changes until live guidance catches up. +type monitorSteerReceipt struct { + sentAt time.Time + id uint64 + session, thread, turn, text, baselineID, baselineText string +} + +func (m *Model) recordMonitorSteer(receipt monitorSteerReceipt) { + if receipt.text == "" || receipt.session == "" { + return + } + m.monitorSteerSequence++ + receipt.id = m.monitorSteerSequence + m.monitorSteers = append(append([]monitorSteerReceipt(nil), m.monitorSteers...), receipt) + m.reconcileMonitorSteers() +} + +func (m *Model) reconcileMonitorSteers() { + if len(m.monitorSteers) == 0 { + return + } + // Only a matching thread/turn can confirm a steer. A child approval, an + // empty queue response, or temporarily missing telemetry cannot clear it. + type target struct{ session, thread, turn string } + confirmed := make(map[target]uint64) + for _, receipt := range m.monitorSteers { + for _, session := range m.monitorSessionData { + c := session.preview + if session.id != receipt.session || c.ThreadID != receipt.thread { + continue + } + key := target{receipt.session, receipt.thread, receipt.turn} + if c.TurnID != receipt.turn { + // A positively newer turn supersedes its predecessor's guidance. + if c.TurnID != "" && !receipt.sentAt.IsZero() && c.At.After(receipt.sentAt) { + confirmed[key] = max(confirmed[key], receipt.id) + } + continue + } + changed := c.LatestGuidanceID != "" && c.LatestGuidanceID != receipt.baselineID || + c.LatestGuidanceID == "" && c.LatestGuidance != receipt.baselineText + if changed && codex.SanitizeSessionContext(c.LatestGuidance) == receipt.text { + confirmed[key] = max(confirmed[key], receipt.id) + } + } + } + remaining := make([]monitorSteerReceipt, 0, len(m.monitorSteers)) + for _, receipt := range m.monitorSteers { + // Guidance shows only the latest steer; earlier receipts for that same + // turn are superseded once a later submitted direction is visible. + if receipt.id > confirmed[target{receipt.session, receipt.thread, receipt.turn}] { + remaining = append(remaining, receipt) + } + } + m.monitorSteers = remaining +} diff --git a/internal/ui/monitor_steer_test.go b/internal/ui/monitor_steer_test.go new file mode 100644 index 0000000..a4610f3 --- /dev/null +++ b/internal/ui/monitor_steer_test.go @@ -0,0 +1,131 @@ +package ui + +import ( + "errors" + "strings" + "testing" + "time" + + "github.com/charmbracelet/x/ansi" + "github.com/merefield/codexometer/internal/codex" +) + +func TestMonitorSteerReceipt(t *testing.T) { + for _, inline := range []bool{false, true} { + m, _ := turnTestModel() + if inline { + m, _ = inlineTurnTestModel() + m.focusMonitorPrompt() + } + m.monitorPrompt.input.SetValue("Use Go please") + m, cmd, _ := m.submitMonitorTurn("steer") + m, _, _ = m.updateMonitorPrompt(cmd()) + entries := m.followupEntries() + if len(entries) != 1 || entries[0].text != "Use Go please" || !entries[0].readOnly || m.monitorPrompt.input.Value() != "" { + t.Fatalf("missing steer receipt: %+v", entries) + } + if len(followupButtons(entries[0], 100)) != 0 { + t.Fatal("sent steer offered mutation controls") + } + for _, action := range []string{"edit", "delete", "now"} { + if m.followupAction(0, action) != nil || m.monitorQueue.open { + t.Fatal("sent steer was actionable") + } + } + for _, width := range []int{40, 100, 180} { + text := ansi.Strip(m.renderMonitorQueue(width, 2, paletteFor(m.theme))) + if !strings.Contains(text, "STEER SENT") || strings.Contains(text, "[EDIT]") || strings.Contains(text, "[×]") { + t.Fatalf("bad read-only rendering: %q", text) + } + } + m.monitorContextDetail = "another-session" + if len(m.followupEntries()) != 0 || len(m.monitorSteers) != 1 { + t.Fatal("receipt leaked or was lost on navigation") + } + } +} + +func TestMonitorSteerReconciliation(t *testing.T) { + m, _ := turnTestModel() + r := monitorSteerReceipt{session: "root-one", thread: "root-one", turn: "turn", text: "Use Go", baselineID: "old", baselineText: "Use Go"} + m.recordMonitorSteer(r) + for _, c := range []codex.SessionContext{ + {}, + {ThreadID: "child", TurnID: "turn", LatestGuidance: "Use Go", LatestGuidanceID: "new"}, + {ThreadID: "root-one", TurnID: "other-turn", LatestGuidance: "Use Go", LatestGuidanceID: "new"}, + {ThreadID: "root-one", TurnID: "turn", LatestGuidance: "Use Go", LatestGuidanceID: "old"}, + } { + m.monitorSessionData[0].preview = c + m.reconcileMonitorSteers() + if len(m.monitorSteers) != 1 { + t.Fatalf("unrelated or old observation cleared steer: %+v", c) + } + } + m.monitorSessionData[0].preview.LatestGuidanceID = "new" + m.reconcileMonitorSteers() + if len(m.monitorSteers) != 0 { + t.Fatal("new identical guidance did not reconcile") + } + // Guidance may arrive before the send acknowledgement. + m.recordMonitorSteer(r) + if len(m.monitorSteers) != 0 { + t.Fatal("already visible guidance created a duplicate receipt") + } +} + +func TestMonitorSteerAckAfterNavigationAndFailure(t *testing.T) { + for _, fail := range []bool{false, true} { + m, c := turnTestModel() + if fail { + c.err = errors.New("unconfirmed") + } + m, cmd, _ := m.submitMonitorTurn("steer") + m.monitorPrompt.session = "other-session" + m.monitorPrompt.input.SetValue("Other draft") + m, _, _ = m.updateMonitorPrompt(cmd()) + if m.monitorPrompt.input.Value() != "Other draft" || (len(m.monitorSteers) == 1) == fail { + t.Fatal("late acknowledgement lost receipt or changed another draft") + } + } +} + +func TestMonitorSteersSurviveQueueReadsAndReconcileFromTelemetry(t *testing.T) { + m, c := queueTestModel(false) + m.recordMonitorSteer(monitorSteerReceipt{session: "root-one", thread: "root-one", turn: "turn", text: "First steer"}) + m.recordMonitorSteer(monitorSteerReceipt{session: "root-one", thread: "root-one", turn: "turn", text: "Latest steer"}) + m.recordMonitorSteer(monitorSteerReceipt{session: "root-two", thread: "root-two", turn: "other", text: "Other session"}) + c.items = nil + m.monitorQueue.next = time.Time{} + refresh := m.pollMonitorQueue() + m, _, _ = m.updateMonitorQueue(refresh()) + if len(m.followupEntries()) != 2 || len(m.monitorSteers) != 3 { + t.Fatal("empty native queue erased sent steers") + } + old := m // model updates must not mutate previous receipt slices + updated, _, accepted := m.applyMonitorFetch(monitorFetchedMsg{kind: monitorFetchSample, at: time.Now(), usage: codex.LiveUsageSnapshot{ + TotalTokens: m.monitorLatest, + Sessions: []codex.LiveUsageSession{{ID: "root-one", Active: true, Context: codex.SessionContext{ + ThreadID: "root-one", TurnID: "turn", LatestGuidance: "Latest steer", LatestGuidanceID: "latest", + }}}, + }}) + m = updated.(Model) + if !accepted || len(m.monitorSteers) != 1 || m.monitorSteers[0].session != "root-two" || len(old.monitorSteers) != 3 { + t.Fatal("telemetry failed to retire superseded steers without affecting another session") + } +} + +func TestMonitorSteerRetiresOnlyForPositivelyNewerTurn(t *testing.T) { + m, _ := turnTestModel() + now := time.Now() + m.recordMonitorSteer(monitorSteerReceipt{session: "root-one", thread: "root-one", turn: "turn", text: "Direction", sentAt: now}) + m.monitorSessionData[0].preview = codex.SessionContext{ThreadID: "root-one", TurnID: "previous", At: now.Add(-time.Second)} + m.reconcileMonitorSteers() + if len(m.monitorSteers) != 1 { + t.Fatal("stale turn observation erased sent steer") + } + m.monitorSessionData[0].preview = codex.SessionContext{ThreadID: "root-one", TurnID: "next", At: now.Add(time.Second)} + m.reconcileMonitorSteers() + if len(m.monitorSteers) != 0 { + t.Fatal("new turn retained obsolete steering receipt") + } +} diff --git a/internal/ui/monitor_suggestions.go b/internal/ui/monitor_suggestions.go new file mode 100644 index 0000000..7b7657d --- /dev/null +++ b/internal/ui/monitor_suggestions.go @@ -0,0 +1,241 @@ +package ui + +import ( + "context" + "strings" + "time" + + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" + "github.com/charmbracelet/x/ansi" + "github.com/merefield/codexometer/internal/codex" +) + +type monitorSuggestionState struct { + session, dismissed string + request uint64 + active, busy bool + selected int + menu codex.SessionCommandMenu + err bool +} +type monitorSuggestionResult struct { + session string + request uint64 + menu codex.SessionCommandMenu + err error +} + +func (m Model) suggestionsActive() bool { + p, s := m.monitorPrompt, m.monitorSuggestions + return m.meterView == viewMonitor && !m.monitorCommands.open && !m.scheduleUI.open && !m.monitorQueue.open && + p.session == m.monitorContextTarget() && p.input.Focused() && !p.busy && len(p.offer.Questions) == 0 && + codex.IsSessionCommand(p.input.Value()) && p.input.Value() != s.dismissed +} + +// Fetch once when a slash draft becomes active; subsequent keystrokes filter +// locally. Selection and RPC capabilities never cross session boundaries. +func (m *Model) syncMonitorSuggestions() tea.Cmd { + s := &m.monitorSuggestions + if !m.suggestionsActive() { + s.active = false + return nil + } + if s.active && s.session == m.monitorPrompt.session { + return nil + } + c, ok := m.fetcher.(codex.SessionCommandsClient) + if !ok { + *s = monitorSuggestionState{active: true, session: m.monitorPrompt.session, request: s.request + 1, menu: localCommandsMenu()} + return nil + } + *s = monitorSuggestionState{active: true, busy: true, session: m.monitorPrompt.session, request: s.request + 1} + id, seq := s.session, s.request + return func() tea.Msg { + ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second) + defer cancel() + menu, err := c.SessionCommands(ctx, id, "") + return monitorSuggestionResult{session: id, request: seq, menu: menu, err: err} + } +} + +func (m Model) suggestionChoices() []codex.SessionCommandChoice { + s := m.monitorSuggestions + if !m.suggestionsActive() || s.session != m.monitorPrompt.session { + return nil + } + query := strings.ToLower(strings.TrimSpace(m.monitorPrompt.input.Value())) + var choices []codex.SessionCommandChoice + for _, o := range s.menu.Choices { + if strings.HasPrefix(strings.ToLower(o.Label), query) { + choices = append(choices, o) + } + } + return choices +} + +func (m Model) withMonitorSuggestions(view string, colors palette) string { + popup, start := m.monitorSuggestionPopup() + if popup.height == 0 { + return view + } + choices := m.suggestionChoices() + var lines []string + if len(choices) == 0 { + text := "No matching slash commands." + if m.monitorSuggestions.busy { + text = "Loading slash commands…" + } else if m.monitorSuggestions.err { + text = "Command catalogue unavailable. Use Codex." + } + lines = append(lines, colors.dimmed().Render(ansi.Truncate(text, popup.width-4, "…"))) + } else { + selected := min(m.monitorSuggestions.selected, len(choices)-1) + for i := start; i < min(start+popup.height-2, len(choices)); i++ { + o := choices[i] + label := ansi.Truncate(o.Label, popup.width-4, "…") + style := colors.dimmed().Background(colors.background) + if i == selected { + style = style.Foreground(colors.background).Background(colors.primary).Bold(true) + } + help := ansi.Truncate(" // "+strings.Join(strings.Fields(o.Help), " "), max(popup.width-4-ansi.StringWidth(label), 0), "…") + row := " " + style.Render(label) + colors.dimmed().Background(colors.background).Render(help) + " " + lines = append(lines, lipgloss.NewStyle().Background(colors.background).Width(popup.width-2).Render(row)) + } + } + box := lipgloss.NewStyle().Border(lipgloss.NormalBorder()).BorderForeground(colors.primary). + Background(colors.background).Width(popup.width).Render(strings.Join(lines, "\n")) + return lipgloss.NewCompositor(lipgloss.NewLayer(view), lipgloss.NewLayer(box).X(popup.x).Y(popup.y).Z(1)).Render() +} + +func (m Model) updateMonitorSuggestions(msg tea.Msg) (Model, tea.Cmd, bool) { + s := &m.monitorSuggestions + if r, ok := msg.(monitorSuggestionResult); ok { + if s.active && s.session == r.session && s.request == r.request { + s.busy = false + s.menu = r.menu + s.menu.Choices = append(s.menu.Choices, localStatusLineChoice()) + s.err = r.err != nil + } + return m, nil, true + } + if !m.suggestionsActive() { + s.active = false + return m, nil, false + } + w, h := m.monitorPromptSize() + if m.monitorPromptRows(w, h) == 0 { + return m, nil, false + } + choices := m.suggestionChoices() + if key, ok := msg.(tea.KeyPressMsg); ok { + switch key.String() { + case "esc": + s.dismissed = m.monitorPrompt.input.Value() + s.active = false + return m, nil, true + case "up", "down": + d := 1 + if key.String() == "up" { + d = -1 + } + s.selected = min(max(s.selected+d, 0), max(len(choices)-1, 0)) + return m, nil, true + case "tab", "enter": + if s.busy { + return m, nil, true + } + if len(choices) == 0 { + return m, nil, false + } + o := choices[min(s.selected, len(choices)-1)] + if key.String() == "tab" { + m.monitorPrompt.input.SetValue(o.Label) + m.monitorPrompt.input.CursorEnd() + s.selected = 0 + return m, nil, true + } + return m.openMonitorCommands(o.Next) + } + } + if mouse, ok := msg.(tea.MouseMsg); ok { + x, y := mouse.Mouse().X, mouse.Mouse().Y + if popup, _ := m.monitorSuggestionPopup(); popup.contains(x, y) { + switch mouse.Mouse().Button { + case tea.MouseWheelUp, tea.MouseWheelDown: + d := 3 + if mouse.Mouse().Button == tea.MouseWheelUp { + d = -3 + } + s.selected = min(max(s.selected+d, 0), max(len(choices)-1, 0)) + return m, nil, true + } + } + if i := m.monitorSuggestionAt(x, y); i >= 0 && i < len(choices) { + s.selected = i + if _, click := msg.(tea.MouseClickMsg); click && mouse.Mouse().Button == tea.MouseLeft { + return m.openMonitorCommands(choices[i].Next) + } + return m, nil, true + } + // Popup borders and empty-state rows must not activate controls below. + if popup, _ := m.monitorSuggestionPopup(); popup.contains(x, y) { + return m, nil, true + } + } + return m, nil, false +} + +// One shared rectangle anchors rendering and hit testing immediately above +// the composer, without reserving rows or moving any existing controls. +func (m Model) monitorSuggestionPopup() (popup monitorRect, start int) { + if !m.suggestionsActive() || m.monitorSuggestions.session != m.monitorPrompt.session { + return + } + w, h := m.monitorPromptSize() + rows := m.monitorPromptRows(w, h) + if rows == 0 || w < 12 { + return + } + g := m.monitorDashboardLayout() + x, y := 4, g.meterY + if m.monitorContextDetail == "" { + a := m.monitorArea(g.contentWidth, g.meterHeight) + sessions, heights, _ := m.monitorSessionPage(a.graphHeight) + rowY := a.topHeight + a.gap - 1 + found := false + for i, s := range sessions { + if s.id == m.monitorContextTarget() { + mw, _, _ := monitorSessionColumnWidths(a.width) + x += mw + 1 + y += rowY + found = true + break + } + rowY += heights[i] + } + if !found { + return + } + } + _, _, cy := monitorContextBodyLayout(h, rows) + choices := m.suggestionChoices() + count := min(max(len(choices), 1), max(cy-3, 0)) + if count == 0 { + return + } + width := min(w-4, 44) + for _, o := range choices { + width = min(w-4, max(width, ansi.StringWidth(o.Label+" // "+strings.Join(strings.Fields(o.Help), " "))+4)) + } + start = max(min(m.monitorSuggestions.selected, max(len(choices)-1, 0))-count+1, 0) + return monitorRect{x: x, y: y + cy - count - 2, width: width, height: count + 2}, start +} + +func (m Model) monitorSuggestionAt(x, y int) int { + popup, start := m.monitorSuggestionPopup() + if popup.height == 0 || x <= popup.x || x >= popup.x+popup.width-1 || y <= popup.y || y >= popup.y+popup.height-1 || len(m.suggestionChoices()) == 0 { + return -1 + } + return start + y - popup.y - 1 +} diff --git a/internal/ui/monitor_suggestions_test.go b/internal/ui/monitor_suggestions_test.go new file mode 100644 index 0000000..a4adc40 --- /dev/null +++ b/internal/ui/monitor_suggestions_test.go @@ -0,0 +1,201 @@ +package ui + +import ( + "context" + "image" + "reflect" + "strings" + "testing" + + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" + uv "github.com/charmbracelet/ultraviolet" + "github.com/charmbracelet/x/ansi" + "github.com/merefield/codexometer/internal/codex" +) + +type suggestionClient struct { + *commandTestClient + reads int +} + +func (f *suggestionClient) SessionCommands(ctx context.Context, id, path string) (codex.SessionCommandMenu, error) { + f.reads++ + if path != "" { + return f.commandTestClient.SessionCommands(ctx, id, path) + } + return codex.SessionCommandMenu{Title: "/ COMMANDS", Choices: []codex.SessionCommandChoice{ + {ID: "model", Label: "/model", Help: "Choose model and reasoning", Next: "model"}, + {ID: "mcp", Label: "/mcp", Help: "Browse tool descriptions", Next: "mcp"}, + }}, nil +} +func suggestionTestModel(t *testing.T) (Model, *suggestionClient) { + t.Helper() + m, p := promptTestModel() + f := &suggestionClient{commandTestClient: &commandTestClient{promptTestClient: p}} + m.fetcher = f + m.focusMonitorPrompt() + m.monitorPrompt.input.SetValue("/") + cmd := m.syncMonitorSuggestions() + if cmd == nil { + t.Fatal("missing catalogue fetch") + } + n, _ := m.Update(cmd()) + return n.(Model), f +} + +func TestMonitorSuggestionsFilterCompleteAndOpen(t *testing.T) { + m, f := suggestionTestModel(t) + n, _ := m.Update(key('m')) + m = n.(Model) + if len(m.suggestionChoices()) != 2 || f.reads != 1 { + t.Fatal("typing did not filter locally") + } + n, _ = m.Update(tea.KeyPressMsg{Code: tea.KeyDown}) + m = n.(Model) + n, _ = m.Update(tea.KeyPressMsg{Code: tea.KeyTab}) + m = n.(Model) + if m.monitorPrompt.input.Value() != "/mcp" || f.calls != 0 { + t.Fatal("Tab did not complete without sending") + } + n, cmd := m.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) + m = n.(Model) + if !m.monitorCommands.open || cmd == nil || f.calls != 0 || len(f.answers) != 0 { + t.Fatal("Enter did not open options without sending") + } +} + +func TestMonitorSuggestionsEscapeAndSessionIsolation(t *testing.T) { + m, _ := suggestionTestModel(t) + n, _ := m.Update(tea.KeyPressMsg{Code: tea.KeyEscape}) + m = n.(Model) + if m.suggestionsActive() || !m.monitorPrompt.input.Focused() || m.monitorPrompt.input.Value() != "/" { + t.Fatal("Escape should only dismiss suggestions") + } + n, _ = m.Update(key('m')) + m = n.(Model) + if !m.suggestionsActive() { + t.Fatal("editing did not reopen suggestions") + } + m.monitorContextDetail = "root-two" + n, _ = m.Update(monitorSuggestionResult{session: "root-one", request: m.monitorSuggestions.request - 1, menu: codex.SessionCommandMenu{Title: "stale"}}) + m = n.(Model) + if len(m.suggestionChoices()) != 0 { + t.Fatal("suggestions crossed session boundary") + } +} + +func TestMonitorSuggestionsRenderedClicks(t *testing.T) { + for _, mode := range []int{contextFull, contextWide} { + for _, width := range []int{100, 180} { + m, f := suggestionTestModel(t) + m.width = width + m.height = 65 + m.monitorSessionData = m.monitorSessionData[:1] + m.monitorSessionData[0].preview.Text = "Ready." + m.setRowContext("root-one", mode) + m.focusMonitorPrompt() + if !strings.Contains(ansi.Strip(m.render()), "/mcp // Browse tool") { + t.Fatalf("mode %d width %d suggestions missing", mode, width) + } + x, y := renderedTextStart(t, m, "/mcp // Browse tool") + if got := m.monitorSuggestionAt(x, y); got != 1 { + t.Fatalf("mode %d width %d hit %d at %d,%d", mode, width, got, x, y) + } + n, cmd := m.Update(tea.MouseClickMsg{X: x, Y: y, Button: tea.MouseLeft}) + m = n.(Model) + if !m.monitorCommands.open || cmd == nil || f.calls != 0 { + t.Fatal("suggestion click did not open safe menu") + } + } + } +} + +func TestMonitorSuggestionPopupShrinksWithoutMovingComposer(t *testing.T) { + for _, mode := range []int{contextFull, contextWide} { + m, f := suggestionTestModel(t) + m.width, m.height = 120, 50 + m.monitorSessionData = m.monitorSessionData[:1] + m.setRowContext("root-one", mode) + m.focusMonitorPrompt() + w, h := m.monitorPromptSize() + composerRows := m.monitorPromptRows(w, h) + initial, _ := m.monitorSuggestionPopup() + if initial.height != 5 { // three commands and a two-line border + t.Fatalf("mode %d: popup height %d", mode, initial.height) + } + for _, step := range []struct { + text string + count int + }{{"/m", 2}, {"/mo", 1}, {"/none", 0}} { + m.monitorPrompt.input.SetValue(step.text) + if cmd := m.syncMonitorSuggestions(); cmd != nil || f.reads != 1 { + t.Fatal("filtering refetched the catalogue") + } + popup, _ := m.monitorSuggestionPopup() + if popup.height != max(step.count, 1)+2 || popup.y+popup.height != initial.y+initial.height || m.monitorPromptRows(w, h) != composerRows { + t.Fatalf("mode %d query %s: popup did not shrink in place: %+v", mode, step.text, popup) + } + view := m.render() + if lipgloss.Width(view) > m.width || lipgloss.Height(view) > m.height { + t.Fatal("popup overflowed the terminal") + } + if m.monitorSuggestionAt(popup.x, popup.y+1) != -1 || m.monitorSuggestionAt(popup.x+1, popup.y) != -1 { + t.Fatal("popup border became a suggestion target") + } + if step.count == 0 && (!strings.Contains(ansi.Strip(view), "No matching") || m.monitorSuggestionAt(popup.x+1, popup.y+1) != -1) { + t.Fatal("empty state became selectable or disappeared") + } + } + } +} + +func TestMonitorSuggestionPopupScrollAndResize(t *testing.T) { + m, _ := suggestionTestModel(t) + m.width, m.height = 100, 24 + for i := 0; i < 30; i++ { + m.monitorSuggestions.menu.Choices = append(m.monitorSuggestions.menu.Choices, codex.SessionCommandChoice{ID: "other", Label: "/other", Help: "Other command"}) + } + m.monitorSuggestions.selected = len(m.suggestionChoices()) - 1 + for _, size := range [][2]int{{100, 24}, {60, 20}, {180, 60}} { + m.width, m.height = size[0], size[1] + popup, start := m.monitorSuggestionPopup() + if popup.height == 0 || popup.x < 0 || popup.y < 0 || popup.x+popup.width > m.width || popup.y+popup.height > m.height { + t.Fatalf("popup not bounded at %v: %+v", size, popup) + } + last := start + popup.height - 3 + if got := m.monitorSuggestionAt(popup.x+1, popup.y+popup.height-2); got != last || last != m.monitorSuggestions.selected { + t.Fatalf("scrolled hit target %d want %d", got, m.monitorSuggestions.selected) + } + before := m.monitorSuggestions.selected + n, cmd := m.Update(tea.MouseWheelMsg{X: popup.x + 1, Y: popup.y + 1, Button: tea.MouseWheelUp}) + if cmd != nil || n.(Model).monitorSuggestions.selected != max(before-3, 0) { + t.Fatal("mouse wheel did not navigate popup without executing a command") + } + } +} + +func TestMonitorSuggestionHelpIsSubduedAndSingleLine(t *testing.T) { + m, _ := suggestionTestModel(t) + m.width, m.height = 80, 30 + m.monitorSuggestions.menu.Choices = []codex.SessionCommandChoice{{ID: "model", Label: "/model", Help: strings.Repeat("Long help\n", 40)}} + popup, _ := m.monitorSuggestionPopup() + colors := paletteFor(m.theme) + view := m.render() + line := strings.Split(ansi.Strip(view), "\n")[popup.y+1] + if !strings.Contains(line, "/model // Long help") || !strings.Contains(line, "…") || popup.height != 3 { + t.Fatal("long help did not stay on one ellipsized row", line) + } + cells := uv.NewScreenBuffer(m.width, m.height) + uv.NewStyledString(view).Draw(&cells, image.Rect(0, 0, m.width, m.height)) + command := cells.CellAt(popup.x+2, popup.y+1) + help := cells.CellAt(popup.x+2+len("/model // "), popup.y+1) + if reflect.DeepEqual(command.Style.Bg, help.Style.Bg) { + t.Fatal("help inherited the command highlight") + } + want := uv.NewScreenBuffer(1, 1) + uv.NewStyledString(colors.dimmed().Render("x")).Draw(&want, image.Rect(0, 0, 1, 1)) + if !reflect.DeepEqual(help.Style.Fg, want.CellAt(0, 0).Style.Fg) { + t.Fatal("help is not subdued") + } +} diff --git a/internal/ui/monitor_summary.go b/internal/ui/monitor_summary.go index fa6f142..bea93b4 100644 --- a/internal/ui/monitor_summary.go +++ b/internal/ui/monitor_summary.go @@ -212,9 +212,11 @@ func (m *Model) openMonitorAttention(action string) { review = "profile" } if m.monitorContextDetail == item.id && m.selectedAttentionAction() == action { + m.showLiveMonitorHistory() return } m.setRowContext(item.id, contextFull) + m.showLiveMonitorHistory() row := m.monitorContextRows[item.id] row.review = review m.monitorContextRows[item.id] = row @@ -232,7 +234,27 @@ func (m *Model) monitorAttentionButtons(width, maxRows int) ([]monitorNavigation // Choose the least compressed presentation that fits the entire list. // A resize that makes everything fit also returns to the first page. for compact := 0; compact < 4; compact++ { - buttons := m.layoutMonitorAttention(sessions, width, maxRows, compact, false) + // After dropping separators, cap the longest names progressively before + // removing names altogether. Binary search avoids per-cell work on wide + // terminals. Six cells retain a useful excerpt plus the ellipsis. + if compact == 2 { + var best []monitorNavigationButton + for low, high := 6, width; low <= high; { + limit := low + (high-low)/2 + candidate := m.layoutMonitorAttention(sessions, width, maxRows, 1, limit, false) + if len(candidate) == len(sessions) { + best = candidate + low = limit + 1 + } else { + high = limit - 1 + } + } + if len(best) > 0 { + m.markSelectedAttention(best) + return best, best[len(best)-1].rect.y + 1 + } + } + buttons := m.layoutMonitorAttention(sessions, width, maxRows, compact, 0, false) if len(buttons) == len(sessions) { m.markSelectedAttention(buttons) return buttons, buttons[len(buttons)-1].rect.y + 1 @@ -244,7 +266,7 @@ func (m *Model) monitorAttentionButtons(width, maxRows int) ([]monitorNavigation } reserve := lipgloss.Width(fmt.Sprintf("[+%d →]", len(sessions))) + 1 limit := max(width-reserve, 1) - buttons := m.layoutMonitorAttention(sessions[start:], limit, maxRows, 3, true) + buttons := m.layoutMonitorAttention(sessions[start:], limit, maxRows, 3, 0, true) if len(buttons) == 0 { return nil, 0 } @@ -282,7 +304,7 @@ func (m *Model) markSelectedAttention(buttons []monitorNavigationButton) { } } -func (m *Model) layoutMonitorAttention(sessions []monitorAttentionItem, width, rows, compact int, truncate bool) []monitorNavigationButton { +func (m *Model) layoutMonitorAttention(sessions []monitorAttentionItem, width, rows, compact, nameLimit int, truncate bool) []monitorNavigationButton { x, y := 0, 0 markerWidth := 0 if m.monitorContextDetail != "" { @@ -319,6 +341,9 @@ func (m *Model) layoutMonitorAttention(sessions []monitorAttentionItem, width, r name = terminalLabel(s.name) } if compact < 2 && name != "" { + if nameLimit > 0 { + name = ansi.Truncate(name, nameLimit, "…") + } separator := " // " if compact == 1 { separator = " " diff --git a/internal/ui/monitor_summary_test.go b/internal/ui/monitor_summary_test.go index 2d5a633..8d23c55 100644 --- a/internal/ui/monitor_summary_test.go +++ b/internal/ui/monitor_summary_test.go @@ -587,6 +587,55 @@ func TestMonitorAttentionProgressiveCompression(t *testing.T) { } } +func TestMonitorAttentionShortensLongestNamesBeforeDroppingThem(t *testing.T) { + for _, detail := range []bool{false, true} { + for _, longName := range []string{strings.Repeat("long-session-name-", 12), strings.Repeat("日本語セッション", 20)} { + m := attentionTestModel() + m.monitorSessionData = []monitorSession{ + {id: "session-AAA01", name: longName, displayed: true, attention: codex.SessionAttentionComplete}, + {id: "session-BBB02", workingDirectory: "/work/project", displayed: true, attention: codex.SessionAttentionComplete}, + {id: "session-CCC03", name: "docs", displayed: true, attention: codex.SessionAttentionComplete}, + {id: "session-DDD04", name: "tests", displayed: true, attention: codex.SessionAttentionComplete}, + } + if detail { + m.monitorContextDetail = "session-AAA01" + } + for _, width := range []int{150, 180, 220} { + buttons, rows := m.monitorAttentionButtons(width, 1) + if len(buttons) != 4 || rows != 1 { + t.Fatalf("width %d: missing pills: %+v", width, buttons) + } + if !strings.Contains(buttons[0].label, "…") { + t.Fatal("long name not abbreviated", buttons[0].label) + } + for i, name := range []string{"project", "docs", "tests"} { + if !strings.Contains(buttons[i+1].label, name) { + t.Fatalf("short name %q unnecessarily removed: %+v", name, buttons) + } + } + last := buttons[len(buttons)-1].rect + if last.x+last.width > width || width-last.x-last.width > 2 { + t.Fatalf("unused/overflowing space at width %d: %+v", width, buttons) + } + for _, b := range buttons { + if lipgloss.Width(b.label) != b.rect.width { + t.Fatal("Unicode label width drifted") + } + for x := b.rect.x; x < b.rect.x+b.rect.width; x++ { + if monitorNavigationButtonsHit(buttons, x, b.rect.y) != b.action { + t.Fatal("shortened pill click target drifted") + } + } + } + } + buttons, _ := m.monitorAttentionButtons(1000, 1) + if !strings.Contains(buttons[0].label, " // "+longName) { + t.Fatal("expansion did not restore original name") + } + } + } +} + func TestMonitorSelectedPillMarkersPreserveLayout(t *testing.T) { for _, width := range []int{24, 40, 80, 240} { m := attentionTestModel() diff --git a/internal/ui/monitor_turn_test.go b/internal/ui/monitor_turn_test.go index 3411918..91fbef3 100644 --- a/internal/ui/monitor_turn_test.go +++ b/internal/ui/monitor_turn_test.go @@ -216,8 +216,16 @@ func TestWorkingComposerErrorAndScheduleIsolation(t *testing.T) { t.Fatal("failed queue lost draft or hid error") } view := m.renderMonitorPrompt(120, 30, paletteFor(themeHacker)) - if !strings.Contains(view, monitorDotWave(m.phase)) || !strings.Contains(view, "WORKING") { - t.Fatal("missing progress above composer") + if !strings.Contains(view, monitorDotWave(m.phase)) || strings.Contains(view, "FOLLOW-UP") || strings.Contains(view, "WORKING") || len(strings.Split(view, "\n")) != 3 { + t.Fatal("composer must retain progress above the input without a redundant heading") + } + if ansi.Strip(strings.Split(view, "\n")[0]) != monitorDotWave(m.phase) { + t.Fatal("working composer heading must contain only activity dots") + } + m.monitorSessionData[0].working = false + view = m.renderMonitorPrompt(120, 30, paletteFor(themeHacker)) + if strings.Contains(view, monitorDotWave(m.phase)) || m.monitorPromptHeaderRows() != 0 || len(strings.Split(view, "\n")) != 2 { + t.Fatal("inactive session retained a progress row") } } diff --git a/internal/ui/preferences.go b/internal/ui/preferences.go index 73a5ec0..eaaefd5 100644 --- a/internal/ui/preferences.go +++ b/internal/ui/preferences.go @@ -3,6 +3,7 @@ package ui import ( "encoding/json" "errors" + "github.com/merefield/codexometer/internal/statusline" "os" "path/filepath" "time" @@ -12,12 +13,13 @@ import ( // Operational, account, quota-estimation, session, and token-event data are // never stored. type Preferences struct { - HideSessionContext bool `json:"hideSessionContext,omitempty"` - Theme string `json:"theme,omitempty"` - MainTab string `json:"mainTab,omitempty"` - QuotaView string `json:"quotaView,omitempty"` - BenchmarkFilter string `json:"benchmarkFilter,omitempty"` - BenchmarkRank string `json:"benchmarkRank,omitempty"` + HideSessionContext bool `json:"hideSessionContext,omitempty"` + Theme string `json:"theme,omitempty"` + MainTab string `json:"mainTab,omitempty"` + QuotaView string `json:"quotaView,omitempty"` + BenchmarkFilter string `json:"benchmarkFilter,omitempty"` + BenchmarkRank string `json:"benchmarkRank,omitempty"` + StatusLine []string `json:"statusLine"` } type PreferenceStore interface { @@ -77,6 +79,9 @@ func NewWithPreferences(fetcher Fetcher, refreshEvery time.Duration, store Prefe } func (m *Model) applyPreferences(preferences Preferences) { + if preferences.StatusLine != nil { + m.monitorStatusLine = statusline.Normalize(preferences.StatusLine) + } m.monitorContextHidden = preferences.HideSessionContext if theme, ok := themePreferenceIDs[preferences.Theme]; ok { m.theme = theme @@ -116,6 +121,7 @@ func (m Model) persistPreferences() { QuotaView: quotaViewPreferenceNames[m.selectedQuotaView()], BenchmarkFilter: benchmarkFilterPreferenceNames[m.benchmarkFilter], BenchmarkRank: benchmarkRankPreferenceNames[m.benchmarkRankMode], + StatusLine: m.monitorStatusLine, }) } diff --git a/internal/ui/view.go b/internal/ui/view.go index 70a607d..cf91c34 100644 --- a/internal/ui/view.go +++ b/internal/ui/view.go @@ -76,6 +76,9 @@ func (m Model) render() string { } else if len(meters) == 1 { parts = append(parts, renderMeterArea(contentWidth, layout.meterHeight, meters[0], m.meterView, colors)) } + if layout.footerSpacer { + parts = append(parts, strings.Repeat(" ", contentWidth)) + } parts = append(parts, footer) } @@ -85,7 +88,7 @@ func (m Model) render() string { if subtitleRow < len(rendered) { rendered[subtitleRow] = linkHeaderVersion(rendered[subtitleRow], m.appVersion, m.versionHovered, colors) } - return strings.Join(rendered, "\n") + return m.withMonitorSuggestions(strings.Join(rendered, "\n"), colors) } func renderHeader(width, phase int, signal, account, appVersion string, colors palette) string { diff --git a/internal/web/control.go b/internal/web/control.go index b50e6d7..edb052a 100644 --- a/internal/web/control.go +++ b/internal/web/control.go @@ -23,6 +23,7 @@ import ( // binds the browser's selection to the exact source request without disclosing // Codex tokens. One pending confirmation bounds memory (including secret input). type control struct { + commands codex.SessionCommandsClient schedules *schedule.Queue profiles *profileControl mu sync.Mutex @@ -71,16 +72,17 @@ type offeredAction struct { } type actionRequest struct { - EditID string `json:"editId,omitempty"` - SendID string `json:"sendId,omitempty"` - Schedule *scheduleRule `json:"schedule,omitempty"` - CancelID string `json:"cancelId,omitempty"` - Review string `json:"review,omitempty"` - Session string `json:"session"` - Offer string `json:"offer"` - Choice *int `json:"choice,omitempty"` - Answers []string `json:"answers,omitempty"` - Confirmation string `json:"confirmation,omitempty"` + Command *commandRequest `json:"command,omitempty"` + EditID string `json:"editId,omitempty"` + SendID string `json:"sendId,omitempty"` + Schedule *scheduleRule `json:"schedule,omitempty"` + CancelID string `json:"cancelId,omitempty"` + Review string `json:"review,omitempty"` + Session string `json:"session"` + Offer string `json:"offer"` + Choice *int `json:"choice,omitempty"` + Answers []string `json:"answers,omitempty"` + Confirmation string `json:"confirmation,omitempty"` } type preparedAction struct { @@ -93,6 +95,7 @@ func newControl(source Source, store *store) *control { c := &control{store: store, key: rand.Text(), schedules: schedule.New()} c.approvals, _ = source.(codex.SessionApprovalClient) c.prompts, _ = source.(codex.SessionPromptClient) + c.commands, _ = source.(codex.SessionCommandsClient) c.profiles = newProfileControl(source) return c } @@ -220,6 +223,14 @@ func (c *control) handle(action, origin string) http.HandlerFunc { http.Error(w, "Invalid request", 400) return } + if action == "commands" { + c.handleCommands(w, r, body) + return + } + if body.Command != nil { + http.Error(w, "Invalid request", 400) + return + } if action == "schedules" { c.handleSchedules(w, r, body) return @@ -231,7 +242,7 @@ func (c *control) handle(action, origin string) http.HandlerFunc { // Consume before any IO, even on failure. Never retry ambiguous writes. if action == "commit" { p := c.pending - if p == nil || p.id != body.Confirmation || p.request.Session != body.Session || p.request.Offer != body.Offer || p.request.Review != body.Review { + if p == nil || p.request.Command != nil || p.id != body.Confirmation || p.request.Session != body.Session || p.request.Offer != body.Offer || p.request.Review != body.Review { c.mu.Unlock() http.Error(w, errUnavailable.Error(), 409) return @@ -282,6 +293,10 @@ func (c *control) handle(action, origin string) http.HandlerFunc { http.Error(w, errUnavailable.Error(), 409) return } + if o.Kind == "prompt" && len(o.Questions) == 0 && len(body.Answers) == 1 && codex.IsSessionCommand(body.Answers[0]) { + http.Error(w, "Use the command browser; use // for literal slash text", 400) + return + } if action == "prepare" { p := &preparedAction{request: body, id: rand.Text(), until: time.Now().Add(30 * time.Second)} c.mu.Lock() diff --git a/internal/web/control_commands.go b/internal/web/control_commands.go new file mode 100644 index 0000000..cdc5df5 --- /dev/null +++ b/internal/web/control_commands.go @@ -0,0 +1,78 @@ +package web + +import ( + "context" + "crypto/rand" + "encoding/json" + "net/http" + "time" +) + +type commandRequest struct { + Mode string `json:"mode"` + Path string `json:"path"` + Revision string `json:"revision,omitempty"` + Choice string `json:"choice,omitempty"` +} + +func (c *control) handleCommands(w http.ResponseWriter, r *http.Request, body actionRequest) { + q := body.Command + if c.commands == nil || q == nil || body.Review != "" || body.Offer != "" || body.Choice != nil || len(body.Answers) != 0 || body.Schedule != nil || body.EditID != "" || body.SendID != "" || body.CancelID != "" || len(q.Path) > 2048 || len(q.Revision) > 128 || len(q.Choice) > 128 || (q.Mode != "list" && q.Mode != "prepare" && q.Mode != "commit") { + http.Error(w, "Commands unavailable", 400) + return + } + c.store.mu.Lock() + _, exists := c.store.contexts[body.Session] + age := time.Since(c.store.state.SessionsAt) + fresh := !c.store.state.SessionsError && age >= 0 && age < 10*time.Second + c.store.mu.Unlock() + if !exists || !fresh { + http.Error(w, errUnavailable.Error(), 409) + return + } + ctx, cancel := context.WithTimeout(r.Context(), 12*time.Second) + defer cancel() + w.Header().Set("Content-Type", "application/json") + if q.Mode == "commit" { + c.mu.Lock() + p := c.pending + if p == nil || p.request.Command == nil || p.id != body.Confirmation || p.request.Session != body.Session || !time.Now().Before(p.until) { + c.mu.Unlock() + http.Error(w, errUnavailable.Error(), 409) + return + } + c.pending = nil + c.mu.Unlock() + q = p.request.Command + if err := c.commands.ExecuteSessionCommand(ctx, body.Session, q.Path, q.Revision, q.Choice); err != nil { + http.Error(w, "Command unconfirmed; check Codex before retrying", 409) + return + } + _ = json.NewEncoder(w).Encode(map[string]string{"message": "Change requested. Codex will apply it to subsequent turns."}) + return + } + m, err := c.commands.SessionCommands(ctx, body.Session, q.Path) + if err != nil { + http.Error(w, "Command catalogue unavailable; use Codex", 409) + return + } + if q.Mode == "list" { + _ = json.NewEncoder(w).Encode(m) + return + } + if m.Revision != q.Revision { + http.Error(w, errUnavailable.Error(), 409) + return + } + for _, o := range m.Choices { + if o.ID == q.Choice && o.Action { + p := &preparedAction{request: body, id: rand.Text(), until: time.Now().Add(30 * time.Second)} + c.mu.Lock() + c.pending = p + c.mu.Unlock() + _ = json.NewEncoder(w).Encode(map[string]any{"confirmation": p.id, "expires": p.until}) + return + } + } + http.Error(w, errUnavailable.Error(), 409) +} diff --git a/internal/web/control_commands_test.go b/internal/web/control_commands_test.go new file mode 100644 index 0000000..f9a507b --- /dev/null +++ b/internal/web/control_commands_test.go @@ -0,0 +1,80 @@ +package web + +import ( + "context" + "encoding/json" + "testing" + "time" + + "github.com/merefield/codexometer/internal/codex" +) + +type commandsFake struct { + calls int + revision string +} + +func (f *commandsFake) SessionCommands(context.Context, string, string) (codex.SessionCommandMenu, error) { + return codex.SessionCommandMenu{Revision: f.revision, Choices: []codex.SessionCommandChoice{{ID: "choice", Label: "Medium", Help: "Dynamic help", Action: true}}}, nil +} +func (f *commandsFake) ExecuteSessionCommand(_ context.Context, id, path, revision, choice string) error { + if id != "parent" || revision != f.revision || choice != "choice" { + return codex.ErrSessionCommand + } + f.calls++ + return nil +} +func TestCommandConfirmationIsBoundAndSingleUse(t *testing.T) { + s, _, token := controlServer(t) + f := &commandsFake{revision: "current"} + s.control.commands = f + b := actionRequest{Session: "parent", Command: &commandRequest{Mode: "prepare", Path: "model/test", Revision: "current", Choice: "choice"}} + w := actionCall(s, token, "commands", b) + var response struct{ Confirmation string } + if w.Code != 200 || json.Unmarshal(w.Body.Bytes(), &response) != nil || response.Confirmation == "" { + t.Fatalf("prepare %d %s", w.Code, w.Body) + } + if f.calls != 0 { + t.Fatal("prepare mutated settings") + } + wrong := actionRequest{Session: "parent", Confirmation: response.Confirmation} + if w = actionCall(s, token, "commit", wrong); w.Code != 409 { + t.Fatalf("normal route accepted command: %d", w.Code) + } + b.Confirmation = response.Confirmation + b.Command = &commandRequest{Mode: "commit"} + if w = actionCall(s, token, "commands", b); w.Code != 200 || f.calls != 1 { + t.Fatalf("commit %d %s calls %d", w.Code, w.Body, f.calls) + } + if w = actionCall(s, token, "commands", b); w.Code != 409 || f.calls != 1 { + t.Fatal("replayed command") + } +} + +func TestCommandsRejectStaleAndMixedRequests(t *testing.T) { + s, _, token := controlServer(t) + f := &commandsFake{revision: "new"} + s.control.commands = f + b := actionRequest{Session: "parent", Command: &commandRequest{Mode: "prepare", Revision: "old", Choice: "choice"}} + if w := actionCall(s, token, "commands", b); w.Code != 409 { + t.Fatal("stale options accepted") + } + b.Command.Mode = "list" + b.Answers = []string{"prompt"} + if w := actionCall(s, token, "commands", b); w.Code != 400 { + t.Fatal("mixed payload accepted") + } + if w := actionCall(s, token, "schedules", b); w.Code != 400 { + t.Fatal("commands accepted on schedule route") + } + b.Answers = nil + s.store.mu.Lock() + s.store.state.SessionsAt = time.Now().Add(-time.Minute) + s.store.mu.Unlock() + if w := actionCall(s, token, "commands", b); w.Code != 409 { + t.Fatal("stale sessions accepted") + } + if f.calls != 0 { + t.Fatal("invalid request mutated settings") + } +} diff --git a/internal/web/dist/assets/index-BdtIe-Dj.js b/internal/web/dist/assets/index-BdtIe-Dj.js deleted file mode 100644 index 12ba842..0000000 --- a/internal/web/dist/assets/index-BdtIe-Dj.js +++ /dev/null @@ -1,42 +0,0 @@ -(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),t.credentials=e.crossOrigin===`use-credentials`?`include`:e.crossOrigin===`anonymous`?`omit`:`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var e=Array.isArray,t=Array.prototype.indexOf,n=Array.prototype.includes,r=Array.from,i=Object.defineProperty,a=Object.getOwnPropertyDescriptor,o=Object.getOwnPropertyDescriptors,s=Object.prototype,c=Array.prototype,l=Object.getPrototypeOf,u=Object.isExtensible;function d(e){return typeof e==`function`}var f=()=>{};function p(e){return e()}function m(e){for(var t=0;t{e=n,t=r}),resolve:e,reject:t}}function g(e,t,n=!1){return e===void 0?n?t():t:e}function _(e,t){if(Array.isArray(e))return e;if(t===void 0||!(Symbol.iterator in e))return Array.from(e);let n=[];for(let r of e)if(n.push(r),n.length===t)break;return n}var v=1024,y=2048,b=4096,x=8192,S=16384,C=32768,w=1<<25,T=65536,ee=1<<19,te=1<<20,E=1<<25,D=1<<21,ne=1<<22,re=1<<23,ie=Symbol(`$state`),ae=Symbol(`component`),oe=Symbol(`legacy props`),se=Symbol(``),ce=Symbol(`attributes`),le=Symbol(`class`),ue=Symbol(`style`),de=Symbol(`text`),fe=Symbol(`form reset`),pe=new class extends Error{name=`StaleReactionError`;message="The reaction that called `getAbortSignal()` was re-run or destroyed"},me=!!globalThis.document?.contentType&&globalThis.document.contentType.includes(`xml`),he={},O=Symbol(`uninitialized`),ge=`http://www.w3.org/1999/xhtml`;function _e(){console.warn(`https://svelte.dev/e/derived_inert`)}function ve(e){console.warn(`https://svelte.dev/e/hydration_mismatch`)}function ye(){console.warn(`https://svelte.dev/e/select_multiple_invalid_value`)}function be(){console.warn(`https://svelte.dev/e/svelte_boundary_reset_noop`)}var k=!1;function xe(e){k=e}var A;function Se(e){if(e===null)throw ve(),he;return A=e}function Ce(){return Se(cn(A))}function j(e){if(k){if(cn(A)!==null)throw ve(),he;A=e}}function we(e=1){if(k){for(var t=e,n=A;t--;)n=cn(n);A=n}}function Te(e=!0){for(var t=0,n=A;;){if(n.nodeType===8){var r=n.data;if(r===`]`){if(t===0)return n;--t}else(r===`[`||r===`[!`||r[0]===`[`&&!isNaN(Number(r.slice(1))))&&(t+=1)}var i=cn(n);e&&n.remove(),n=i}}function Ee(e){if(!e||e.nodeType!==8)throw ve(),he;return e.data}function De(e){return e===this.v}function Oe(e,t){return e==e?e!==t||typeof e==`object`&&!!e||typeof e==`function`:t==t}function ke(e){return!Oe(e,this.v)}function Ae(e){throw Error(`https://svelte.dev/e/lifecycle_outside_component`)}function je(){throw Error(`https://svelte.dev/e/async_derived_orphan`)}function Me(e,t,n){throw Error(`https://svelte.dev/e/each_key_duplicate`)}function Ne(e){throw Error(`https://svelte.dev/e/effect_in_teardown`)}function Pe(){throw Error(`https://svelte.dev/e/effect_in_unowned_derived`)}function Fe(e){throw Error(`https://svelte.dev/e/effect_orphan`)}function Ie(){throw Error(`https://svelte.dev/e/effect_update_depth_exceeded`)}function Le(e){throw Error(`https://svelte.dev/e/props_invalid_value`)}function Re(){throw Error(`https://svelte.dev/e/state_descriptors_fixed`)}function ze(){throw Error(`https://svelte.dev/e/state_prototype_fixed`)}function Be(){throw Error(`https://svelte.dev/e/state_unsafe_mutation`)}function Ve(){throw Error(`https://svelte.dev/e/svelte_boundary_reset_onerror`)}var He=!1;function Ue(){He=!0}var M=null;function We(e){M=e}function Ge(e,t=!1,n){M={p:M,i:!1,c:null,e:null,s:e,x:null,r:U,l:He&&!t?{s:null,u:null,$:[]}:null}}function Ke(e){var t=M,n=t.e;if(n!==null){t.e=null;for(var r of n)xn(r)}return e!==void 0&&(t.x=e),t.i=!0,M=t.p,qe(e)}function qe(e={}){return i(e,ae,{value:!0}),e}function Je(){return!He||M!==null&&M.l===null}var Ye=[];function Xe(){var e=Ye;Ye=[],m(e)}function Ze(e){if(Ye.length===0&&!Tt){var t=Ye;queueMicrotask(()=>{t===Ye&&Xe()})}Ye.push(e)}function Qe(){for(;Ye.length>0;)Xe()}var $e=~(y|b|v);function et(e,t){e.f=e.f&$e|t}function tt(e){e.f&512||e.deps===null?et(e,v):et(e,b)}function nt(e,t,n){e.f&2048?t.add(e):e.f&4096&&n.add(e),et(e,v)}function rt(e){k&&sn(e)!==null&&ln(e)}var it=!1;function at(){it||(it=!0,document.addEventListener(`reset`,e=>{Promise.resolve().then(()=>{if(!e.defaultPrevented)for(let t of e.target.elements)t[fe]?.()})},{capture:!0}))}function ot(e){var t=H,n=U;Gn(null),Kn(null);try{return e()}finally{Gn(t),Kn(n)}}function st(e,t,n,r=n){e.addEventListener(t,()=>ot(n));let i=e[fe];e[fe]=i?()=>{i(),r(!0)}:()=>r(!0),at()}function ct(e,t,n,r){let i=Je()?ft:ht;var a=e.filter(e=>!e.settled),o=t.map(i);if(n.length===0&&a.length===0){r(o);return}var s=U,c=lt(),l=a.length===1?a[0].promise:a.length>1?Promise.all(a.map(e=>e.promise)):null;function u(e){if(!(s.f&16384)){c();try{r([...o,...e])}catch(e){mn(e,s)}ut()}}var d=dt();if(n.length===0){l.then(()=>u([])).finally(d);return}function f(){Promise.all(n.map(e=>mt(e))).then(u).catch(e=>mn(e,s)).finally(d)}l?l.then(()=>{c(),f(),ut()}):f()}function lt(){var e=U,t=H,n=M,r=P;return function(i=!0){Kn(e),Gn(t),We(n),i&&!(e.f&16384)&&(r?.activate(),r?.apply())}}function ut(e=!0){Kn(null),Gn(null),We(null),e&&P?.deactivate()}function dt(){var e=U,t=e.b,n=P,r=!!t?.is_rendered();return t?.update_pending_count(1,n),n.increment(r,e),()=>{t?.update_pending_count(-1,n),n.decrement(r,e)}}function ft(e){var t=2|y;return U!==null&&(U.f|=ee),{ctx:M,deps:null,effects:null,equals:De,f:t,fn:e,reactions:null,rv:0,v:O,wv:0,parent:U,ac:null}}var pt=Symbol(`obsolete`);function mt(e,t,n){let r=U;r===null&&je();var i=void 0,a=Ht(O),o=!H,s=new Set;return Tn(()=>{var t=U,n=h();i=n.promise;try{Promise.resolve(e()).then(n.resolve,e=>{e!==pe&&n.reject(e)}).finally(ut)}catch(e){n.reject(e),ut()}var c=P;if(o){if(t.f&32768)var l=dt();if(r.b?.is_rendered())c.async_deriveds.get(t)?.reject(pt);else for(let e of s.values())e.reject(pt);s.add(n),c.async_deriveds.set(t,n)}let u=(e,t=void 0)=>{l?.(),s.delete(n),t!==pt&&(c.activate(),t?(a.f|=re,Kt(a,t)):(a.f&8388608&&(a.f^=re),Kt(a,e)),c.deactivate())};n.promise.then(u,e=>u(null,e||`unknown`))}),yn(()=>{for(let e of s)e.reject(pt)}),new Promise(e=>{function t(n){function r(){n===i?e(a):t(i)}n.then(r,r)}t(i)})}function N(e){let t=ft(e);return Jn(t),t}function ht(e){let t=ft(e);return t.equals=ke,t}function gt(e){var t=e.effects;if(t!==null){e.effects=null;for(var n=0;n{t.ac.abort(pe),t.ac=null}),t.fn!==null&&(t.teardown=f),lr(t,0),An(t))}function bt(e){if(e.effects!==null)for(let t of e.effects)t.teardown&&t.fn!==null&&ur(t)}var xt=null,P=null,St=null,Ct=null,wt=null,Tt=!1,Et=!1,Dt=null,Ot=null,kt=0,At=1,jt=class e{id=At++;#e=!1;linked=!0;#t=null;#n=null;async_deriveds=new Map;current=new Map;previous=new Map;#r=new Set;#i=new Set;#a=0;#o=new Map;#s=null;#c=[];#l=[];#u=new Set;#d=new Set;#f=new Map;#p=new Set;is_fork=!1;#m=!1;constructor(){xt===null?xt=this:(xt.#n=this,this.#t=xt),xt=this}#h(){if(this.is_fork)return!0;for(let n of this.#o.keys()){for(var e=n,t=!1;e.parent!==null;){if(this.#f.has(e)){t=!0;break}e=e.parent}if(!t)return!0}return!1}skip_effect(e){this.#f.has(e)||this.#f.set(e,{d:[],m:[]}),this.#p.delete(e)}unskip_effect(e,t=e=>this.schedule(e)){var n=this.#f.get(e);if(n){this.#f.delete(e);for(var r of n.d)et(r,y),t(r);for(r of n.m)et(r,b),t(r)}this.#p.add(e)}#g(){var e=[];for(let i of this.#c)if(!(i.f&16384||!(i.f&6144))){for(var t=i,n=!1;t.parent!==null;){t=t.parent;var r=t.f;if(r&96){if(!(r&1024)){n=!0;break}t.f^=v}}n||e.push(t)}return this.#c=[],e}#_(){this.#e=!0;for(let e of this.#u)this.#d.delete(e),et(e,y),this.schedule(e);for(let e of this.#d)et(e,b),this.schedule(e);this.apply();for(var t=Dt=[],n=[],r=Ot=[];this.#c.length>0;){kt++>1e3&&(this.#S(),Nt());for(let e of this.#g())try{this.#v(e,t,n)}catch(t){throw Rt(e),this.#h()||this.discard(),t}}if(P=null,r.length>0){var i=e.ensure();for(let e of r)i.schedule(e)}if(Dt=null,Ot=null,this.#h()){this.#x(n),this.#x(t);for(let[e,t]of this.#f)Lt(e,t);r.length>0&&P.#_();return}let a=this.#y();if(a){this.#x(n),this.#x(t),a.#b(this);return}this.#u.clear(),this.#d.clear();for(let e of this.#r)e(this);this.#r.clear(),St=this,Ft(n),Ft(t),St=null,this.#s?.resolve();var o=P;if(this.#a===0&&(this.#c.length===0||o!==null)&&this.#S(),this.#c.length>0){if(o!==null){for(let e of this.#c)o.#c.push(e);this.#c=[]}else o=this}o!==null&&(Bt.clear(),o.#_())}#v(e,t,n){e.f^=v;for(var r=e.first;r!==null;){var i=r.f,a=!!(i&96);if(!(a&&i&1024||i&8192||this.#f.has(r))&&r.fn!==null){a?r.f^=v:i&4?t.push(r):ir(r)&&(i&16&&this.#d.add(r),ur(r));var o=r.first;if(o!==null){r=o;continue}}for(;r!==null;){var s=r.next;if(s!==null){r=s;break}r=r.parent}}}#y(){for(var e=this.#t;e!==null;){if(!e.is_fork){for(let[t,[,n]]of this.current)if(e.current.has(t)&&!n)return e}e=e.#t}return null}#b(e){for(let[t,n]of e.current)!this.previous.has(t)&&e.previous.has(t)&&this.previous.set(t,e.previous.get(t)),this.current.set(t,n);for(let[t,n]of e.async_deriveds){let e=this.async_deriveds.get(t);e&&n.promise.then(e.resolve).catch(e.reject)}e.async_deriveds.clear(),this.transfer_effects(e.#u,e.#d);let t=e=>{var n=e.reactions;if(n!==null&&!(e.f&2&&!(e.f&6144)))for(let e of n){var r=e.f;if(r&2)t(e);else{var i=e;r&4194320&&!this.async_deriveds.has(i)&&(this.#d.delete(i),et(i,y),this.schedule(i))}}};for(let e of this.current.keys())t(e);this.oncommit(()=>e.discard()),e.#S(),P=this,this.#_()}#x(e){for(var t=0;t{this.#m=!1,this.linked&&this.flush()}))}transfer_effects(e,t){for(let t of e)this.#u.add(t);for(let e of t)this.#d.add(e);e.clear(),t.clear()}oncommit(e){this.#r.add(e)}ondiscard(e){this.#i.add(e)}settled(){return(this.#s??=h()).promise}static ensure(){if(P===null){let t=P=new e;!Et&&!Tt&&Ze(()=>{t.#e||t.flush()})}return P}apply(){Ct=null}schedule(e){if(wt=e,e.b?.is_pending&&e.f&16777228&&!(e.f&32768)){e.b.defer_effect(e);return}this.#c.push(e)}#S(){if(this.linked){var e=this.#t,t=this.#n;e===null||(e.#n=t),t===null?xt=e:t.#t=e,this.linked=!1}}};function Mt(e){var t=Tt;Tt=!0;try{var n;for(e&&(P!==null&&!P.is_fork&&P.flush(),n=e());;){if(Qe(),P===null)return n;P.flush()}}finally{Tt=t}}function Nt(){try{Ie()}catch(e){mn(e,wt)}}var Pt=null;function Ft(e){var t=e.length;if(t!==0){for(var n=0;n0)){Bt.clear();for(let e of Pt){if(e.f&24576)continue;let t=[e],n=e.parent;for(;n!==null;)Pt.has(n)&&(Pt.delete(n),t.push(n)),n=n.parent;for(let e=t.length-1;e>=0;e--){let n=t[e];n.f&24576||ur(n)}}Pt.clear()}}Pt=null}}function It(e){P.schedule(e)}function Lt(e,t){if(!(e.f&32&&e.f&1024)){e.f&2048?t.d.push(e):e.f&4096&&t.m.push(e),et(e,v);for(var n=e.first;n!==null;)Lt(n,t),n=n.next}}function Rt(e){et(e,v);for(var t=e.first;t!==null;)Rt(t),t=t.next}var zt=new Set,Bt=new Map,Vt=!1;function Ht(e,t){return{f:0,v:e,reactions:null,equals:De,rv:0,wv:0}}function F(e,t){let n=Ht(e,t);return Jn(n),n}function Ut(e,t=!1,n=!0){let r=Ht(e);return t||(r.equals=ke),He&&n&&M!==null&&M.l!==null&&(M.l.s??=[]).push(r),r}function I(e,t,n=!1){return H!==null&&(!Wn||H.f&131072)&&Je()&&H.f&4325394&&(qn===null||!qn.has(e))&&Be(),Kt(e,n?Zt(t):t,Ot)}var Wt=null,Gt=0;function Kt(e,t,n=null){if(!e.equals(t)){Hn?Bt.set(e,t):Bt.has(e)||Bt.set(e,e.v);var r=jt.ensure();if(r.capture(e,t),e.f&2){let t=e;e.f&2048&&_t(t),Ct===null&&tt(t)}e.wv=rr(),Wt=null,Gt=0,Xt(e,y,n),Wt=null,Je()&&U!==null&&U.f&1024&&!(U.f&96)&&(Zn===null?Qn([e]):Zn.push(e)),!r.is_fork&&zt.size>0&&!Vt&&qt()}return t}function qt(){Vt=!1;for(let e of zt){e.f&1024&&et(e,b);let t;try{t=ir(e)}catch{t=!0}t&&ur(e)}zt.clear()}function Jt(e,t=1){var n=W(e),r=t===1?n++:n--;return I(e,n),r}function Yt(e){I(e,e.v+1)}function Xt(e,t,n){var r=e.reactions;if(r!==null){var i=Je(),a=r.length;if(Gt+=a,Gt>1e5&&Wt===null&&(Wt=new Set),Wt!==null){if(Wt.has(e))return;Wt.add(e)}for(var o=0;o{if(tr===d)return e();var t=H,n=tr;Gn(null),nr(d);var r=e();return Gn(t),nr(n),r};return i&&r.set(`length`,F(t.length,u)),new Proxy(t,{defineProperty(e,t,n){(!(`value`in n)||n.configurable===!1||n.enumerable===!1||n.writable===!1)&&Re();var i=r.get(t);return i===void 0?f(()=>{var e=F(n.value,u);return r.set(t,e),e}):I(i,n.value,!0),!0},deleteProperty(e,t){var n=r.get(t);if(n===void 0){if(t in e){let e=f(()=>F(O,u));r.set(t,e),Yt(o)}}else I(n,O),Yt(o);return!0},get(e,n,i){if(n===ie)return t;var o=r.get(n),s=n in e;if(o===void 0&&(!s||a(e,n)?.writable)&&(o=f(()=>F(Zt(s?e[n]:O),u)),r.set(n,o)),o!==void 0){var c=W(o);return c===O?void 0:c}return Reflect.get(e,n,i)},getOwnPropertyDescriptor(e,t){this.has?.(e,t);var n=Reflect.getOwnPropertyDescriptor(e,t),i=r.get(t);if(i!==void 0){var a=W(i);if(a===O)return;if(n&&`value`in n)n.value=a;else return{enumerable:!0,configurable:!0,value:a,writable:!0}}return n},has(e,t){if(t===ie)return!0;var n=r.get(t),i=n!==void 0&&n.v!==O||Reflect.has(e,t);return(n!==void 0||U!==null&&(!i||a(e,t)?.writable))&&(n===void 0&&(n=f(()=>F(i?Zt(e[t]):O,u)),r.set(t,n)),W(n)===O)?!1:i},set(e,t,n,s){var c=r.get(t),l=t in e;if(i&&t===`length`)for(var d=n;dF(O,u)),r.set(d+``,p)):I(p,O)}if(c===void 0)(!l||a(e,t)?.writable)&&(c=f(()=>F(void 0,u)),I(c,Zt(n)),r.set(t,c));else{l=c.v!==O;var m=f(()=>Zt(n));I(c,m)}var h=Reflect.getOwnPropertyDescriptor(e,t);if(h?.set&&h.set.call(s,n),!l){if(i&&typeof t==`string`){var g=r.get(`length`),_=Number(t);Number.isInteger(_)&&_>=g.v&&I(g,_+1)}Yt(o)}return!0},ownKeys(e){W(o);var t=Reflect.ownKeys(e).filter(e=>{var t=r.get(e);return t===void 0||t.v!==O});for(var[n,i]of r)i.v!==O&&!(n in e)&&t.push(n);return t},setPrototypeOf(){ze()}})}function Qt(e){try{if(typeof e==`object`&&e&&ie in e)return e[ie]}catch{}return e}function $t(e,t){return Object.is(Qt(e),Qt(t))}var en,tn,nn,rn;function an(){if(en===void 0){en=window,tn=/Firefox/.test(navigator.userAgent);var e=Element.prototype,t=Node.prototype,n=Text.prototype;nn=a(t,`firstChild`).get,rn=a(t,`nextSibling`).get,u(e)&&(e[le]=void 0,e[ce]=null,e[ue]=void 0,e.__e=void 0),u(n)&&(n[de]=void 0)}}function on(e=``){return document.createTextNode(e)}function sn(e){return nn.call(e)}function cn(e){return rn.call(e)}function L(e,t){if(!k)return sn(e);var n=sn(A);if(n===null)n=A.appendChild(on());else if(t&&n.nodeType!==3){var r=on();return n?.before(r),Se(r),r}return t&&fn(n),Se(n),n}function R(e,t=!1){if(!k){var n=sn(e);return n instanceof Comment&&n.data===``?cn(n):n}if(t){if(A?.nodeType!==3){var r=on();return A?.before(r),Se(r),r}fn(A)}return A}function z(e,t=!1){if(!k)return sn(e);var n=L(e,t);return j(e),n}function B(e,t=1,n=!1){let r=k?A:e;for(var i;t--;)i=r,r=cn(r);if(!k)return r;if(n){if(r?.nodeType!==3){var a=on();return r===null?i?.after(a):r.before(a),Se(a),a}fn(r)}return Se(r),r}function ln(e){e.textContent=``}function un(){return!1}function dn(e,t,n){return t==null||t===`http://www.w3.org/1999/xhtml`?n?document.createElement(e,{is:n}):document.createElement(e):n?document.createElementNS(t,e,{is:n}):document.createElementNS(t,e)}function fn(e){if(e.nodeValue.length<65536)return;let t=e.nextSibling;for(;t!==null&&t.nodeType===3;)t.remove(),e.nodeValue+=t.nodeValue,t=e.nextSibling}function pn(e){var t=U;if(t===null)return H.f|=re,e;if(!(t.f&32768)&&!(t.f&4))throw e;mn(e,t)}function mn(e,t){if(!(t!==null&&t.f&16384)){for(;t!==null;){if(t.f&128&&!(t.f&33570816)){if(!(t.f&32768))throw e;try{t.b.error(e);return}catch(t){e=t}}t=t.parent}throw e}}function hn(e){U===null&&(H===null&&Fe(e),Pe()),Hn&&Ne(e)}function gn(e,t){var n=t.last;n===null?t.last=t.first=e:(n.next=e,e.prev=n,t.last=e)}function _n(e,t){var n=U;n!==null&&n.f&8192&&(e|=x);var r={ctx:M,deps:null,nodes:null,f:e|y|512,first:null,fn:t,last:null,next:null,parent:n,b:n&&n.b,prev:null,teardown:null,wv:0,ac:null};P?.register_created_effect(r);var i=r;if(e&4)Dt===null?jt.ensure().schedule(r):Dt.push(r);else if(t!==null){try{ur(r)}catch(e){throw Mn(r),e}i.deps===null&&i.teardown===null&&i.nodes===null&&i.first===i.last&&!(i.f&524288)&&(i=i.first,e&16&&e&65536&&i!==null&&(i.f|=T))}if(i!==null&&(i.parent=n,n!==null&&gn(i,n),H!==null&&H.f&2&&!(e&64))){var a=H;(a.effects??=[]).push(i)}return r}function vn(){return H!==null&&!Wn}function yn(e){let t=_n(8,null);return et(t,v),t.teardown=e,t}function bn(e){hn(`$effect`);var t=U.f;if(!H&&t&32&&M!==null&&!M.i){var n=M;(n.e??=[]).push(e)}else return xn(e)}function xn(e){return _n(4|te,e)}function Sn(e){return hn(`$effect.pre`),_n(8|te,e)}function Cn(e){jt.ensure();let t=_n(64|ee,e);return(e={})=>new Promise(n=>{e.outro?Fn(t,()=>{Mn(t),n(void 0)}):(Mn(t),n(void 0))})}function wn(e){return _n(4,e)}function Tn(e){return _n(ne|ee,e)}function En(e,t=0){return _n(8|t,e)}function V(e,t=[],n=[],r=[]){ct(r,t,n,t=>{_n(8,()=>{e(...t.map(W))})})}function Dn(e,t=0){return _n(16|t,e)}function On(e){return _n(32|ee,e)}function kn(e){var t=e.teardown;if(t!==null){let n=Hn,r=H;Un(!0),Gn(null);try{t.call(null)}catch(t){mn(t,e.parent)}finally{Un(n),Gn(r)}}}function An(e,t=!1){var n=e.first;for(e.first=e.last=null;n!==null;){let e=n.ac;e!==null&&ot(()=>{e.abort(pe)});var r=n.next;n.f&64?n.parent=null:Mn(n,t),n=r}}function jn(e){for(var t=e.first;t!==null;){var n=t.next;t.f&32||Mn(t),t=n}}function Mn(e,t=!0){var n=!1;(t||e.f&262144)&&e.nodes!==null&&e.nodes.end!==null&&(Nn(e.nodes.start,e.nodes.end),n=!0),e.f|=w,An(e,t&&!n),lr(e,0);var r=e.nodes&&e.nodes.t;if(r!==null)for(let e of r)e.stop();kn(e),e.f^=w,e.f|=S;var i=e.parent;i!==null&&i.first!==null&&Pn(e),e.next=e.prev=e.teardown=e.ctx=e.deps=e.fn=e.nodes=e.ac=e.b=null}function Nn(e,t){for(;e!==null;){var n=e===t?null:cn(e);e.remove(),e=n}}function Pn(e){var t=e.parent,n=e.prev,r=e.next;n!==null&&(n.next=r),r!==null&&(r.prev=n),t!==null&&(t.first===e&&(t.first=r),t.last===e&&(t.last=n))}function Fn(e,t,n=!0){var r=[];e.f|=256,In(e,r,!0);var i=()=>{n&&Mn(e),t&&t()},a=r.length;if(a>0){var o=()=>--a||i();for(var s of r)s.out(o)}else i()}function In(e,t,n){if(!(e.f&8192)){e.f^=x;var r=e.nodes&&e.nodes.t;if(r!==null)for(let e of r)(e.is_global||n)&&t.push(e);for(var i=e.first;i!==null;){var a=i.next;if(!(i.f&64)){var o=!!(i.f&65536)||!!(i.f&32)&&!!(e.f&16);In(i,t,o?n:!1)}i=a}}}function Ln(e){e.f&=-257,Rn(e,!0)}function Rn(e,t){if(!(e.f&256)&&e.f&8192){e.f^=x,e.f&1024||(et(e,y),jt.ensure().schedule(e));for(var n=e.first;n!==null;){var r=n.next,i=!!(n.f&65536)||!!(n.f&32);Rn(n,i?t:!1),n=r}var a=e.nodes&&e.nodes.t;if(a!==null)for(let e of a)(e.is_global||t)&&e.in()}}function zn(e,t){if(e.nodes)for(var n=e.nodes.start,r=e.nodes.end;n!==null;){var i=n===r?null:cn(n);t.append(n),n=i}}var Bn=null,Vn=!1,Hn=!1;function Un(e){Hn=e}var H=null,Wn=!1;function Gn(e){H=e}var U=null;function Kn(e){U=e}var qn=null;function Jn(e){H!==null&&(H.f&2097152||H.f&2)&&(qn??=new Set).add(e)}var Yn=null,Xn=0,Zn=null;function Qn(e){Zn=e}var $n=1,er=0,tr=er;function nr(e){tr=e}function rr(){return++$n}function ir(e){var t=e.f;if(t&2048)return!0;if(t&4096){for(var n=e.deps,r=n.length,i=0;ie.wv)return!0}t&512&&Ct===null&&et(e,v)}return!1}function ar(e,t,n=!0){var r=e.reactions;if(r!==null&&!(qn!==null&&qn.has(e)))for(var i=0;i{e.ac.abort(pe)}),e.ac=null);try{e.f|=D;var u=e.fn,d=u();e.f|=C;var f=sr(e);if(Je()&&Zn!==null&&!Wn&&f!==null&&!(e.f&6146))for(var p=0;p0)for(t.length=Xn+Yn.length,r=0;r{s.ac.abort(pe),s.ac=null,et(s,y)}),yt(s),lr(s,0)}}function lr(e,t){var n=e.deps;if(n!==null)for(var r=t;rn?.call(this,e))}return e.startsWith(`pointer`)||e.startsWith(`touch`)||e===`wheel`?(i.__removed=!1,Ze(()=>{i.__removed||t.addEventListener(e,i,r)})):t.addEventListener(e,i,r),i}function Cr(e,t,n,r,i){var a={capture:r,passive:i},o=Sr(e,t,n,a);(t===document.body||t===window||t===document||t instanceof HTMLMediaElement)&&yn(()=>{o.__removed=!0,t.removeEventListener(e,o,a)})}function G(e,t,n){(t[yr]??={})[e]=n}function wr(e){for(var t=0;t{Er=!1,Tr=null}));var s=0,c=Tr===e&&e[yr];if(c){var l=a.indexOf(c);if(l!==-1&&(t===document||t===window)){e[yr]=t;return}var u=a.indexOf(t);if(u===-1)return;l<=u&&(s=l)}if(o=a[s]||e.target,o!==t){i(e,`currentTarget`,{configurable:!0,get(){return o||n}});var d=H,f=U;Gn(null),Kn(null);try{for(var p,m=[];o!==null&&o!==t;){try{var h=o[yr]?.[r];h!=null&&(!o.disabled||e.target===o)&&h.call(o,e)}catch(e){p?m.push(e):p=e}if(e.cancelBubble)break;s++,o=s{throw e});throw p}}finally{e[yr]=t,delete e.currentTarget,Gn(d),Kn(f)}}}var Or=globalThis?.window?.trustedTypes&&globalThis.window.trustedTypes.createPolicy(`svelte-trusted-html`,{createHTML:e=>e});function kr(e){return Or?.createHTML(e)??e}function Ar(e){var t=dn(`template`);return t.innerHTML=kr(e.replaceAll(``,``)),t.content}function jr(e,t){var n=U;n.nodes===null&&(n.nodes={start:e,end:t,a:null,t:null})}function K(e,t){var n=!!(t&1),r=!!(t&2),i,a=!e.startsWith(``);return()=>{if(k)return jr(A,null),A;i===void 0&&(i=Ar(a?e:``+e),n||(i=sn(i)));var t=r||tn?document.importNode(i,!0):i.cloneNode(!0);if(n){var o=sn(t),s=t.lastChild;jr(o,s)}else jr(t,t);return t}}function Mr(e,t,n=`svg`){var r=!e.startsWith(``),i=!!(t&1),a=`<${n}>${r?e:``+e}`,o;return()=>{if(k)return jr(A,null),A;if(!o){var e=sn(Ar(a));if(i)for(o=document.createDocumentFragment();sn(e);)o.appendChild(sn(e));else o=sn(e)}var t=o.cloneNode(!0);if(i){var n=sn(t),r=t.lastChild;jr(n,r)}else jr(t,t);return t}}function Nr(e,t){return Mr(e,t,`svg`)}function Pr(e=``){if(!k){var t=on(e+``);return jr(t,t),t}var n=A;return n.nodeType===3?fn(n):(n.before(n=on()),Se(n)),jr(n,n),n}function Fr(){if(k)return jr(A,null),A;var e=document.createDocumentFragment(),t=document.createComment(``),n=on();return e.append(t,n),jr(t,n),e}function q(e,t){if(k){var n=U;(!(n.f&32768)||n.nodes.end===null)&&(n.nodes.end=A),Ce();return}e!==null&&e.before(t)}function Ir(){if(k&&A&&A.nodeType===8&&A.textContent?.startsWith(`$`)){let e=A.textContent.substring(1);return Ce(),e}return(window.__svelte??={}).uid??=1,`c${window.__svelte.uid++}`}function Lr(e){let t=0,n=Ht(0),r;return()=>{vn()&&(W(n),En(()=>(t===0&&(r=mr(()=>e(()=>Yt(n)))),t+=1,()=>{Ze(()=>{--t,t===0&&(r?.(),r=void 0,Yt(n))})})))}}var Rr=T|ee;function zr(e,t,n,r){new Br(e,t,n,r)}var Br=class{parent;is_pending=!1;transform_error;#e;#t=k?A:null;#n;#r;#i;#a=null;#o=null;#s=null;#c=null;#l=0;#u=0;#d=!1;#f=new Set;#p=new Set;#m=null;#h=Lr(()=>(this.#m=Ht(this.#l),()=>{this.#m=null}));constructor(e,t,n,r){this.#e=e,this.#n=t,this.#r=e=>{var t=U;t.b=this,t.f|=128,n(e)},this.parent=U.b,this.transform_error=r??this.parent?.transform_error??(e=>e),this.#i=Dn(()=>{if(k){let e=this.#t;Ce();let t=e.data===`[!`;if(e.data.startsWith(`[?`)){let t=JSON.parse(e.data.slice(2));this.#_(t)}else t?this.#y():this.#g()}else this.#b()},Rr),k&&(this.#e=A)}#g(){try{this.#a=On(()=>this.#r(this.#e))}catch(e){this.error(e)}}#_(e){let t=this.#n.failed,{reset:n,invoke_onerror:r}=this.#v(e);Ze(r),t&&(this.#s=On(()=>{t(this.#e,()=>e,()=>n)}))}#v(e){var t=!1,n=!1;let r=()=>{if(t){be();return}t=!0,n&&Ve(),this.#s!==null&&Fn(this.#s,()=>{this.#s=null}),this.#S(()=>{this.#b()})};return{reset:r,invoke_onerror:()=>{try{n=!0,this.#n.onerror?.(e,r),n=!1}catch(e){mn(e,this.#i&&this.#i.parent)}}}}#y(){let e=this.#n.pending;e&&(this.is_pending=!0,this.#o=On(()=>e(this.#e)),Ze(()=>{var e=this.#c=document.createDocumentFragment(),t=on(),n=!1;if(e.append(t),this.#a=this.#S(()=>{try{return On(()=>this.#r(t))}catch(e){try{this.error(e),n=!0}catch(e){mn(e,this.#i.parent)}return null}}),this.#a===null){this.#c=null,n&&this.#x(P);return}this.#u===0&&(this.#e.before(e),this.#c=null,Fn(this.#o,()=>{this.#o=null}),this.#x(P))}))}#b(){try{if(this.is_pending=this.has_pending_snippet(),this.#u=0,this.#l=0,this.#a=On(()=>{this.#r(this.#e)}),this.#u>0){var e=this.#c=document.createDocumentFragment();zn(this.#a,e);let t=this.#n.pending;this.#o=On(()=>t(this.#e))}else this.#x(P)}catch(e){this.error(e)}}#x(e){this.is_pending=!1,e.transfer_effects(this.#f,this.#p)}defer_effect(e){nt(e,this.#f,this.#p)}is_rendered(){return!this.is_pending&&(!this.parent||this.parent.is_rendered())}has_pending_snippet(){return!!this.#n.pending}#S(e){var t=U,n=H,r=M;Kn(this.#i),Gn(this.#i),We(this.#i.ctx);try{return jt.ensure(),e()}finally{Kn(t),Gn(n),We(r)}}#C(e,t){if(!this.has_pending_snippet()){this.parent&&this.parent.#C(e,t);return}this.#u+=e,this.#u===0&&(this.#x(t),this.#o&&Fn(this.#o,()=>{this.#o=null}),this.#c&&=(this.#e.before(this.#c),null))}update_pending_count(e,t){this.#C(e,t),this.#l+=e,!(!this.#m||this.#d)&&(this.#d=!0,Ze(()=>{this.#d=!1,this.#m&&Kt(this.#m,this.#l)}))}get_effect_pending(){return this.#h(),W(this.#m)}error(e){if(!this.#n.onerror&&!this.#n.failed)throw e;P?.is_fork?(this.#a&&P.skip_effect(this.#a),this.#o&&P.skip_effect(this.#o),this.#s&&P.skip_effect(this.#s),P.oncommit(()=>{this.#w(e)})):this.#w(e)}#w(e){this.#a&&=(Mn(this.#a),null),this.#o&&=(Mn(this.#o),null),this.#s&&=(Mn(this.#s),null),k&&(Se(this.#t),we(),Se(Te()));let t=this.#n.failed,n=e=>{let{reset:n,invoke_onerror:r}=this.#v(e);r(),t&&(this.#s=this.#S(()=>{try{return On(()=>{var r=U;r.b=this,r.f|=128,t(this.#e,()=>e,()=>n)})}catch(e){return mn(e,this.#i.parent),null}}))};Ze(()=>{var t;try{t=this.transform_error(e)}catch(e){mn(e,this.#i&&this.#i.parent);return}typeof t==`object`&&t&&typeof t.then==`function`?t.then(n,e=>mn(e,this.#i&&this.#i.parent)):n(t)})}};function J(e,t){var n=t==null?``:typeof t==`object`?`${t}`:t;n!==(e[de]??=e.nodeValue)&&(e[de]=n,e.nodeValue=`${n}`)}function Vr(e,t){return Ur(e,t)}var Hr=new Map;function Ur(e,{target:t,anchor:n,props:i={},events:a,context:o,intro:s=!0,transformError:c}){an();var l=void 0,u=Cn(()=>{var s=n??t.appendChild(on());zr(s,{pending:()=>{}},t=>{Ge({});var n=M;if(o&&(n.c=o),a&&(i.$$events=a),k&&jr(t,null),l=e(t,i)||qe(),k&&(U.nodes.end=A,A===null||A.nodeType!==8||A.data!==`]`))throw ve(),he;Ke()},c);var u=new Set,d=e=>{for(var n=0;n{for(var e of u)for(let n of[t,document]){var r=Hr.get(n),i=r.get(e);--i==0?(n.removeEventListener(e,Dr),r.delete(e),r.size===0&&Hr.delete(n)):r.set(e,i)}xr.delete(d),s!==n&&s.parentNode?.removeChild(s)}});return Wr.set(l,u),l}var Wr=new WeakMap,Gr=class{anchor;#e=new Map;#t=new Map;#n=new Map;#r=new Set;#i=!0;constructor(e,t=!0){this.anchor=e,this.#i=t}#a=e=>{if(this.#e.has(e)){var t=this.#e.get(e),n=this.#t.get(t);if(n)Ln(n),this.#r.delete(t);else{var r=this.#n.get(t);r&&(Ln(r.effect),this.#t.set(t,r.effect),this.#n.delete(t),r.fragment.lastChild.remove(),this.anchor.before(r.fragment),n=r.effect)}for(let[t,n]of this.#e){if(this.#e.delete(t),t===e)break;let r=this.#n.get(n);r&&(Mn(r.effect),this.#n.delete(n))}for(let[e,r]of this.#t){if(e===t||this.#r.has(e))continue;let i=()=>{if(Array.from(this.#e.values()).includes(e)){var t=document.createDocumentFragment();zn(r,t),t.append(on()),this.#n.set(e,{effect:r,fragment:t})}else Mn(r);this.#r.delete(e),this.#t.delete(e)};this.#i||!n?(this.#r.add(e),Fn(r,i,!1)):i()}}};#o=e=>{this.#e.delete(e);let t=Array.from(this.#e.values());for(let[e,n]of this.#n)t.includes(e)||(Mn(n.effect),this.#n.delete(e))};ensure(e,t){var n=P,r=un();if(t&&!this.#t.has(e)&&!this.#n.has(e)){if(r){var i=document.createDocumentFragment(),a=on();i.append(a),this.#n.set(e,{effect:On(()=>t(a)),fragment:i})}else this.#t.set(e,On(()=>t(this.anchor)))}if(this.#e.set(n,e),r){for(let[t,r]of this.#t)t===e?n.unskip_effect(r):n.skip_effect(r);for(let[t,r]of this.#n)t===e?n.unskip_effect(r.effect):n.skip_effect(r.effect);n.oncommit(this.#a),n.ondiscard(this.#o)}else k&&(this.anchor=A),this.#a(n)}};function Y(e,t,n=!1){var r;k&&(r=A,Ce());var i=new Gr(e),a=n?T:0;function o(e,t){if(k){var n=Ee(r);if(e!==parseInt(n.substring(1))){var a=Te();Se(a),i.anchor=a,xe(!1),i.ensure(e,t),xe(!0);return}}i.ensure(e,t)}Dn(()=>{var e=!1;t((t,n=0)=>{e=!0,o(n,t)}),e||o(-1,null)},a)}var Kr=Symbol(`NaN`);function qr(e,t,n){k&&Ce();var r=new Gr(e),i=!Je();Dn(()=>{var e=t();e!==e&&(e=Kr),i&&typeof e==`object`&&e&&(e={}),r.ensure(e,n)})}function X(e,t){return t}function Jr(e,t,n){for(var i=[],a=t.length,o,s=t.length,c=0;c{if(o){if(o.pending.delete(n),o.done.add(n),o.pending.size===0){var t=e.outrogroups;Yr(e,r(o.done)),t.delete(o),t.size===0&&(e.outrogroups=null)}}else--s},!1)}if(s===0){var l=i.length===0&&n!==null&&e.pending.size===0;if(l){var u=n,d=u.parentNode;ln(d),d.append(u),e.items.clear()}Yr(e,t,!l)}else o={pending:new Set(t),done:new Set},(e.outrogroups??=new Set).add(o)}function Yr(e,t,n=!0){var r;if(e.pending.size>0){r=new Set;for(let t of e.pending.values())for(let n of t)r.add(e.items.get(n).e)}for(var i=0;i{var t=i();return e(t)?t:t==null?[]:r(t)}),p,m=new Map,h=!0;function g(e){v.effect.f&16384||(v.pending.delete(e),v.fallback=d,Qr(v,p,c,n,a),d!==null&&(p.length===0?d.f&33554432?(d.f^=E,ei(d,null,c)):Ln(d):Fn(d,()=>{d=null})))}function _(e){v.pending.delete(e)}var v={effect:Dn(()=>{p=W(f);var e=p.length;let t=!1;k&&Ee(c)===`[!`!=(e===0)&&(c=Te(),Se(c),xe(!1),t=!0);for(var r=new Set,u=P,v=un(),y=0;ys(c)):(d=On(()=>s(Xr??=on())),d.f|=E)),e>r.size&&Me(``,``,``),k&&e>0&&Se(Te()),!h){if(m.set(u,r),v){for(let[e,t]of l)r.has(e)||u.skip_effect(t.e);u.oncommit(g),u.ondiscard(_)}else g(u)}t&&xe(!0),W(f)}),flags:n,items:l,pending:m,outrogroups:null,fallback:d};h=!1,k&&(c=A)}function Zr(e){for(;e!==null&&!(e.f&32);)e=e.next;return e}function Qr(e,t,n,i,a){var o=!!(i&8),s=t.length,c=e.items,l=Zr(e.effect.first),u,d=null,f,p=[],m=[],h,g,_,v;if(o)for(v=0;v0){var ee=i&4&&s===0?n:null;if(o){for(v=0;v{if(f!==void 0)for(_ of f)_.nodes?.a?.apply()})}function $r(e,t,n,r,i,a,o,s){var c=o&1?o&16?Ht(n):Ut(n,!1,!1):null,l=o&2?Ht(i):null;return{v:c,i:l,e:On(()=>(a(t,c??n,l??i,s),()=>{e.delete(r)}))}}function ei(e,t,n){if(e.nodes)for(var r=e.nodes.start,i=e.nodes.end,a=t&&!(t.f&33554432)?t.nodes.start:n;r!==null;){var o=cn(r);if(a.before(r),r===i)return;r=o}}function ti(e,t,n){t===null?e.effect.first=n:t.next=n,n===null?e.effect.last=t:n.prev=t}function ni(e,t,n){var r;k&&(r=A,Ce());var i=new Gr(e);Dn(()=>{var e=t()??null;if(k&&Ee(r)===`[`!=(e!==null)){var a=Te();Se(a),i.anchor=a,xe(!1),i.ensure(e,e&&(t=>n(t,e))),xe(!0);return}i.ensure(e,e&&(t=>n(t,e)))},T)}var ri=[...` -\r\f\xA0\v`];function ii(e,t,n){var r=e==null?``:``+e;if(t&&(r=r?r+` `+t:t),n){for(var i of Object.keys(n))if(n[i])r=r?r+` `+i:i;else if(r.length)for(var a=i.length,o=0;(o=r.indexOf(i,o))>=0;){var s=o+a;(o===0||ri.includes(r[o-1]))&&(s===r.length||ri.includes(r[s]))?r=(o===0?``:r.substring(0,o))+r.substring(s+1):o=s}}return r===``?null:r}function ai(e,t=!1){var n=t?` !important;`:`;`,r=``;for(var i of Object.keys(e)){var a=e[i];a!=null&&a!==``&&(r+=` `+i+`: `+a+n)}return r}function oi(e){return e[0]!==`-`||e[1]!==`-`?e.toLowerCase():e}function si(e,t){if(t){var n=``,r,i;if(Array.isArray(t)?(r=t[0],i=t[1]):r=t,e){e=String(e).replaceAll(/\/\*.*?\*\//g,``).trim();var a=!1,o=0,s=!1,c=[];r&&c.push(...Object.keys(r).map(oi)),i&&c.push(...Object.keys(i).map(oi));var l=0,u=-1;let t=e.length;for(var d=0;d{t.every(_i)||(`__defaultValue`in e&&fi(e,!1),`__value`in e&&pi(e,e.__value))});t.observe(e,{childList:!0,subtree:!0,attributes:!0,attributeFilter:[`value`]}),yn(()=>{t.disconnect()})}function hi(e,t,n=t){var r=new WeakSet,i=!0;st(e,`change`,t=>{var i=t?`[selected]`:`:checked`,a;if(e.multiple)a=[].map.call(e.querySelectorAll(i),gi);else{var o=e.querySelector(i)??e.querySelector(`option:not([disabled])`);a=o&&gi(o)}n(a),e.__value=a,P!==null&&r.add(P)}),wn(()=>{var a=t();if(e===document.activeElement){var o=P;if(r.has(o))return}if(pi(e,a,i),i&&a===void 0){var s=e.querySelector(`:checked`);s!==null&&(a=gi(s),n(a))}e.__value=a,i=!1})}function gi(e){return`__value`in e?e.__value:e.value}function _i(e){if(e.target.closest(`selectedcontent`)!==null)return!0;if(e.type===`childList`){var t=[...e.addedNodes,...e.removedNodes];return t.length>0&&t.every(e=>e.nodeName===`SELECTEDCONTENT`)}return!1}var vi=Symbol(`is custom element`),yi=Symbol(`is html`),bi=me?`link`:`LINK`;function xi(e){if(k){var t=!1,n=()=>{if(!t){if(t=!0,e.hasAttribute(`value`)){var n=e.value;Q(e,`value`,null),e.value=n}if(e.hasAttribute(`checked`)){var r=e.checked;Q(e,`checked`,null),e.checked=r}}};e[fe]=n,Ze(n),at()}}function Q(e,t,n,r){var i=Si(e);k&&(i[t]=e.getAttribute(t),t===`src`||t===`srcset`||t===`href`&&e.nodeName===bi)||i[t]!==(i[t]=n)&&(t===`loading`&&(e[se]=n),n==null?e.removeAttribute(t):typeof n!=`string`&&wi(e).has(t)?e[t]=n:e.setAttribute(t,n))}function Si(e){return e[ce]??={[vi]:e.nodeName.includes(`-`),[yi]:e.namespaceURI===ge}}var Ci=new Map;function wi(e){var t=e.getAttribute(`is`)||e.nodeName,n=Ci.get(t);if(n)return n;Ci.set(t,n=new Set);for(var r,i=e,a=Element.prototype;a!==i;){for(var s in r=o(i),r)r[s].set&&s!==`innerHTML`&&s!==`textContent`&&s!==`innerText`&&n.add(s);i=l(i)}return n}function Ti(e,t,n=t){var r=new WeakSet;st(e,`input`,async i=>{var a=i?e.defaultValue:e.value;if(a=ki(e)?Ai(a):a,n(a),P!==null&&r.add(P),await dr(),a!==(a=t())){var o=e.selectionStart,s=e.selectionEnd,c=e.value.length;if(e.value=a??``,s!==null){var l=e.value.length;o===s&&s===c&&l>c?(e.selectionStart=l,e.selectionEnd=l):(e.selectionStart=o,e.selectionEnd=Math.min(s,l))}}}),(k&&e.defaultValue!==e.value||mr(t)==null&&e.value)&&(n(ki(e)?Ai(e.value):e.value),P!==null&&r.add(P)),En(()=>{var n=t();if(e===document.activeElement){var i=P;if(r.has(i))return}ki(e)&&n===Ai(e.value)||(e.type!==`date`||n||e.value)&&n!==e.value&&(e.value=n??``)})}var Ei=new Set;function Di(e,t,n,r,i=r){var a=n.getAttribute(`type`)===`checkbox`,o=e;let s=!1;if(t!==null)for(var c of t)o=o[c]??=[];o.push(n),st(n,`change`,()=>{var e=n.__value;a&&(e=Oi(o,e,n.checked)),i(e)},()=>i(a?[]:null)),En(()=>{var e=r();if(k&&n.defaultChecked!==n.checked){s=!0;return}a?(e||=[],n.checked=e.includes(n.__value)):n.checked=$t(n.__value,e)}),yn(()=>{var e=o.indexOf(n);e!==-1&&o.splice(e,1)}),Ei.has(o)||(Ei.add(o),Ze(()=>{o.sort((e,t)=>e.compareDocumentPosition(t)===4?-1:1),Ei.delete(o)})),Ze(()=>{if(s){var e=a?Oi(o,e,n.checked):o.find(e=>e.checked)?.__value;i(e)}})}function Oi(e,t,n){for(var r=new Set,i=0;i{var n=this.#e.get(e);n.delete(t),n.size===0&&(this.#e.delete(e),this.#t.unobserve(e))}}#r(){return this.#t??=new ResizeObserver(t=>{for(var n of t){e.entries.set(n.target,n);for(var r of this.#e.get(n.target)||[])r(n)}})}}({box:`border-box`});function Mi(e,t,n){var r=ji.observe(e,()=>n(e[t]));wn(()=>(mr(()=>n(e[t])),r))}function Ni(e,t){return e===t||e?.[ie]===t}function Pi(e=qe(),t,n,r){var i=M.r,a=U;return wn(()=>{var o,s;return En(()=>{o=s,s=r?.()||[],mr(()=>{Ni(n(...s),e)||(t(e,...s),o&&Ni(n(...o),e)&&t(null,...o))})}),()=>{let r=a;for(;r!==i&&r.parent!==null&&r.parent.f&33554432;)r=r.parent;let o=()=>{s&&Ni(n(...s),e)&&t(null,...s)},c=r.teardown;r.teardown=()=>{o(),c?.()}}}),e}function Fi(e,t,n,r,i){var a=()=>{r(n[e])};n.addEventListener(t,a),i?En(()=>{n[e]=i()}):a(),(n===document.body||n===window||n===document)&&yn(()=>{n.removeEventListener(t,a)})}function Ii(e=!1){let t=M,n=t.l.u;if(!n)return;let r=()=>hr(t.s);if(e){let e=0,n={},i=ft(()=>{let r=!1,i=t.s;for(let e in i)i[e]!==n[e]&&(n[e]=i[e],r=!0);return r&&e++,e});r=()=>W(i)}n.b.length&&Sn(()=>{Li(t,r),m(n.b)}),bn(()=>{let e=mr(()=>n.m.map(p));return()=>{for(let t of e)typeof t==`function`&&t()}}),n.a.length&&bn(()=>{Li(t,r),m(n.a)})}function Li(e,t){if(e.l.s)for(let t of e.l.s)W(t);t()}var Ri=!1;function zi(e){var t=Ri;try{return Ri=!1,[e(),Ri]}finally{Ri=t}}var Bi={get(e,t){let n=e.props.length;for(;n--;){let r=e.props[n];if(d(r)&&(r=r()),typeof r==`object`&&r&&t in r)return r[t]}},set(e,t,n){let r=e.props.length;for(;r--;){let i=e.props[r];d(i)&&(i=i());let o=a(i,t);if(o&&o.set)return o.set(n),!0}return!1},getOwnPropertyDescriptor(e,t){let n=e.props.length;for(;n--;){let r=e.props[n];if(d(r)&&(r=r()),typeof r==`object`&&r&&t in r){let e=a(r,t);return e&&!e.configurable&&(e.configurable=!0),e}}},has(e,t){if(t===ie||t===oe)return!1;for(let n of e.props)if(d(n)&&(n=n()),n!=null&&t in n)return!0;return!1},ownKeys(e){let t=[];for(let n of e.props)if(d(n)&&(n=n()),n){for(let e in n)t.includes(e)||t.push(e);for(let e of Object.getOwnPropertySymbols(n))t.includes(e)||t.push(e)}return t}};function Vi(...e){return new Proxy({props:e},Bi)}function Hi(e,t,n,r){var i=!He||!!(n&2),o=!!(n&8),s=!!(n&16),c=r,l=!0,u=void 0,d=()=>s&&i?(u??=ft(r),W(u)):(l&&(l=!1,c=s?mr(r):r),c);let f;if(o){var p=ie in e||oe in e;f=a(e,t)?.set??(p&&t in e?n=>e[t]=n:void 0)}var m,h=!1;o?[m,h]=zi(()=>e[t]):m=e[t],m===void 0&&r!==void 0&&(m=d(),f&&(i&&Le(t),f(m)));var g=i?()=>{var n=e[t];return n===void 0?d():(l=!0,n)}:()=>{var n=e[t];return n!==void 0&&(c=void 0),n===void 0?c:n};if(i&&!(n&4))return g;if(f){var _=e.$$legacy;return(function(e,t){return arguments.length>0?((!i||!t||_||h)&&f(t?g():e),e):g()})}var v=!1,y=(n&1?ft:ht)(()=>(v=!1,g()));o&&W(y);var b=U;return(function(e,t){if(arguments.length>0){let n=t?W(y):i&&o?Zt(e):e;return I(y,n),v=!0,c!==void 0&&(c=n),e}return Hn&&v||b.f&16384?y.v:W(y)})}function Ui(e){M===null&&Ae(`onMount`),He&&M.l!==null?Wi(M).m.push(e):bn(()=>{let t=mr(e);if(typeof t==`function`)return t})}function Wi(e){var t=e.l;return t.u??={a:[],b:[],m:[]}}typeof window<`u`&&((window.__svelte??={}).v??=new Set).add(`5`);function Gi(e,t){if(e instanceof RegExp)return{keys:!1,pattern:e};var n,r,i,a,o=[],s=``,c=e.split(`/`);for(c[0]||c.shift();i=c.shift();)n=i[0],n===`*`?(o.push(`wild`),s+=`/(.*)`):n===`:`?(r=i.indexOf(`?`,1),a=i.indexOf(`.`,1),o.push(i.substring(1,~r?r:~a?a:i.length)),s+=~r&&!~a?`(?:/([^/]+?))?`:`/([^/]+?)`,~a&&(s+=(~r?`?`:``)+`\\`+i.substring(a))):s+=`/`+i;return{keys:o,pattern:RegExp(`^`+s+(t?`(?=$|/)`:`/?$`),`i`)}}var Ki=new class{#e=F(qi());get _loc(){return W(this.#e)}set _loc(e){I(this.#e,e)}#t=N(()=>this._loc.location);get _location(){return W(this.#t)}set _location(e){I(this.#t,e)}#n=N(()=>this._loc.querystring);get _querystring(){return W(this.#n)}set _querystring(e){I(this.#n,e)}#r=F(void 0);get _params(){return W(this.#r)}set _params(e){I(this.#r,e)}get loc(){return this._loc}get location(){return this._location}get querystring(){return this._querystring}get params(){return this._params}constructor(){typeof window<`u`?window.addEventListener(`hashchange`,()=>{this._loc=qi()}):console.warn(`[svelte-spa-router] window 'window' is not defined, skipping initiation`)}};function qi(){let e=typeof window<`u`?window.location.href:``,t=e.indexOf(`#/`),n=t>-1?e.substr(t+1):`/`,r=n.indexOf(`?`),i=``;return r>-1&&(i=n.substr(r+1),n=n.substr(0,r)),{location:n,querystring:i}}function Ji(e){e?window.scrollTo(e.__svelte_spa_router_scrollX||0,e.__svelte_spa_router_scrollY||0):window.scrollTo(0,0)}function Yi(e,t){Ge(t,!0);let n=Hi(t,`routes`,19,()=>({})),r=Hi(t,`prefix`,3,``),i=Hi(t,`restoreScrollState`,3,!1),a=Hi(t,`onConditionsFailed`,3,()=>{}),o=Hi(t,`onRouteLoaded`,3,()=>{}),s=Hi(t,`onRouteLoading`,3,()=>{}),c=Hi(t,`onRouteEvent`,3,()=>{});class l{path;component;conditions;userData;props;_pattern;_keys;constructor(e,t){let n=e=>typeof e==`object`&&!!e&&e._sveltesparouter===!0;if(!t||typeof t!=`function`&&!n(t))throw Error(`Invalid component object`);if(!e||typeof e==`string`&&(e.length<1||e.charAt(0)!=`/`&&e.charAt(0)!=`*`)||typeof e==`object`&&!(e instanceof RegExp))throw Error(`Invalid value for "path" argument - strings must start with / or *`);let r=Gi(e);if(this.path=e,n(t)){let e=t;this.component=e.component,this.conditions=e.conditions||[],this.userData=e.userData,this.props=e.props||{}}else{let e=t;this.component=()=>Promise.resolve(e),this.conditions=[],this.props={}}this._pattern=r.pattern,this._keys=r.keys}match(e){if(r()){if(typeof r()==`string`){if(e.startsWith(r()))e=e.substr(r().length)||`/`;else return null}else if(r()instanceof RegExp){let t=e.match(r());if(t&&t[0])e=e.substr(t[0].length)||`/`;else return null}}let t=this._pattern.exec(e);if(t===null)return null;if(this._keys===!1)return t;let n={},i=0;for(;i{u.push(new l(t,e))}):Object.keys(n()).forEach(e=>{let t=n();u.push(new l(e,t[e]))});let d=F(null),f=F(null),p=F({}),m=null,h=null;bn(()=>{history.scrollRestoration=i()?`manual`:`auto`}),bn(()=>{if(!i())return;let e=e=>{m=e.state&&(e.state.__svelte_spa_router_scrollY||e.state.__svelte_spa_router_scrollX)?e.state:null};return window.addEventListener(`popstate`,e),()=>window.removeEventListener(`popstate`,e)});async function g(e,t){await dr(),e(t)}bn(()=>{let e=Ki.loc,t=!1;return mr(async()=>{let n=0;for(;n{t=!0}});function _(e){return e&&typeof e==`object`&&Object.keys(e).length?e:null}var v=Fr(),y=R(v),b=e=>{let t=N(()=>W(d));var n=Fr(),r=R(n),i=e=>{var n=Fr();ni(R(n),()=>W(t),(e,t)=>{t(e,Vi({get params(){return W(f)},get onRouteEvent(){return c()}},()=>W(p)))}),q(e,n)},a=e=>{var n=Fr();ni(R(n),()=>W(t),(e,t)=>{t(e,Vi({get onRouteEvent(){return c()}},()=>W(p)))}),q(e,n)};Y(r,e=>{W(f)?e(i):e(a,-1)}),q(e,n)};Y(y,e=>{W(d)&&e(b)}),q(e,v),Ke()}var Xi=[`bars`,`pace`,`zone`,`pie`,`fuel`,`resets`,`thresholds`],Zi=`codexometer.web.preferences.v1`,Qi={tab:`quota`,view:`bars`,selected:``,defaultDetail:0,layouts:[]};function $i(){try{let e=JSON.parse(localStorage.getItem(Zi)||`null`);return!e||typeof e!=`object`?Qi:{tab:[`quota`,`sessions`,`usage`].includes(e.tab)?e.tab:`quota`,view:Xi.includes(e.view)?e.view:`bars`,selected:typeof e.selected==`string`?e.selected.slice(0,256):``,defaultDetail:+(e.defaultDetail===1),layouts:Array.isArray(e.layouts)?e.layouts.filter(e=>{if(!e||typeof e!=`object`)return!1;let t=e;return typeof t.id==`string`&&t.id.length<=256&&[0,1,2].includes(t.level)}).slice(-100):[]}}catch{return Qi}}var ea=Zt($i());function ta(){let e=JSON.stringify(ea);try{localStorage.setItem(Zi,e)}catch{}}function na(){return ea.tab===`quota`?`/quota/`+ea.view:`/`+ea.tab}function ra(e){return ea.layouts.find(t=>t.id===e)?.level??ea.defaultDetail}function ia(e){ea.defaultDetail=e,ea.layouts=[]}function aa(e,t){ea.layouts=[...ea.layouts.filter(t=>t.id!==e),{id:e,level:Math.max(0,Math.min(2,t))}].slice(-100)}var $=Zt({data:null,connected:!1,error:``}),oa=e=>e==null?`Unavailable`:e.toLocaleString(`en-GB`),sa=e=>!e||String(e).startsWith(`0001-`)?`Not yet available`:new Date(typeof e==`number`?e*1e3:e).toLocaleString(`en-GB`),ca=`codexometer.web.session`,la=class extends Error{},ua;async function da(e,t,n){if(!ua||!$.connected||!$.data?.control||$.data.sessionsError)throw Error(`Session controls unavailable. Check the live connection.`);return await ua(e,t,n)}function fa(){let e=new AbortController,t,n=``;ua=async(t,r,i)=>{let a=await fetch(`/api/control/`+t,{method:`POST`,headers:{Authorization:`Bearer ${n}`,"Content-Type":`application/json`},body:JSON.stringify(r),signal:AbortSignal.any([e.signal,AbortSignal.timeout(8e3),...i?[i]:[]]),cache:`no-store`});if(!a.ok)throw a.status===502?Error(`Outcome uncertain. Check Codex before taking another action; nothing was retried.`):new la(`Action rejected, expired or changed. Refresh and check Codex; nothing was retried.`);return a.json()};try{n=sessionStorage.getItem(ca)||``}catch{}let r=location.hash,i=r.startsWith(`#pair=`)?r.slice(6):``;(i||!r||r===`#`)&&(history.replaceState(null,``,`/#`+na()),window.dispatchEvent(new HashChangeEvent(`hashchange`)));async function a(){if(i)try{let t=await fetch(`/api/pair`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({secret:i}),signal:e.signal});if(!t.ok)throw Error(`Pairing link expired or already used. Restart --web for a fresh link.`);n=(await t.json()).token;try{sessionStorage.setItem(ca,n)}catch{}}catch(t){e.signal.aborted||($.error=t instanceof Error?t.message:`Pairing failed`);return}if(!n){$.error=`Open the private pairing link printed by codexometer --web in your terminal.`;return}await o()}async function o(){try{let t=await fetch(`/api/events`,{headers:{Authorization:`Bearer ${n}`},signal:e.signal,cache:`no-store`});if(t.status===401){try{sessionStorage.removeItem(ca)}catch{}$.error=`Browser session expired. Restart --web and open its new pairing link.`;return}if(!t.ok||!t.body)throw Error(`Live connection unavailable`);let r=t.body.getReader(),i=new TextDecoder,a=``;for(;!e.signal.aborted;){let{done:e,value:t}=await r.read();if(e)break;a+=i.decode(t,{stream:!0});let n;for(;(n=a.indexOf(` - -`))>=0;){let e=a.slice(0,n);a=a.slice(n+2),e.startsWith(`data: `)&&($.data=JSON.parse(e.slice(6)),$.connected=!0,$.error=``)}}}catch{}finally{$.connected=!1}e.signal.aborted||($.error=`Connection lost — showing last observation. Reconnecting…`,t=setTimeout(o,2500))}return a(),()=>{ua=void 0,e.abort(),clearTimeout(t),$.connected=!1}}Ue();var pa=K(`

Quota refresh failed. Policy state is based on the last successful - observation.

`),ma=K(`

`),ha=K(`

The longest Codex quota window selects one active model profile. ASK - creates a per-session review; AUTO applies the profile on the next - eligible check.

`,1),ga=K(`

No threshold-based model steps were configured at launch.

`);function _a(e,t){Ge(t,!1),Ii();var n=Fr(),r=R(n),i=e=>{var t=ha(),n=R(t),r=z(n),i=B(n,2),a=e=>{q(e,pa())};Y(i,e=>{$.data.quotaError&&e(a)});var o=B(i,2),s=L(o),c=z(s),l=B(s,4);Z(l,5,()=>$.data.thresholds,X,(e,t)=>{var n=ma();let r;var i=L(n),a=z(i),o=B(i,2),s=L(o),c=z(s,!0),l=z(B(s,2));j(o);var u=B(o,2),d=z(u,!0),f=z(B(u,2));j(n),V((e,i)=>{r=ci(n,1,``,null,r,{active:W(t).state===`ACTIVE`,next:W(t).state===`NEXT`}),J(a,`${W(t).threshold??``}%`),J(c,W(t).model),J(l,`${e??``} REASONING // ${i??``} - SPEED`),J(d,W(t).mode),J(f,`${W(t).state??``}${W(t).state===`NEXT`?` // ${W(t).remaining||0} PP TO GO`:``}`)},[()=>W(t).effort.toUpperCase(),()=>W(t).speed.toUpperCase()]),q(e,n)}),j(l),j(o),V(e=>{J(r,`MODEL STEP POLICY // OBSERVED ${e??``}`),J(c,`THRESHOLDS // ${$.data.thresholds.length??``} CONFIGURED`)},[()=>sa($.data.quotaAt)]),q(e,t)},a=e=>{q(e,ga())};Y(r,e=>{$.data?.thresholds?.length?e(i):e(a,-1)}),q(e,n),Ke()}var va=Nr(` `,1),ya=Nr(` `,1),ba=K(` `),xa=K(`
Quota observations
Observed atPeriod elapsedConsumedTrail segment
`),Sa=K(`


Observed quota - path, not individual session usage. Gaps are not interpolated. Expand the - observation table for times, positions and gaps.

OBSERVATION TABLE
`,1),Ca=K(`
CONSUMPTIONQUOTA PERIOD ELAPSED


`,1);function wa(e,t){let n=Ir();Ge(t,!0);let r=Hi(t,`trail`,19,()=>[]),i=F(!1),a=[0,25,50,75,100],o=F(400),s=F(240),c=N(()=>W(o)-24),l=N(()=>W(s)-48),u=N(()=>Math.max(1,W(c)-48)),d=N(()=>Math.max(1,W(l)-20)),f=N(()=>48+Math.max(0,Math.min(100,t.elapsed))/100*W(u)),p=N(()=>W(l)-Math.max(0,Math.min(100,t.used))/100*W(d)),m=N(()=>t.used-t.elapsed),h=N(()=>r().map((e,t)=>`${t===0||e.break?`M`:`L`}${48+e.elapsed/100*W(u)} ${W(l)-e.used/100*W(d)}`).join(` `));var g=Ca(),_=R(g),v=L(_),y=L(v),b=z(y),x=B(y),S=B(x);Z(S,17,()=>a,X,(e,t)=>{var n=va(),r=R(n),i=B(r),a=B(i),o=z(a),s=B(a),f=z(s);V(()=>{Q(r,`x1`,48+W(t)/100*W(u)),Q(r,`x2`,48+W(t)/100*W(u)),Q(r,`y2`,W(l)),Q(i,`y1`,W(l)-W(t)/100*W(d)),Q(i,`x2`,W(c)),Q(i,`y2`,W(l)-W(t)/100*W(d)),Q(a,`x`,48+W(t)/100*W(u)),Q(a,`y`,W(l)+20),J(o,`${W(t)??``}%`),Q(s,`y`,W(l)+4-W(t)/100*W(d)),J(f,`${W(t)??``}%`)}),q(e,n)});var C=B(S),w=B(C),T=B(w),ee=e=>{var t=ya(),n=R(t),i=B(n),a=z(L(i));j(i),V(e=>{Q(n,`d`,W(h)),Q(i,`cx`,48+r()[0].elapsed/100*W(u)),Q(i,`cy`,W(l)-r()[0].used/100*W(d)),J(a,`First observation: ${e??``}`)},[()=>sa(r()[0].at)]),q(e,t)};Y(T,e=>{r().length&&e(ee)});var te=B(T,2),E=B(te),D=B(E),ne=z(L(D));j(D),j(v),j(_);var re=B(_,2),ie=L(re),ae=z(B(ie,3),!0);j(re);var oe=B(re,2),se=e=>{var t=Sa(),a=R(t),o=L(a);we(2),j(a);var s=B(a,2),c=B(L(s),2),l=e=>{var t=xa(),n=L(t),i=B(L(n),2);Z(i,21,r,X,(e,t,n)=>{var r=ba(),i=L(r),a=L(i),o=z(a,!0);j(i);var s=B(i),c=z(s),l=B(s),u=z(l),d=z(B(l),!0);j(r),V((e,r)=>{Q(a,`datetime`,W(t).at),J(o,e),J(c,`${r??``}%`),J(u,`${W(t).used??``}%`),J(d,n===0?`First observation`:W(t).break?`Gap before this observation`:`Connected to previous observation`)},[()=>sa(W(t).at),()=>W(t).elapsed.toFixed(1)]),q(e,r)}),j(i),j(n),j(t),q(e,t)};Y(c,e=>{W(i)&&e(l)}),j(s),V(e=>{Q(a,`id`,n+`-trail-summary`),J(o,`○ START ${e??``} // ${r().length??``} - ${r().length===1?`OBSERVATION`:`OBSERVATIONS`}`)},[()=>sa(r()[0].at)]),Fi(`open`,`toggle`,s,e=>I(i,e),()=>W(i)),q(e,t)};Y(oe,e=>{r().length&&e(se)}),V((e,i,a,m)=>{Q(v,`viewBox`,`0 0 ${W(o)} ${W(s)}`),Q(v,`aria-describedby`,r().length?n+`-trail-summary`:void 0),Q(v,`aria-label`,e),Q(b,`id`,n),Q(x,`width`,W(u)),Q(x,`height`,W(d)),Q(x,`fill`,`url(#${n})`),Q(C,`d`,`M48 20 V${W(l)} H${W(c)}`),Q(w,`y1`,W(l)),Q(w,`x2`,W(c)),Q(te,`x`,48+W(u)/2),Q(te,`y`,W(s)-5),Q(E,`cx`,W(f)),Q(E,`cy`,W(p)),Q(D,`cx`,W(f)),Q(D,`cy`,W(p)),J(ne,`${t.used??``}% consumed // ${i??``}% of period elapsed`),J(ie,`${t.used??``}% USED // ${a??``}% TIME ELAPSED`),J(ae,m)},[()=>`Consumption zone: ${t.used}% consumed, ${t.elapsed.toFixed(1)}% of quota period elapsed. ${W(m)>0?`Above`:W(m)<0?`Below`:`On`} the steady-consumption line.`,()=>t.elapsed.toFixed(1),()=>t.elapsed.toFixed(1),()=>Math.abs(W(m))<.05?`ON PACE`:W(m)>0?`ABOVE THE LINE — CONSUMING FASTER THAN TIME`:`BELOW THE LINE — WITHIN PACE`]),Mi(_,`clientWidth`,e=>I(o,e)),Mi(_,`clientHeight`,e=>I(s,e)),q(e,g),Ke()}var Ta=K(` `),Ea=K(`

Quota refresh failed. Values below are the last successful observation.

`),Da=K(`

Expiry details unavailable. No listed expiry does not mean no expiry.

`),Oa=K(`

`),ka=K(`

Read-only preview. Use the terminal to redeem a reset.

The backend may return only some credits. This list does not establish - redemption order.

`),Aa=K(` `,1),ja=Nr(``),Ma=Nr(``),Na=K(`
`),Pa=K(`

Cycle duration or reset date unavailable — position cannot be - plotted.

`),Fa=K(`
▼
−100 // OVER BUDGET+100 // HEADROOM

`,1),Ia=K(`

Cycle duration unavailable — pace cannot be calculated.

`),La=K(`
EMPTYFULL
`),Ra=K(`

`,1),za=K(`
`,1),Ba=K(`

`),Va=K(`

`),Ha=K(`

No quota windows reported yet.

`),Ua=K(`
`,1),Wa=K(`

All reported windows are shown. API-equivalent learning and quota status - scoring remain in the terminal for this first preview.

`,1),Ga=K(` `,1);function Ka(e,t){Ge(t,!0);let n=Hi(t,`params`,19,()=>({})),r=N(()=>Xi.filter(e=>e!==`thresholds`||!!$.data?.thresholds?.length)),i=F(Zt(Date.now())),a=N(()=>W(r).includes(n().view||``)?n().view:`bars`);Ui(()=>{let e=setInterval(()=>I(i,Date.now(),!0),1e3);return()=>clearInterval(e)});function o(e){return!e.duration||e.duration<=0||!e.reset?null:Math.max(0,Math.min(100,100*(1-(e.reset*1e3-W(i))/(e.duration*6e4))))}bn(()=>{ea.view=W(a)});function s(e){let t=e/100*Math.PI*2;return`M60 60 L60 14 A46 46 0 ${+(e>50)} 1 ${60+46*Math.sin(t)} ${60-46*Math.cos(t)} Z`}var c=Ga(),l=R(c);Z(l,21,()=>W(r),X,(e,t)=>{var n=Ta();let r;var i=z(n,!0);V(e=>{Q(n,`href`,`#/quota/`+W(t)),Q(n,`aria-current`,W(a)===W(t)?`page`:void 0),r=ci(n,1,``,null,r,{active:W(a)===W(t)}),J(i,e)},[()=>W(t)===`pace`?`CONSUMPTION PACE`:W(t)===`zone`?`CONSUMPTION ZONE`:W(t)===`fuel`?`FUEL TANK`:W(t).toUpperCase()]),q(e,n)}),j(l);var u=B(l,2),d=e=>{var t=Wa(),n=R(t),r=e=>{q(e,Ea())};Y(n,e=>{$.data.quotaError&&e(r)});var i=B(n,2),c=z(i),l=B(i,2),u=e=>{_a(e,{})},d=e=>{var t=ka(),n=L(t),r=z(n),i=B(n,4),a=e=>{q(e,Da())};Y(i,e=>{$.data.credits.length||e(a)}),Z(B(i,2),17,()=>$.data.credits,X,(e,t)=>{var n=Oa(),r=L(n),i=z(r),a=z(B(r,2),!0);j(n),V(e=>{J(i,`${(W(t).title||`Quota reset`)??``} // ${W(t).status??``}`),J(a,e)},[()=>W(t).expiryKnown?W(t).expires?`EXPIRES `+sa(W(t).expires):`Does not expire`:`Expiry information unavailable`]),q(e,n)}),we(2),j(t),V(()=>J(r,`RESET INVENTORY // ${$.data.creditCount??``} AVAILABLE`)),q(e,t)},f=e=>{var t=Ua(),n=R(t);let r;Z(n,21,()=>$.data.meters,X,(e,t)=>{let n=N(()=>o(W(t))),r=N(()=>W(n)===null?null:W(n)-W(t).used);var i=Va(),c=L(i),l=z(c,!0),u=B(c,2),d=L(u),f=e=>{var n=Aa(),r=R(n),i=z(r),a=z(B(r));V(()=>{J(i,`FREE ${100-W(t).used}%`),J(a,`USED ${W(t).used??``}%`)}),q(e,n)},p=e=>{var n=Aa(),r=R(n),i=z(r),a=z(B(r));V(()=>{J(i,`USED ${W(t).used??``}%`),J(a,`FREE ${100-W(t).used}%`)}),q(e,n)};Y(d,e=>{W(a)===`fuel`?e(f):e(p,-1)}),j(u);var m=B(u,2);let h;var g=L(m),_=e=>{var n=Na(),r=L(n),i=B(L(r)),a=e=>{q(e,ja())},o=e=>{var n=Ma();V(e=>Q(n,`d`,e),[()=>s(W(t).used)]),q(e,n)};Y(i,e=>{W(t).used>=100?e(a):W(t).used>0&&e(o,1)}),j(r),j(n),V(()=>Q(r,`aria-label`,`${W(t).used}% quota used`)),q(e,n)},v=e=>{var r=Fr(),i=R(r),a=e=>{{let r=N(()=>W(t).trail||[]);wa(e,{get used(){return W(t).used},get elapsed(){return W(n)},get trail(){return W(r)}})}},o=e=>{q(e,Pa())};Y(i,e=>{W(n)===null?e(o,-1):e(a)}),q(e,r)},y=e=>{var t=Fr(),n=R(t),i=e=>{var t=Fa(),n=R(t),i=B(L(n),2);let a;j(n);var o=B(n,4),s=L(o),c=z(B(s),!0);j(o),V(e=>{a=ui(i,``,a,{left:`${(W(r)+100)/2}%`}),J(s,`${W(r)>=0?`+`:``}${e??``} PP `),J(c,W(r)>=0?`WITHIN PACE`:`USING FASTER THAN TIME`)},[()=>W(r).toFixed(1)]),q(e,t)},a=e=>{q(e,Ia())};Y(n,e=>{W(r)===null?e(a,-1):e(i)}),q(e,t)},b=e=>{var r=za(),i=R(r),o=L(i);let s;j(i);var c=B(i,2),l=e=>{q(e,La())};Y(c,e=>{W(a)===`fuel`&&e(l)});var u=B(c,2),d=e=>{var t=Ra(),r=R(t),i=z(r),o=B(r,2),s=L(o);let c;j(o),V(e=>{J(i,`RESET CYCLE // ${e??``}% ELAPSED`),c=ui(s,``,c,{width:`${W(a)===`fuel`?100-W(n):W(n)}%`})},[()=>Math.floor(W(n))]),q(e,t)};Y(u,e=>{W(n)!==null&&e(d)}),V(()=>{Q(i,`aria-label`,W(a)===`fuel`?`Fuel remaining`:`Quota used`),Q(i,`aria-valuenow`,W(a)===`fuel`?100-W(t).used:W(t).used),s=ui(o,``,s,{width:`${W(a)===`fuel`?100-W(t).used:W(t).used}%`})}),q(e,r)};Y(g,e=>{W(a)===`pie`?e(_):W(a)===`zone`?e(v,1):W(a)===`pace`?e(y,2):e(b,-1)}),j(m);var x=B(m,2),S=z(x),C=B(x,2),w=e=>{var n=Ba(),r=z(n,!0);V(()=>J(r,W(t).details)),q(e,n)};Y(C,e=>{W(t).details&&e(w)}),j(i),V(e=>{J(l,W(t).name),h=ci(m,1,`meter-graphic`,null,h,{"bar-graphic":W(a)===`bars`||W(a)===`fuel`}),J(S,`RESETS // ${e??``}`)},[()=>sa(W(t).reset)]),q(e,i)}),j(n);var i=B(n,2),c=e=>{q(e,Ha())};Y(i,e=>{$.data.meters.length||e(c)}),V(()=>r=ci(n,1,`quota-grid`,null,r,{radial:W(a)===`pie`,zone:W(a)===`zone`})),q(e,t)};Y(l,e=>{W(a)===`thresholds`?e(u):W(a)===`resets`?e(d,1):e(f,-1)}),we(2),V(e=>J(c,`QUOTA // OBSERVED ${e??``}`),[()=>sa($.data.quotaAt)]),q(e,t)};Y(u,e=>{$.data&&e(d)}),q(e,c),Ke()}var qa=K(`
`),Ja=K(`

`,1);function Ya(e,t){Ge(t,!0);let n=Hi(t,`values`,19,()=>[]),r=Hi(t,`label`,3,`Token activity`),i=Hi(t,`capacity`,3,0),a=N(()=>Math.max(0,...n())),o=N(()=>i()>n().length?[...Array(i()-n().length).fill(0),...n()]:n());var s=Ja(),c=R(s),l=z(c),u=B(c,2);Z(u,21,()=>W(o),X,(e,t)=>{var n=qa();let r;V((e,t)=>{Q(n,`title`,e),r=ui(n,``,r,{height:t})},[()=>W(t).toLocaleString(`en-GB`)+` tokens`,()=>`${100*W(t)/Math.max(1,W(a))}%`]),q(e,n)}),j(u),V((e,t)=>{J(l,`SCALE // 0 — ${e??``} TOKENS`),Q(u,`aria-label`,t)},[()=>W(a).toLocaleString(`en-GB`),()=>`${r()}. Peak ${W(a).toLocaleString(`en-GB`)} tokens.`]),q(e,s),Ke()}var Xa=K(` `,1),Za=K(`
`),Qa=K(`
/
`);function $a(e,t){Ge(t,!0);let n=N(()=>t.lines.filter(e=>e.kind===`addition`).length),r=N(()=>t.lines.filter(e=>e.kind===`removal`).length);var i=Qa(),a=L(i),o=L(a),s=z(o),c=z(B(o,2));j(a),Z(B(a,2),17,()=>t.lines,X,(e,t)=>{var n=Za();let r;var i=L(n),a=e=>{var n=Xa(),r=R(n),i=z(r,!0),a=z(B(r),!0);V(()=>{J(i,W(t).old||``),J(a,W(t).new||``)}),q(e,n)};Y(i,e=>{W(t).kind!==`heading`&&W(t).kind!==`metadata`&&e(a)});var o=z(B(i),!0);j(n),V(()=>{r=ci(n,1,`diff-line svelte-x4gtb6`,null,r,{added:W(t).kind===`addition`,removed:W(t).kind===`removal`,heading:W(t).kind===`heading`}),J(o,W(t).text)}),q(e,n)}),j(i),V(()=>{Q(a,`aria-label`,`${W(n)} lines added, ${W(r)} lines removed`),J(s,`+${W(n)??``}`),J(c,`−${W(r)??``}`)}),q(e,i),Ke()}var eo=K(`

`),to=K(`

Send this saved prompt now? This starts Codex work and consumes quota.

`,1),no=K(``),ro=K(`

 

In memory only. Follows session model, reasoning and speed. Closing - Codexometer cancels pending work.

`),io=K(`

CURRENT PROFILE

MODEL / REASONING LEVEL / SPEED

 

PROPOSED PROFILE

MODEL / REASONING LEVEL / SPEED

 

Applied settings remain after Codexometer closes.

`,1),ao=K(`

`),oo=K(`

 
`,1),so=K(`

Command unavailable from this observation. Open Codex to inspect the - request.

`),co=K(`

Session controls temporarily unavailable. Check Codex for current state.

`),lo=K(`

Checking session controls…

`),uo=K(`

`),fo=K(`
About browser controls

Controls require a supported live request from a connected shared - app-server session. Local observations alone cannot provide them.

`),po=K(` `,1),mo=K(` `),ho=K(`Grants permission beyond this one action. Check the scope - carefully.`),go=K(``),_o=K(``),vo=K(``),yo=K(``),bo=K(`

Not before this time; when quota and idle allow.

`),xo=K(`

`,1),So=K(`

The existing trigger remains active until saved or deleted. If - it changes or sends while you edit, saving is blocked.

`),Co=K(` `,1),wo=K(``),To=K(``),Eo=K(``),Do=K(` `,1),Oo=K(`
  • `),ko=K(`
    View fixed choices

    Type one of these choices exactly. Your answer stays masked.

      `),Ao=K(` `,1),jo=K(`

      `),Mo=K(`

      `,1),No=K(` `,1),Po=K(``),Fo=K(`

      `,1),Io=K(`

      `);function Lo(e,t){Ge(t,!0);let n=[],r=Hi(t,`observedCommand`,3,``),i=Hi(t,`observedFileChanges`,19,()=>[]),a=Hi(t,`review`,3,``),o=Hi(t,`suspended`,3,!1),s=Hi(t,`openScheduler`,3,!1),c=Hi(t,`onProtectedChange`,3,e=>{}),l=F(null),u=F(Zt([])),d=F(null),f=F(``),p=F(0),m=F(Zt(Date.now())),h=F(!1),g=F(``),_=F(!1),v=F(!1),y=F(``),b=F(!1),x=F(``),S=F(!1),C=F(``),w=F(`now`),T=F(60),ee=F(``),te=F(!1),E=F(``),D=F(Zt([])),ne=N(()=>W(D).find(e=>e.status!==`sent`)),re=F(void 0);function ie(e){if(W(b)){dr().then(()=>W(re)?.focus());return}if(e.status!==`pending`||W(h)||!W(l)?.id||W(l).kind!==`prompt`||W(l).questions?.length)return;I(b,!0),I(x,e.id,!0),dr().then(()=>W(re)?.focus()),I(u,[e.text],!0),I(w,e.trigger===`quota`?`quota`:`at`,!0),I(f,``),I(C,``);let t=new Date(e.at);e.trigger!==`quota`&&I(ee,new Date(t.getTime()-t.getTimezoneOffset()*6e4).toISOString().slice(0,16),!0)}bn(()=>{s()&&W(ne)&&!W(S)&&W(l)?.id&&W(l).kind===`prompt`&&!W(l).questions?.length&&(I(S,!0),ie(W(ne)))});async function ae(){if(!a())try{I(D,await da(`schedules`,{session:t.session},ye.signal),!0),W(b)&&!W(D).some(e=>e.id===W(x)&&e.status===`pending`)&&I(f,``),W(C)&&!W(D).some(e=>e.id===W(C)&&e.status===`pending`)&&(I(f,``),I(C,``))}catch{}}async function oe(e){try{I(D,await da(`schedules`,{session:t.session,cancelId:e},ye.signal),!0),I(b,!1),I(f,``),I(C,``),I(u,[],!0)}catch(e){I(g,e instanceof Error?e.message:`Unable to cancel trigger.`,!0)}}bn(()=>(c()(W(h)||W(u).some(e=>e.length>0)),()=>c()(!1)));let se=N(()=>$.data?.sessions.find(e=>e.id===t.session)?.status),ce=N(()=>!$.connected||!!$.data?.sessionsError),le=N(()=>a()!==`profile`&&W(l)?.kind===`prompt`&&!W(l).questions?.length&&!!$.data?.profiles?.some(e=>e.session===t.session&&e.pending)),ue=N(()=>!W(ce)&&!W(v)&&!!W(l)?.id&&W(l).kind===`approval`&&W(y)!==W(l).id),de=N(()=>W(ue)?W(l).command:r()),fe=N(()=>!!W(f)&&W(m)W(l)?.questions?.length?W(l).questions:[{text:`Follow-up message`,secret:!1,freeText:!0,options:[]}]),me=N(()=>W(l)?.kind===`approval`||W(l)?.kind===`profile`?W(d)!==null:W(u).length===W(pe).length&&W(u).every((e,t)=>e.trim().length>0&&(W(pe)[t].freeText||W(pe)[t].options?.includes(e)))),he=N(()=>W(l)?.kind===`prompt`&&!W(l).questions?.length&&W(w)!==`now`),O=N(()=>W(w)===`delay`?W(m)+W(T)*6e4:Date.parse(W(ee))),ge=N(()=>{if(!W(he))return``;if(W(b)&&!W(D).some(e=>e.id===W(x)&&e.status===`pending`))return`Trigger changed, removed or already sent. Go back and reopen it; your draft has not been saved.`;if(!W(u)[0]?.trim())return`Enter a message to schedule.`;if(W(w)===`quota`)return``;if(!Number.isFinite(W(O))||W(O)<=W(m))return`Choose a future date/time or a delay greater than zero.`;let e=new Date(W(m));return e.setFullYear(e.getFullYear()+1),W(O)>=e.getTime()?`Choose a time within one year.`:``});function _e(e){return new Date(e).toLocaleString([],{timeZoneName:`short`})}function ve(e){if(e.status!==`pending`)return e.status;if(e.trigger===`at`&&Date.parse(e.at)>W(m))return`Due in `+Math.ceil((Date.parse(e.at)-W(m))/6e4)+` min`;let t=W(ce)?`waiting for a fresh connection`:e.waitingReason||(e.canSend?`ready; awaiting dispatch`:`waiting for quota and an eligible idle session`);return(e.trigger===`at`?`Time reached — `:``)+t}bn(()=>{(W(ce)||o()||W(le)||W(m)>=W(p))&&I(f,``)});let ye=new AbortController;Ui(()=>{let e=e=>{e.ctrlKey&&e.key.toLowerCase()===`s`&&!a()&&W(ne)&&(e.preventDefault(),ie(W(ne)))};window.addEventListener(`keydown`,e);let n,r=setInterval(()=>{I(m,Date.now(),!0)},1e3);async function i(){await ae();try{let e=await da(`offer`,{session:t.session,...a()?{review:a()}:{}},ye.signal);if(ye.signal.aborted)return;I(v,!1),W(l)?.id!==e.id&&(I(f,``),I(d,null),I(u,Array(Math.max(1,e.questions?.length||0)).fill(``),!0)),I(l,e,!0),W(_)&&e.id&&e.id!==W(y)&&(I(g,``),I(_,!1))}catch{ye.signal.aborted||(I(l,null),I(v,!0),I(f,``))}finally{ye.signal.aborted||(n=setTimeout(i,2e3))}}return i(),()=>{window.removeEventListener(`keydown`,e),ye.abort(),clearTimeout(n),clearInterval(r)}});async function be(e){if(!W(l)?.id||W(h)||W(ce)||o()||W(le)||(e?!e.canSend:!W(me)||W(ge)))return;let n=W(l).id;I(h,!0),I(g,``),I(_,!1),I(f,``),I(C,e?.id||``,!0);try{let r=!e&&W(l).kind===`prompt`&&!W(l).questions?.length&&W(w)!==`now`?{trigger:W(w)===`quota`?`quota`:`at`,zone:Intl.DateTimeFormat().resolvedOptions().timeZone,...W(w)===`quota`?{}:{at:(W(w)===`delay`?new Date(Date.now()+W(T)*6e4):new Date(W(ee))).toISOString()}}:void 0;I(te,!!r),I(E,r?r.trigger===`quota`?`After fresh quota becomes available, when the session is idle`:_e(r.at)+` — not before this time; when quota and idle allow`:``,!0);let i=await da(`prepare`,{session:t.session,...a()?{review:a()}:{},offer:n,...e?{sendId:e.id}:{},...r?{schedule:r}:{},...r&&W(b)?{editId:W(x)}:{},...W(l).kind===`approval`||W(l).kind===`profile`?{choice:W(d)}:{answers:e?[e.text]:[...W(u)]}},ye.signal);W(l)?.id===n&&!ye.signal.aborted&&!W(ce)&&!o()&&!W(le)&&(I(f,i.confirmation,!0),I(p,Date.parse(i.expires),!0))}catch(e){ye.signal.aborted||I(g,e instanceof Error?e.message:`Unable to prepare action.`,!0)}finally{I(h,!1)}}async function k(){if(!W(l)?.id||W(h)||o()||W(le)||!W(fe))return;let e=W(l).id,n=W(l).kind,r=W(f);I(f,``),I(h,!0),I(y,e,!0);try{await da(`commit`,{session:t.session,...a()?{review:a()}:{},offer:e,confirmation:r},ye.signal),I(_,!0),I(b,!1),W(te)&&(I(y,``),await ae()),I(g,W(te)?`Trigger set. Closing Codexometer cancels it.`:n===`profile`?`Profile decision completed.`:n===`approval`?`Decision sent.`:`Text sent.`,!0)}catch(e){I(g,e instanceof la?e.message:`Outcome uncertain. Check Codex before taking another action; nothing was retried.`,!0)}finally{I(h,!1),I(C,``),await ae(),I(u,[],!0),I(d,null)}}var xe=po(),A=R(xe),Se=e=>{var t=Fr();Z(R(t),17,()=>W(D).filter(e=>e.status!==`sent`),e=>e.id,(e,t)=>{var n=ro(),r=L(n),i=z(r,!0),a=B(r,2),s=z(a),c=B(a,2),u=z(c,!0),d=B(c,2),p=z(d,!0),m=B(d,2),_=z(m,!0),v=B(m,2),y=e=>{var t=eo(),n=z(t,!0);V(()=>J(n,W(g))),q(e,t)};Y(v,e=>{W(g)&&e(y)});var b=B(v,4),x=B(b,2),S=B(x,2),w=e=>{var n=to(),r=B(R(n),2),i=B(r,2);V(()=>r.disabled=W(h)||W(ce)||!W(t).canSend),G(`click`,r,k),G(`click`,i,()=>{I(f,``),I(C,``)}),q(e,n)},T=e=>{var n=no();V(()=>n.disabled=W(h)||W(ce)||!W(t).canSend||o()),G(`click`,n,()=>be(W(t))),q(e,n)};Y(S,e=>{W(fe)&&W(C)===W(t).id?e(w):e(T,-1)}),j(n),V((e,n)=>{J(i,W(t).status===`uncertain`?`CHECK TRIGGER — outcome uncertain; not retried`:`SCHEDULED FOLLOW-UP`),J(s,`TARGET // ${W(t).session??``}`),J(u,e),J(p,n),J(_,W(t).text),b.disabled=W(h)||!W(l)?.id||W(l).kind!==`prompt`||!!W(l).questions?.length||W(t).status!==`pending`,x.disabled=W(h)||W(t).status===`sending`},[()=>W(t).trigger===`quota`?`After confirmed quota recovery, when idle`:`Not before ${_e(W(t).at)}`,()=>ve(W(t))]),G(`click`,b,()=>ie(W(t))),G(`click`,x,()=>oe(W(t).id)),q(e,n)}),q(e,t)},Ce=N(()=>!a()&&W(l)?.kind!==`approval`&&!W(l)?.questions?.length&&![`APPROVAL NEEDED`,`INPUT NEEDED`].includes(W(se)||``));Y(A,e=>{W(Ce)&&e(Se)});var Te=B(A,2),Ee=e=>{var r=Io(),o=L(r),s=z(o,!0),c=B(o,2),x=e=>{var t=io(),n=R(t),r=z(n),i=B(n,6),a=z(i,!0),o=z(B(i,6),!0);we(2),V(()=>{J(r,`Your ${W(l).profile.threshold??``}% quota threshold has been reached. Review - the profile for subsequent turns.`),J(a,W(l).profile.current),J(o,W(l).profile.proposed)}),q(e,t)};Y(c,e=>{W(l)?.profile&&!W(ce)&&!W(v)&&e(x)});var S=B(c,2),C=e=>{var t=ao();let n;var r=z(t,!0);V(()=>{n=ci(t,1,`svelte-1oupzfc`,null,n,{notice:!W(_),sent:W(_)}),J(r,W(g))}),q(e,t)};Y(S,e=>{W(g)&&e(C)});var D=B(S,2),ne=e=>{$a(e,{get lines(){return W(l).fileChanges}})},ie=e=>{$a(e,{get lines(){return i()}})},ae=e=>{var t=oo(),n=R(t),r=z(n,!0),i=z(B(n,2),!0);V(()=>{J(r,W(ue)?`EXACT COMMAND`:`LAST OBSERVED COMMAND`),J(i,W(de))}),q(e,t)},oe=e=>{q(e,so())};Y(D,e=>{W(l)?.fileChanges?.length&&!W(ce)&&!W(v)?e(ne):i().length?e(ie,1):W(de)?e(ae,2):a()!==`profile`&&W(se)===`APPROVAL NEEDED`&&!W(_)&&e(oe,3)});var le=B(D,2),he=e=>{q(e,co())},ve=e=>{q(e,lo())},ye=e=>{var t=po(),n=R(t),r=e=>{var t=uo(),n=z(t,!0);V(e=>J(n,e),[()=>W(se)===`WORKING`?`Codex is working — nothing to respond to.`:[`APPROVAL NEEDED`,`INPUT NEEDED`].includes(W(se)||``)?`Respond in Codex for this request.`:`Nothing needs a response right now.`]),q(e,t)};Y(n,e=>{(W(se)===`WORKING`||!W(_)&&!W(h))&&e(r)});var i=B(n,2),a=e=>{q(e,fo())},o=N(()=>[`APPROVAL NEEDED`,`INPUT NEEDED`].includes(W(se)||``)&&!W(_));Y(i,e=>{W(o)&&e(a)}),q(e,t)},xe=e=>{var r=Fo(),i=R(r),o=z(i),s=B(i,2),c=L(s),g=z(c,!0),_=B(c,2),v=e=>{var r=Fr();Z(R(r),17,()=>W(l).choices||[],X,(e,r,i)=>{var o=go(),s=L(o);xi(s),s.value=s.__value=i;var c=B(s),l=B(c),u=e=>{var t=mo(),n=z(t,!0);V(()=>J(n,W(r).detail)),q(e,t)};Y(l,e=>{W(r).detail&&e(u)});var f=B(l,2),p=e=>{q(e,ho())};Y(f,e=>{W(r).persistent&&e(p)}),j(o),V(()=>{Q(s,`name`,`decision-`+t.session+`-`+a()),J(c,` ${W(r).label??``} `)}),Di(n,[],s,()=>W(d),e=>I(d,e)),q(e,o)}),q(e,r)},y=e=>{var t=po(),n=R(t),r=e=>{var t=Co(),n=R(t),r=B(L(n)),i=L(r),a=e=>{var t=_o();t.value=t.__value=`now`,q(e,t)};Y(i,e=>{W(b)||e(a)});var o=B(i);o.value=o.__value=`quota`;var s=B(o);s.value=s.__value=`delay`;var c=B(s);c.value=c.__value=`at`,j(r),mi(r),j(n);var l=B(n,2),u=e=>{var t=vo(),n=B(L(t));xi(n),j(t),Ti(n,()=>W(T),e=>I(T,e)),q(e,t)};Y(l,e=>{W(w)===`delay`&&e(u)});var d=B(l,2),f=e=>{var t=yo(),n=B(L(t));xi(n),j(t),Ti(n,()=>W(ee),e=>I(ee,e)),q(e,t)};Y(d,e=>{W(w)===`at`&&e(f)});var p=B(d,2),m=e=>{var t=xo(),n=R(t),r=z(n),i=B(n,2),a=e=>{q(e,bo())};Y(i,e=>{W(w)!==`quota`&&e(a)});var o=B(i,2);o.textContent=`Timezone: ${Intl.DateTimeFormat().resolvedOptions().timeZone??``}. - One pending request per session; saving replaces it. Memory - only; Codexometer must stay running. Uses session model, - reasoning and speed.`,V(e=>J(r,`Will send: ${e??``}`),[()=>W(w)===`quota`?`when quota is available and this session is idle`:Number.isFinite(W(O))?_e(W(O)):`choose a date and time`]),q(e,t)};Y(p,e=>{W(w)!==`now`&&e(m)});var h=B(p,2),g=e=>{q(e,So())};Y(h,e=>{W(b)&&e(g)}),hi(r,()=>W(w),e=>I(w,e)),q(e,t)};Y(n,e=>{W(l).questions?.length||e(r)}),Z(B(n,2),17,()=>W(pe),X,(e,t,n)=>{var r=Ao(),i=R(r),a=L(i),o=B(a),s=e=>{var t=wo();xi(t),Ti(t,()=>W(u)[n],e=>W(u)[n]=e),q(e,t)},c=e=>{var r=Eo(),i=L(r);i.value=i.__value=``,Z(B(i),17,()=>W(t).options||[],X,(e,t)=>{var n=To(),r=z(n,!0),i={};V(()=>{J(r,W(t)),i!==(i=W(t))&&(n.value=(n.__value=i)??``)}),q(e,n)}),j(r),mi(r),hi(r,()=>W(u)[n],e=>W(u)[n]=e),q(e,r)},l=e=>{var r=Do(),i=R(r);rt(i),Pi(i,e=>I(re,e),()=>W(re));var a=B(i,2),o=e=>{var n=uo(),r=z(n);V(e=>J(r,`Suggested answers: ${e??``}`),[()=>W(t).options.join(` · `)]),q(e,n)};Y(a,e=>{W(t).options?.length&&e(o)}),Ti(i,()=>W(u)[n],e=>W(u)[n]=e),q(e,r)};Y(o,e=>{W(t).secret?e(s):W(t).freeText?e(l,-1):e(c,1)}),j(i);var d=B(i,2),f=e=>{var n=ko(),r=B(L(n),4);Z(r,21,()=>W(t).options||[],X,(e,t)=>{var n=Oo(),r=z(n,!0);V(()=>J(r,W(t))),q(e,n)}),j(r),j(n),q(e,n)};Y(d,e=>{W(t).secret&&!W(t).freeText&&e(f)}),V(()=>J(a,`${W(t).text??``} `)),q(e,r)}),q(e,t)};Y(_,e=>{W(l).kind===`approval`||W(l).kind===`profile`?e(v):e(y,-1)}),j(s);var x=B(s,2),S=e=>{var t=Mo(),n=R(t),r=e=>{var t=jo(),n=z(t);V(()=>J(n,`TRIGGER // ${W(E)??``}`)),q(e,t)};Y(n,e=>{W(te)&&e(r)});var i=B(n,2),a=z(i),o=B(i,2),s=z(o),c=B(o,2);V(e=>{J(a,`Check the target and ${W(l).kind===`profile`?`profile above`:W(l).kind===`approval`?W(l).fileChanges?.length?`proposed file changes above`:`exact command and permission scope`:`message above`}. ${W(te)?`This schedules a follow-up that will send automatically when eligible`:`This will send to Codex`}; it may start work using your quota. - Confirmation expires in ${e??``}s.`),o.disabled=W(h)||W(ce),J(s,`CONFIRM ${(W(l).kind===`approval`||W(l).kind===`profile`?W(l).choices?.[W(d)]?.label:W(te)?W(b)?`SAVE CHANGES`:`SCHEDULE`:`SEND`)??``}`)},[()=>Math.max(0,Math.ceil((W(p)-W(m))/1e3))]),G(`click`,o,k),G(`click`,c,()=>{I(f,``)}),q(e,t)},C=e=>{var t=No(),n=R(t),r=e=>{var t=eo(),n=z(t,!0);V(()=>J(n,W(ge))),q(e,t)};Y(n,e=>{W(ge)&&e(r)});var i=B(n,2),a=z(i,!0);V(()=>{i.disabled=W(h)||W(ce)||!W(me)||!!W(ge),J(a,W(h)?`SENDING…`:W(b)?`REVIEW CHANGES`:`REVIEW BEFORE SENDING`)}),G(`click`,i,()=>be()),q(e,t)};Y(x,e=>{W(fe)?e(S):e(C,-1)});var D=B(x,2),ne=e=>{var t=Po();G(`click`,t,()=>{I(b,!1),I(f,``),I(u,[],!0)}),q(e,t)};Y(D,e=>{W(b)&&e(ne)}),V(()=>{J(o,`TARGET // ${W(l).thread??``} // ${(W(l).directory||`Directory unavailable`)??``}`),s.disabled=W(h)||W(fe)||W(ce),J(g,W(l).kind===`approval`||W(l).kind===`profile`?`Choose a decision`:W(b)?`EDIT SCHEDULED FOLLOW-UP`:`Reply to this session`)}),q(e,r)};Y(le,e=>{W(ce)||W(v)?e(he):W(l)?W(l).id?W(y)!==W(l).id&&e(xe,3):e(ye,2):e(ve,1)}),j(r),V(()=>J(s,a()===`profile`?`QUOTA THRESHOLD`:`SESSION CONTROL // EXPERIMENTAL`)),q(e,r)};Y(Te,e=>{!W(le)&&(!W(ne)||W(b)||a()||W(l)?.kind!==`prompt`||W(l).questions?.length)&&e(Ee)}),q(e,xe),Ke()}wr([`click`]);var Ro=K(`
      `);function zo(e,t){Ge(t,!0);let n=Hi(t,`active`,3,!1),r=N(()=>t.session.text||t.session.contextKind===`LAST ACTIVITY`&&t.session.workingCommand||``),i=N(()=>[`LAST REPLY`,`LAST ACTIVITY`].includes(t.session.contextKind)&&!!W(r).trim()),a=F(!1),o=F(``),s=F(!1);bn(()=>{if(!W(s))return;let e=setTimeout(()=>{I(s,!1)},150);return()=>clearTimeout(e)}),bn(()=>{W(r),t.session.status,I(o,``)});async function c(){if(!W(i)||W(a))return;let e=W(r);I(a,!0),I(s,!0),I(o,``);try{await navigator.clipboard.writeText(e),W(r)===e&&I(o,`Copied.`)}catch{W(r)===e&&I(o,`Clipboard unavailable. Select the visible text and copy it manually.`)}finally{I(a,!1)}}function l(e){!n()||!W(i)||e.defaultPrevented||e.repeat||e.ctrlKey||e.metaKey||e.altKey||e.key.toLowerCase()!==`c`||e.target instanceof Element&&e.target.closest(`input, textarea, select, [contenteditable]`)||(e.preventDefault(),c())}var u=Fr();Cr(`keydown`,en,l);var d=R(u),f=e=>{var t=Ro(),n=L(t),r=z(n,!0),i=B(n,2);let l;j(t),V(()=>{J(r,W(o)),i.disabled=W(a),l=ci(i,1,`svelte-543j00`,null,l,{flashed:W(s)})}),G(`click`,i,c),q(e,t)};Y(d,e=>{W(i)&&e(f)}),q(e,u),Ke()}wr([`click`]);var Bo=K(`

       
      `,1),Vo=K(`

      LATEST GUIDANCE

       
      `,1),Ho=K(`

      CONTEXT

       

      `,1),Uo=K(`

      `),Wo=K(`

      `),Go=K(`
       
      `),Ko=K(`

       
      `,1),qo=K(`
       
      `),Jo=K(`

      Command unavailable from this observation. Open Codex to inspect the - request.

      `),Yo=K(`

      `,1),Xo=K(`

      `),Zo=K(` `,1),Qo=K(`
      `),$o=K(`

      Session connection or refresh unavailable. Context and telemetry may be - stale.

      `),es=K(`

      Some quota profile checks are unavailable. Only sessions with freshly - verified quota and settings can be updated; previous outcome notices remain - visible.

      `),ts=K(` `),ns=K(` `),rs=K(` `),is=K(``),as=K(`

      Read only — reply or approve in Codex.

      `),os=K(`

      `),ss=K(`QUOTA THRESHOLD // REVIEW PROFILE ↗`),cs=K(`
      `),ls=K(`
      `),us=K(`

      This session is no longer in the current observation. Return to sessions.

      `),ds=K(`

      `),fs=K(``),ps=K(`

      `),ms=K(` `),hs=K(`

      `),gs=K(`QUOTA THRESHOLD // REVIEW PROFILE ↗`),_s=K(` `,1),vs=K(`

      `),ys=K(`

      TOKEN ACTIVITY // 30 SECOND SAMPLES

      `),bs=K(`

      TOKENS

      FULL DETAIL →
      `),xs=K(`

      No locally observed sessions yet. Keep Codex running alongside - Codexometer.

      `),Ss=K(`

      ↑ ↓ SELECT SESSION // ← LESS DETAIL // → MORE DETAIL // ESC BACK

      `,1),Cs=K(`

      SESSION TOTALS

      `,1);function ws(e,t){Ge(t,!0);let n=(e,t=f,n,r)=>{let i=ht(()=>g(n?.(),!0)),a=ht(()=>g(r?.(),!1));var o=Zo(),s=R(o),c=e=>{var n=Bo(),r=R(n),i=z(r,!0),a=z(B(r,2),!0);V(()=>{J(i,t().contextKind===`LAST REPLY`&&!t().streaming?`TASK`:`CURRENT TASK`),J(a,t().currentTask)}),q(e,n)};Y(s,e=>{t().currentTask&&e(c)});var l=B(s,2),u=e=>{var n=Vo(),r=z(B(R(n),2),!0);V(()=>J(r,t().latestGuidance)),q(e,n)};Y(l,e=>{t().latestGuidance&&e(u)});var d=B(l,2),p=e=>{var n=Ho(),r=z(B(R(n),2),!0);we(2),V(()=>J(r,t().approvalContext)),q(e,n)};Y(d,e=>{W(a)&&t().contextKind===`APPROVAL REQUEST`&&t().approvalContext&&e(p)});var m=B(d,2),_=e=>{var n=Uo();let r;var i=z(n,!0);V(e=>{r=ci(n,1,`attention-note`,null,r,{inferred:t().status===`CHECK SESSION`}),J(i,e)},[()=>C(t())]),q(e,n)},v=N(()=>C(t())&&(!W(a)||W(h)||t().status===`CHECK SESSION`));Y(m,e=>{W(v)&&e(_)});var y=B(m,2),b=e=>{var n=Wo(),r=z(n,!0);V(e=>J(r,e),[()=>w(t())]),q(e,n)};Y(y,e=>{W(i)&&e(b)});var x=B(y,2),S=e=>{var n=Go(),r=z(n,!0);V(()=>J(r,t().text||`No session context available.`)),q(e,n)};Y(x,e=>{(t().text||!t().workingCommand)&&e(S)});var T=B(x,2),ee=e=>{var n=Ko(),r=B(R(n),2),i=L(r),a=B(i),o=e=>{var n=Pr();V(()=>J(n,`// +${(t().runningCommands||0)-+(t().commandStatus===`running`)} RUNNING`)),q(e,n)};Y(a,e=>{!W(h)&&(t().runningCommands||0)>+(t().commandStatus===`running`)&&e(o)});var s=B(a,2),c=e=>{q(e,Pr(`+`))};Y(s,e=>{t().runningLimited&&e(c)}),j(r);var l=z(B(r,2),!0);V(e=>{J(i,`COMMAND // ${e??``} `),J(l,t().workingCommand)},[()=>W(h)?`UNKNOWN`:(t().commandStatus||`unknown`).toUpperCase()]),q(e,n)};Y(T,e=>{t().workingCommand&&e(ee)});var te=B(T,2),E=e=>{$a(e,{get lines(){return t().fileChanges}})},D=e=>{var n=Yo(),r=B(R(n),2),i=z(r,!0),a=B(r,2),o=e=>{var n=qo(),r=z(n,!0);V(()=>J(r,t().command)),q(e,n)},s=e=>{q(e,Jo())};Y(a,e=>{t().command?e(o):e(s,-1)}),V(()=>J(i,W(h)||t().status!==`APPROVAL NEEDED`?`LAST OBSERVED COMMAND`:$.data?.control?`COMMAND REQUEST`:`COMMAND TO APPROVE IN CODEX`)),q(e,n)};Y(te,e=>{(!W(a)||!$.data?.control)&&t().fileChanges?.length?e(E):(!W(a)||!$.data?.control)&&(t().command||t().status===`APPROVAL NEEDED`)&&e(D,1)});var ne=B(te,2),re=e=>{var n=Xo(),r=z(n);V(()=>J(r,`CONTEXT SOURCE // ${(t().source||`LOCAL`)??``} // OBSERVATION`)),q(e,n)};Y(ne,e=>{W(a)||e(re)}),q(e,o)},r=Hi(t,`params`,19,()=>({})),i=N(()=>$.data?.sessions||[]),a=N(()=>$.data?.control&&$.data.profiles||[]),o=N(()=>($.data?.triggers||[]).filter(e=>W(i).some(t=>t.id===e.session)));function s(e,t){if(r().id&&r().id!==t&&W(l)){e.preventDefault();return}b(t)}function c(e){return[`TURN COMPLETE`,`IDLE`,`ACTIVE`].includes(e.status)&&W(o).some(t=>t.session===e.id)?`TRIGGER SET`:e.status}let l=F(!1);function u(e,t){if(r().id&&r().id!==t&&W(l)){e.preventDefault();return}b(t)}bn(()=>{let e=r().id;e&&mr(()=>{ea.selected=e,aa(e,2)})});let d=N(()=>W(i).find(e=>e.id===r().id)),p=N(()=>new URLSearchParams(Ki.querystring).get(`review`)===`profile`&&W(a).some(e=>e.session===r().id&&e.pending)),m=N(()=>W(i).some(e=>e.id===ea.selected)?ea.selected:W(i)[0]?.id),h=N(()=>!$.connected||!!$.data?.sessionsError),v=N(()=>[[`OBSERVED TOKENS`,oa(W(i).reduce((e,t)=>e+t.tokens,0))],[`LISTED SESSIONS`,oa(W(i).length)],...[[`WORKING`,`WORKING`],[`AWAITING APPROVAL`,`APPROVAL NEEDED`],[`AWAITING INPUT`,`INPUT NEEDED`],[`CHECK · INFERRED`,`CHECK SESSION`]].map(([e,t])=>[e,W(h)?`—`:oa(W(i).filter(e=>e.status===t).length)])]),y=N(()=>W(i).filter(e=>[`INPUT NEEDED`,`APPROVAL NEEDED`,`CHECK SESSION`].includes(e.status)));function b(e){ea.selected=e,dr().then(()=>document.querySelector(`.session-row.selected`)?.scrollIntoView({block:`nearest`}))}function x(e,t){if(b(e),r().id){t<0&&(aa(e,2),location.hash=`/sessions`);return}let n=ra(e)+t;n>2?location.hash=`/sessions/`+encodeURIComponent(e):aa(e,n)}function S(e){let t=e.target;if(!(e.altKey||e.ctrlKey||e.metaKey||e.shiftKey||t.closest(`input, textarea, select, [contenteditable="true"]`))){if(e.key===`Escape`&&r().id)e.preventDefault(),x(r().id,-1);else if([`ArrowLeft`,`ArrowRight`].includes(e.key)&&(r().id||W(m)))e.preventDefault(),x(r().id||W(m),e.key===`ArrowLeft`?-1:1);else if(!r().id&&[`ArrowUp`,`ArrowDown`].includes(e.key)&&W(i).length){e.preventDefault();let t=W(i).findIndex(e=>e.id===W(m));b(W(i)[Math.max(0,Math.min(W(i).length-1,t+(e.key===`ArrowUp`?-1:1)))].id)}}}function C(e){return W(h)?`Last observation only — refresh unavailable. Check Codex for current state.`:e.status===`CHECK SESSION`?`INFERRED INACTIVITY — a quiet session, not a confirmed input or approval request. Local tools may still be running.`:$.data?.control&&[`APPROVAL NEEDED`,`INPUT NEEDED`].includes(e.status)?`Open full detail for supported live controls; otherwise reply in Codex.`:e.status===`APPROVAL NEEDED`?`OBSERVED APPROVAL SIGNAL — approve or decline in Codex.`:e.status===`INPUT NEEDED`?`OBSERVED INPUT SIGNAL — reply in Codex.`:e.status===`TURN COMPLETE`?`OBSERVED TURN COMPLETION — informational, not an approval request.`:``}function w(e){return e.streaming&&e.contextKind===`LAST REPLY`?`REPLY // STREAMING`:e.contextKind||`LAST ACTIVITY`}var T=Cs();Cr(`keydown`,en,S);var ee=R(T),te=z(B(L(ee),2));j(ee);var E=B(ee,2);let D;Z(E,21,()=>W(v),X,(e,t)=>{var n=N(()=>_(W(t),2));let r=()=>W(n)[0],i=()=>W(n)[1];var a=Qo(),o=L(a),s=z(o,!0),c=z(B(o,2),!0);j(a),V(()=>{J(s,r()),J(c,i())}),q(e,a)}),j(E);var ne=B(E,2),re=z(ne),ie=B(ne,2),ae=e=>{q(e,$o())};Y(ie,e=>{W(h)&&e(ae)});var oe=B(ie,2),se=e=>{q(e,es())};Y(oe,e=>{$.data?.profileError&&e(se)});var ce=B(oe,2),le=e=>{var t=is(),n=L(t);Z(n,17,()=>W(y),X,(e,t)=>{var n=ts();let r;var i=z(n);V((e,a)=>{r=ci(n,1,`button`,null,r,{approval:W(t).status===`APPROVAL NEEDED`}),Q(n,`href`,e),J(i,`${W(t).status??``} - ${a??``} // ${(W(t).name||W(t).directory||W(t).id)??``}`)},[()=>`#/sessions/`+encodeURIComponent(W(t).id),()=>Array.from(W(t).id).slice(-5).join(``).toUpperCase()]),G(`click`,n,()=>b(W(t).id)),q(e,n)});var c=B(n,2);Z(c,17,()=>W(a).filter(e=>e.pending&&W(i).some(t=>t.id===e.session)),X,(e,t)=>{var n=ns(),a=z(n);V((e,i,o)=>{Q(n,`title`,r().id&&r().id!==W(t).session&&W(l)?`Finish sending or clear your current draft before switching sessions.`:`Review this session’s quota threshold`),Q(n,`href`,e),J(a,`QUOTA THRESHOLD ${i??``} // ${o??``}`)},[()=>`#/sessions/`+encodeURIComponent(W(t).session)+`?review=profile`,()=>Array.from(W(t).session).slice(-5).join(``).toUpperCase(),()=>W(i).find(e=>e.id===W(t).session)?.name||W(i).find(e=>e.id===W(t).session)?.directory||W(t).session]),G(`click`,n,e=>u(e,W(t).session)),q(e,n)}),Z(B(c,2),17,()=>W(o),X,(e,t)=>{var n=rs(),r=z(n);V((e,t,i)=>{Q(n,`href`,e),J(r,`TRIGGER SET ${t??``} // ${i??``}`)},[()=>`#/sessions/`+encodeURIComponent(W(t).session),()=>Array.from(W(t).session).slice(-5).join(``).toUpperCase(),()=>W(i).find(e=>e.id===W(t).session)?.name||W(i).find(e=>e.id===W(t).session)?.directory]),G(`click`,n,e=>s(e,W(t).session)),q(e,n)}),j(t),q(e,t)},ue=N(()=>(W(y).length||W(a).some(e=>e.pending)||W(o).length)&&!W(h));Y(ce,e=>{W(ue)&&e(le)});var de=B(ce,2),fe=e=>{var t=Fr(),i=R(t),o=e=>{var t=ls(),i=L(t),o=L(i),s=L(o);let u;var f=B(s);j(o);var m=B(o,2);j(i);var g=B(i,2),_=e=>{var t=Xo(),n=z(t,!0);V(()=>J(n,W(d).directory)),q(e,t)};Y(g,e=>{W(d).name&&W(d).directory&&e(_)});var v=B(g,2),y=z(v),x=B(v,2);let S;var C=L(x),w=L(C);n(w,()=>W(d),()=>!0,()=>!0),j(C);var T=B(C,2),ee=e=>{var t=Fr();qr(R(t),()=>W(d).id,e=>{{let t=N(()=>new URLSearchParams(Ki.querystring).get(`schedule`)===`edit`),n=N(()=>W(d).fileChanges||[]);Lo(e,{get session(){return W(d).id},get openScheduler(){return W(t)},get observedCommand(){return W(d).command},get observedFileChanges(){return W(n)},get suspended(){return W(p)},onProtectedChange:e=>{I(l,e,!0)}})}}),q(e,t)},te=e=>{q(e,as())};Y(T,e=>{$.data?.control?e(ee):e(te,-1)}),j(x);var E=B(x,2);Z(E,17,()=>W(a).filter(e=>e.session===W(d).id),e=>e.session,(e,t)=>{var n=cs(),r=L(n),i=e=>{var n=os(),r=z(n,!0);V(()=>J(r,W(t).notice)),q(e,n)};Y(r,e=>{W(t).notice&&e(i)});var a=B(r,2),o=e=>{Lo(e,{get session(){return W(d).id},review:`profile`})},s=e=>{var t=ss();V(e=>Q(t,`href`,e),[()=>`#/sessions/`+encodeURIComponent(W(d).id)+`?review=profile`]),q(e,t)};Y(a,e=>{W(t).pending&&W(p)?e(o):W(t).pending&&e(s,1)}),j(n),V(()=>Q(n,`id`,`quota-profile-`+W(d).id)),q(e,n)});var D=B(E,2),ne=e=>{var t=Fr();qr(R(t),()=>W(d).id,e=>{zo(e,{get session(){return W(d)},active:!0})}),q(e,t)};Y(D,e=>{W(p)||e(ne)}),j(t),V((e,t)=>{u=ci(s,1,`lamp lit`,null,u,{working:W(d).status===`WORKING`&&!W(h)}),J(f,`${e??``} // ${(W(d).name||W(d).directory)??``}`),J(y,`${t??``} TOKENS // ${W(d).id??``} // CONTEXT SOURCE // ${(W(d).source||`LOCAL`)??``}`),S=ui(x,``,S,{display:W(p)?`none`:void 0})},[()=>W(h)?`STALE`:W(p)?`QUOTA THRESHOLD`:c(W(d)),()=>oa(W(d).tokens)]),G(`click`,m,()=>{b(r().id),aa(r().id,2)}),q(e,t)},s=e=>{q(e,us())};Y(i,e=>{W(d)?e(o):e(s,-1)}),q(e,t)},pe=e=>{var t=Ss(),r=R(t),o=B(L(r),2),s=L(o),l=B(s,2);j(o),j(r);var d=B(r,2);Z(d,17,()=>W(i),e=>e.id,(e,t)=>{let r=N(()=>ra(W(t).id));var i=bs();let o;var s=L(i),l=L(s),d=e=>{var n=ds(),r=L(n),i=z(r);j(n),V(e=>{Q(r,`aria-pressed`,W(m)===W(t).id),J(i,`${e??``} // ${W(t).name??``}`)},[()=>Array.from(W(t).id).slice(-5).join(``).toUpperCase()]),G(`click`,r,()=>b(W(t).id)),q(e,n)};Y(l,e=>{W(t).name&&e(d)});var f=B(l,2),p=L(f),g=L(p);let _;var v=B(g,1,!0);j(p),j(f);var y=B(f,2),S=e=>{var n=fs(),r=z(n,!0);V(()=>{Q(n,`aria-pressed`,W(m)===W(t).id),J(r,W(t).directory||W(t).id)}),G(`click`,n,()=>b(W(t).id)),q(e,n)};Y(y,e=>{W(t).name||e(S)});var C=B(y,2),T=L(C);we(),j(C);var ee=B(C,2),te=e=>{var n=Xo(),r=z(n,!0);V(()=>J(r,W(t).directory)),q(e,n)};Y(ee,e=>{W(t).name&&W(t).directory&&e(te)});var E=B(ee,2),D=z(E),ne=B(E,2),re=z(ne),ie=B(ne,2),ae=L(ie),oe=B(ae,2),se=z(oe,!0),ce=B(oe,2);j(ie);var le=B(ie,2),ue=B(le,2),de=e=>{var n=ps(),r=z(n,!0);V(()=>J(r,W(t).status===`CHECK SESSION`?`INFERRED INACTIVITY`:$.data?.control?`OPEN FULL DETAIL OR REPLY IN CODEX`:W(t).status===`APPROVAL NEEDED`?`APPROVE OR DECLINE IN CODEX`:`REPLY IN CODEX`)),q(e,n)},fe=N(()=>!W(h)&&[`APPROVAL NEEDED`,`INPUT NEEDED`,`CHECK SESSION`].includes(W(t).status));Y(ue,e=>{W(fe)&&e(de)}),j(s);var pe=B(s,2),me=e=>{var r=vs(),i=L(r),o=L(i),s=z(o,!0),c=B(o,2),l=e=>{var n=ms(),r=z(n,!0);V(e=>{Q(n,`href`,e),J(r,$.data?.control?`REVIEW IN FULL DETAIL ↗`:`APPROVAL IN CODEX ↗`)},[()=>`#/sessions/`+encodeURIComponent(W(t).id)]),q(e,n)};Y(c,e=>{!W(h)&&W(t).status===`APPROVAL NEEDED`&&e(l)}),j(i);var d=B(i,2);n(d,()=>W(t),()=>!1);var f=B(d,2);Z(f,17,()=>W(a).filter(e=>e.session===W(t).id),X,(e,n)=>{var r=_s(),i=R(r),a=e=>{var t=hs(),r=z(t,!0);V(()=>J(r,W(n).notice)),q(e,t)};Y(i,e=>{W(n).notice&&e(a)});var o=B(i,2),s=e=>{var n=gs();V(e=>Q(n,`href`,e),[()=>`#/sessions/`+encodeURIComponent(W(t).id)+`?review=profile`]),G(`click`,n,e=>u(e,W(t).id)),q(e,n)};Y(o,e=>{W(n).pending&&e(s)}),q(e,r)});var p=B(f,2);{let e=N(()=>W(m)===W(t).id);zo(p,{get session(){return W(t)},get active(){return W(e)}})}j(r),V(e=>J(s,e),[()=>w(W(t))]),q(e,r)};Y(pe,e=>{W(r)>0&&e(me)});var he=B(pe,2),O=e=>{var n=ys(),r=B(L(n),2);{let e=N(()=>(W(t).samples||[]).map(e=>e.tokens));Ya(r,{get values(){return W(e)},capacity:120})}var i=z(B(r,2));j(n),V(()=>J(i,`LAST ${(W(t).samples?.length||0)??``} SAMPLES // AUTO SCALE`)),q(e,n)};Y(he,e=>{W(r)<2&&e(O)}),j(i),V((e,n,a,s,c)=>{o=ci(i,1,`session-row`,null,o,{selected:W(m)===W(t).id,wide:W(r)===2,split:W(r)===1}),Q(i,`aria-label`,`Session `+(W(t).name||W(t).directory||W(t).id)),Q(p,`href`,e),_=ci(g,1,`lamp lit`,null,_,{working:W(t).status===`WORKING`&&!W(h)}),J(v,n),J(T,`${a??``} `),J(D,`${W(t).agents??``} LINKED AGENTS`),J(re,`ACTIVE // ${s??``}`),ae.disabled=W(r)===0,Q(oe,`aria-expanded`,W(r)>0),J(se,W(r)?`HIDE DETAIL`:`SHOW DETAIL`),Q(le,`href`,c)},[()=>`#/sessions/`+encodeURIComponent(W(t).id),()=>W(h)?`STALE`:c(W(t)),()=>oa(W(t).tokens),()=>sa(W(t).activity),()=>`#/sessions/`+encodeURIComponent(W(t).id)]),G(`click`,p,()=>b(W(t).id)),G(`click`,ae,()=>x(W(t).id,-1)),G(`click`,oe,()=>{b(W(t).id),aa(W(t).id,+!W(r))}),G(`click`,ce,()=>x(W(t).id,1)),G(`click`,le,()=>b(W(t).id)),q(e,i)});var f=B(d,2),p=e=>{q(e,xs())};Y(f,e=>{W(i).length||e(p)}),G(`click`,s,()=>ia(1)),G(`click`,l,()=>ia(0)),q(e,t)};Y(de,e=>{r().id?e(fe):e(pe,-1)}),V(e=>{J(te,`OBSERVED ${e??``}`),D=ci(E,1,`session-totals`,null,D,{stale:W(h)}),J(re,`${W(h)?`LAST KNOWN TOTALS — live state counts unavailable. `:``}Tokens - observed since this server started for currently listed sessions; linked - agents are already included. Totals can decrease when a session leaves the - list. History samples every 30 seconds. Not account-wide totals.`)},[()=>sa($.data?.sessionsAt)]),q(e,T),Ke()}wr([`click`]);var Ts=K(`

      `),Es=K(`

      Relative usage/credits are not tokens or quota percentages. Groupings - describe the same usage, not additional consumption.

      `,1),Ds=K(`// APPROXIMATE`),Os=K(`

      Partial accounting: totals may still change.

      `),ks=K(`

      Percentage of this period's historical allowance, not today's limit. May - exceed 100%. This is not a manual/automatic reset log.

      `,1),As=K(`

      `,1),js=K(`

      This optional account report is unavailable. Token history remains - available; missing data does not mean zero usage.

      `),Ms=K(`
      `),Ns=K(`
      `),Ps=K(`

      No usage reported for this day.

      `),Fs=K(`

      Selected breakdown unavailable; missing does not mean zero.

      `),Is=K(`

      `,1);function Ls(e,t){Ge(t,!0);let n=F(`surface`),r=F(0),i=N(()=>t.reports?.daily),a=N(()=>t.reports?.plan),o=N(()=>t.mode===`breakdown`?W(i)?.days.length||0:W(a)?.periods.length||0),s=N(()=>Math.min(W(r),Math.max(0,W(o)-1))),c=N(()=>W(i)?.days[(W(i)?.days.length||0)-1-W(s)]),l=N(()=>W(a)?.periods[W(s)]),u=N(()=>t.mode===`breakdown`?W(i)?.units||``:`%`),d=N(()=>{let e=Object.create(null);if(t.mode===`breakdown`)e=W(c)?.groups[W(n)]||{};else{let t=W(n)===`feature`?`thread_source`:W(n)===`task start`?`turn_trigger`:W(n);for(let n of W(l)?.breakdowns||[])if(n.dimension===t)for(let t of n.rows)e[t.key]=(e[t.key]||0)+t.basis_points/100}return Object.entries(e).sort((e,t)=>t[1]-e[1]||e[0].localeCompare(t[0]))}),f=N(()=>t.mode===`breakdown`?W(c)?.total:W(l)?.used_basis_points==null?null:W(l).used_basis_points/100),p=N(()=>Math.max(1,W(f)||0,...W(d).map(([,e])=>e))),m=e=>e==null?`UNKNOWN`:e.toLocaleString(`en-GB`,{maximumFractionDigits:2});var h=Is(),g=R(h),v=L(g),y=B(L(v)),b=L(y);b.value=b.__value=`surface`;var x=B(b);x.value=x.__value=`model`;var S=B(x);S.value=S.__value=`feature`;var C=B(S);C.value=C.__value=`task start`,j(y),mi(y),j(v);var w=B(v,2),T=B(w,2),ee=z(B(T,2),!0);j(g);var te=B(g,2),E=L(te),D=z(E,!0),ne=B(E,2),re=z(ne),ie=B(ne,2),ae=e=>{var t=Es(),n=R(t),r=z(n),a=B(n,2),o=e=>{var t=Ts(),n=z(t);V(()=>J(n,`Data as of ${W(i).dataAsOf??``}`)),q(e,t)};Y(a,e=>{W(i).dataAsOf&&e(o)});var s=z(B(a,2));we(2),V((e,t)=>{J(r,`Latest fetch ${e??``} // queried ${W(i).from??``} → ${W(i).through??``}. - Reports are fetched from OpenAI and are not saved between runs.`),J(s,`${(W(c)?.date||`No daily buckets reported`)??``} // ${t??``} - ${W(u)??``}`)},[()=>sa(W(i).fetchedAt),()=>m(W(f))]),q(e,t)},oe=e=>{var t=As(),n=R(t),r=L(n);let i;var o=z(r,!0),s=B(r),c=e=>{q(e,Ds())};Y(s,e=>{W(a).approximate&&e(c)});var u=B(s);j(n);var d=B(n,2),p=L(d),h=B(p),g=e=>{var t=Pr();V(()=>J(t,`// Boundary tolerance ${W(a).boundary_tolerance_seconds??``}s`)),q(e,t)};Y(h,e=>{W(a).boundary_tolerance_seconds!=null&&e(g)}),j(d);var _=B(d,2),v=e=>{var t=ks(),n=R(t),r=L(n),i=B(r);let a;var o=z(i);j(n);var s=B(n,2),c=z(s),u=B(s,2),d=e=>{q(e,Os())};Y(u,e=>{W(l).accounting_complete||e(d)}),we(2),V(e=>{J(r,`${W(l).window_minutes??``} MIN // ${W(l).plan_type??``} // `),a=ci(i,1,`svelte-1fbui7h`,null,a,{muted:W(f)==null,"quota-near":W(f)!=null&&W(f)>=80&&W(f)<100,"quota-full":W(f)!=null&&W(f)>=100}),J(o,`USED ${e??``}${W(f)==null?``:`%`}`),J(c,`${W(l).starts_at??``} → ${W(l).ends_at??``}`)},[()=>m(W(f))]),q(e,t)};Y(_,e=>{W(l)&&e(v)}),V(e=>{i=ci(r,1,`svelte-1fbui7h`,null,i,{"quality-warning":!W(a).coverage_complete}),J(o,W(a).coverage_complete?`Reported range complete`:`Partial coverage`),J(u,` // Latest fetch ${e??``}`),J(p,`Coverage starts ${(W(a).coverage_start||`unknown`)??``} // Data as of ${(W(a).data_as_of||`unknown`)??``}`)},[()=>sa(W(a).fetchedAt)]),q(e,t)},se=e=>{q(e,js())};Y(ie,e=>{t.mode===`breakdown`&&W(i)?e(ae):t.mode===`periods`&&W(a)?e(oe,1):e(se,-1)});var ce=B(ie,2),le=e=>{var n=Ns(),r=L(n),i=z(B(L(r),2),!0);j(r),Z(B(r,2),17,()=>W(d),X,(e,n,r)=>{var i=N(()=>_(W(n),2));let a=()=>W(i)[0],o=()=>W(i)[1];var s=Ms();let c;var l=L(s),u=z(l,!0),d=B(l,2),f=L(d);let h;j(d);var g=z(B(d,2));j(s),V((e,n,i)=>{c=ci(s,1,`report-row svelte-1fbui7h`,null,c,{unknown:e,alternate:r%2==1}),Q(l,`title`,a()),J(u,a()),h=ui(f,``,h,{width:n}),J(g,`${i??``}${t.mode===`periods`?`%`:``}`)},[()=>a().trim().toLowerCase()===`unknown`,()=>`${Math.max(0,100*o()/W(p))}%`,()=>m(o())]),q(e,s)}),j(n),V(()=>J(i,t.mode===`breakdown`?W(u):`QUOTA USED`)),q(e,n)},ue=e=>{q(e,Ps())},de=e=>{q(e,Fs())};Y(ce,e=>{W(d).length?e(le):t.mode===`breakdown`&&W(c)?.total===0&&W(c)?.groups[W(n)]!=null?e(ue,1):(W(i)&&t.mode===`breakdown`||W(a)&&t.mode===`periods`)&&e(de,2)}),j(te),V(()=>{w.disabled=W(s)>=W(o)-1,T.disabled=W(s)===0,J(ee,W(o)?`${W(s)+1} / ${W(o)}`:`NO DATA`),J(D,t.mode===`breakdown`?`DAILY BREAKDOWN`:`QUOTA WINDOWS`),J(re,`${(t.mode===`breakdown`?t.reports?.dailyStatus||`UNAVAILABLE`:t.reports?.planStatus||`UNAVAILABLE`)??``} // UTC`)}),hi(y,()=>W(n),e=>I(n,e)),G(`click`,w,()=>I(r,W(s)+1)),G(`click`,T,()=>I(r,W(s)-1)),q(e,h),Ke()}wr([`click`]);var Rs=864e5;function zs(e,t){let n=new Date(e);n.setUTCDate(1),n.setUTCMonth(n.getUTCMonth()+t);let r=new Date(Date.UTC(n.getUTCFullYear(),n.getUTCMonth()+1,0)).getUTCDate();return n.setUTCDate(Math.min(e.getUTCDate(),r)),n}function Bs(e,t,n){let r=new Date(Date.UTC(e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate()));for(let e=0;;e++){let i=zs(r,-t),a=new Date(i.getTime()+Rs);if(e===n)return{start:a,end:r};r=i}}var Vs=K(``),Hs=K(``,1),Us=K(`

      History refresh failed. Any displayed history is the last successful - observation.

      `),Ws=K(`

      Token history unavailable; other reports may still be available.

      `),Gs=K(``),Ks=K(`
      `),qs=K(`

      LESS ░ ▒ ▓ █ MORE // HOVER FOR DATE AND TOKENS

      `,1),Js=K(`
      `,1),Ys=K(` `),Xs=K(`

      LIFETIME TOKENS

      PEAK DAY

      CURRENT STREAK

      DAYS

      LONGEST TURN

      SECONDS

      LONGEST STREAK

      DAYS

      OPENAI // UTC

      Accessible data table
      Date (UTC)Tokens

      `,1),Zs=K(`

      History unavailable or awaiting a matching account observation. Missing - history is not treated as zero usage.

      `),Qs=K(`

      USAGE // ACCOUNT HISTORY

      Account-wide history reported by Codex, not the local Sessions counter. Dates - use UTC. Allowance history is approximate where indicated; reset causes are - not provided.

      `,1);function $s(e,t){Ge(t,!0);let n=F(`daily`),r=F(12),i=F(0),a=N(()=>{let e=$.data?.usage?.dailyUsageBuckets;if(!e)return[];let{start:t,end:n}=Bs(new Date,W(r),W(i)),a=new Map;for(let t of e)!Number.isFinite(t.tokens)||t.tokens<0||a.set(t.startDate,(a.get(t.startDate)||0)+t.tokens);let o=[];for(let e=t;e<=n;e=new Date(e.getTime()+864e5)){let t=e.toISOString().slice(0,10);o.push({date:t,tokens:a.get(t)||0})}return o}),o=N(()=>Math.max(1,...W(a).map(e=>e.tokens))),s=N(()=>W(a).length?new Date(W(a)[0].date+`T00:00:00Z`).getUTCDay():0),c=N(()=>{if(W(n)===`monthly`){let e=new Map;for(let t of W(a))e.set(t.date.slice(0,7),(e.get(t.date.slice(0,7))||0)+t.tokens);return[...e].map(([e,t])=>({date:e,tokens:t}))}let e=0;return W(a).map(t=>({date:t.date,tokens:e+=t.tokens}))});var l=Qs(),u=B(R(l),4);Z(u,20,()=>[[`daily`,`Daily`],[`monthly`,`Monthly`],[`cumulative`,`Cumulative`],[`breakdown`,`Breakdown`],[`periods`,`Windows`]],X,(e,t)=>{var r=N(()=>_(t,2));let a=()=>W(r)[0],o=()=>W(r)[1];var s=Vs();let c;var l=z(s,!0);V(()=>{Q(s,`aria-pressed`,W(n)===a()),c=ci(s,1,``,null,c,{active:W(n)===a()}),J(l,o())}),G(`click`,s,()=>{I(n,a(),!0),I(i,0)}),q(e,s)}),j(u);var d=B(u,2),f=L(d),p=e=>{var t=Hs(),n=R(t),a=B(L(n)),o=L(a);o.value=o.__value=6;var s=B(o);s.value=s.__value=12,j(a),mi(a),j(n);var c=B(n),l=B(c);V(()=>l.disabled=W(i)===0),G(`change`,a,()=>I(i,0)),hi(a,()=>W(r),e=>I(r,e)),G(`click`,c,()=>Jt(i)),G(`click`,l,()=>Jt(i,-1)),q(e,t)};Y(f,e=>{W(n)!==`breakdown`&&W(n)!==`periods`&&e(p)}),j(d);var m=B(d,2),h=e=>{q(e,Us())};Y(m,e=>{$.data?.usageError&&e(h)});var g=B(m,2),v=e=>{q(e,Ws())};Y(g,e=>{$.data?.usage?.tokenStatus===`UNAVAILABLE`&&W(n)!==`breakdown`&&W(n)!==`periods`&&e(v)});var y=B(g,2),b=e=>{var t=Fr();qr(R(t),()=>W(n),e=>{{let t=N(()=>$.data?.usage?.reports);Ls(e,{get reports(){return W(t)},get mode(){return W(n)}})}}),q(e,t)},x=e=>{var t=Xs(),r=R(t),i=L(r),l=z(B(L(i),2),!0);j(i);var u=B(i,2),d=z(B(L(u),2),!0);j(u);var f=B(u,2),p=B(L(f),2),m=L(p);we(),j(p),j(f);var h=B(f,2),g=B(L(h),2),_=L(g);we(),j(g),j(h);var v=B(h,2),y=B(L(v),2),b=L(y);we(),j(y),j(v),j(r);var x=B(r,4),S=L(x),C=z(S),w=B(S,2),T=e=>{var t=qs(),n=R(t),r=L(n),i=L(r);Z(i,17,()=>Array(W(s)),X,(e,t)=>{q(e,Gs())}),Z(B(i,2),17,()=>W(a),X,(e,t)=>{var n=Ks();let r,i;V(e=>{r=ci(n,1,`heat-cell`,null,r,{zero:W(t).tokens===0}),Q(n,`title`,e),i=ui(n,``,i,{opacity:W(t).tokens?.25+.75*W(t).tokens/W(o):1})},[()=>`${W(t).date}: ${oa(W(t).tokens)} tokens`]),q(e,n)}),j(r),j(n),we(2),q(e,t)},ee=e=>{var t=Js(),r=R(t);{let e=N(()=>W(c).map(e=>e.tokens)),t=N(()=>W(n)+` usage`);Ya(r,{get values(){return W(e)},get label(){return W(t)}})}var i=B(r,2),a=L(i),o=z(a,!0),s=z(B(a),!0);j(i),V(e=>{J(o,W(c)[0]?.date),J(s,e)},[()=>W(c).at(-1)?.date]),q(e,t)};Y(w,e=>{W(n)===`daily`?e(T):e(ee,-1)});var te=B(w,2),E=B(L(te),2),D=L(E),ne=B(L(D));Z(ne,21,()=>W(n)===`daily`?W(a):W(c),X,(e,t)=>{var n=Ys(),r=L(n),i=z(r,!0),a=z(B(r),!0);j(n),V(e=>{J(i,W(t).date),J(a,e)},[()=>oa(W(t).tokens)]),q(e,n)}),j(ne),j(D),j(E),j(te),j(x);var re=z(B(x,2));V((e,t,n,r,i,o,s)=>{J(l,e),J(d,t),J(m,`${n??``} `),J(_,`${r??``} `),J(b,`${i??``} `),J(C,`${W(a)[0]?.date??``} — ${o??``}`),J(re,`OBSERVED ${s??``} // CUMULATIVE IS FOR THE SELECTED PERIOD`)},[()=>oa($.data.usage.summary.lifetimeTokens),()=>oa($.data.usage.summary.peakDailyTokens),()=>oa($.data.usage.summary.currentStreakDays),()=>oa($.data.usage.summary.longestRunningTurnSec),()=>oa($.data.usage.summary.longestStreakDays),()=>W(a).at(-1)?.date,()=>sa($.data.usageAt)]),q(e,t)},S=e=>{q(e,Zs())};Y(y,e=>{W(n)===`breakdown`||W(n)===`periods`?e(b):$.data?.usage&&$.data.usage.dailyUsageBuckets!==null?e(x,1):e(S,-1)}),q(e,l),Ke()}wr([`click`,`change`]);var ec=K(`

      Page not found

      Return to Quota

      `,1);function tc(e){var t=ec();we(2),q(e,t)}var nc=K(` `),rc=K(`

      `),ic=K(`

      Connecting to your local Codexometer…

      `),ac=K(``),oc=K(`
      CODEXOMETER

      Your quota. Your sessions. Your command centre.

      `);function sc(e,t){Ge(t,!0);let n={"/":Ka,"/quota/:view?":Ka,"/sessions/:id?":ws,"/usage":$s,"*":tc},r={quota:/^(?:\/|\/quota(?:\/[^/]+)?\/?)$/,sessions:/^\/sessions(?:\/[^/]+)?\/?$/,usage:/^\/usage\/?$/},i=F(`hacker`),a=[`hacker`,`rust`,`blue-steel`,`ultraviolet`,`nightshade`];bn(()=>{ta()}),bn(()=>{if($.data)for(let[e,t]of Object.entries(r))t.test(Ki.location)&&(ea.tab=e)}),Ui(()=>{try{let e=localStorage.getItem(`codexometer.web.theme`);e&&a.includes(e)&&I(i,e,!0)}catch{}return fa()});function o(){try{localStorage.setItem(`codexometer.web.theme`,W(i))}catch{}}var s=oc(),c=L(s),l=B(L(c),2),u=L(l);let d;var f=B(u,1,!0),p=z(B(f));j(l),j(c);var m=B(c,2);Z(m,21,()=>Object.entries(r),X,(e,t)=>{var n=N(()=>_(W(t),2));let r=()=>W(n)[0],i=()=>W(n)[1],a=N(()=>i().test(Ki.location));var o=nc();let s;var c=z(o,!0);V(e=>{Q(o,`href`,r()===`quota`?`#/quota/`+ea.view:`#/`+r()),Q(o,`aria-current`,W(a)?`page`:void 0),s=ci(o,1,``,null,s,{active:W(a)}),J(c,e)},[()=>r().toUpperCase()]),q(e,o)}),j(m);var h=B(m,2),g=L(h),v=e=>{var t=rc(),n=z(t,!0);V(()=>J(n,$.error)),q(e,t)};Y(g,e=>{$.error&&e(v)});var y=B(g,2),b=e=>{Yi(e,{get routes(){return n}})},x=e=>{q(e,ic())};Y(y,e=>{$.data?e(b):$.error||e(x,1)}),j(h);var S=B(h,2),C=L(S),w=z(C),T=B(C,2),ee=z(T,!0),te=B(T,2),E=B(L(te));Z(E,21,()=>a,X,(e,t)=>{var n=ac(),r=z(n,!0),i={};V(e=>{J(r,e),i!==(i=W(t))&&(n.value=(n.__value=i)??``)},[()=>W(t).replace(`-`,` `).toUpperCase()]),q(e,n)}),j(E),mi(E),j(te),j(S),j(s),V(()=>{Q(s,`data-theme`,W(i)),d=ci(u,1,`lamp`,null,d,{lit:$.connected}),J(f,$.connected?`CONNECTED`:`OFFLINE`),J(p,`EXPERIMENTAL // ${$.data?.control?`SESSION CONTROL`:`READ ONLY`}`),J(w,`v${($.data?.version||`…`)??``} // LOCAL ONLY`),J(ee,$.data?.control?`Session control enabled. Stop the server to revoke access.`:`No actions can be sent from this preview.`)}),G(`change`,E,o),hi(E,()=>W(i),e=>I(i,e)),q(e,s),Ke()}wr([`change`]),Vr(sc,{target:document.getElementById(`app`)}); \ No newline at end of file diff --git a/internal/web/dist/assets/index-CIiWCxs0.css b/internal/web/dist/assets/index-CIiWCxs0.css deleted file mode 100644 index b5dfc9b..0000000 --- a/internal/web/dist/assets/index-CIiWCxs0.css +++ /dev/null @@ -1 +0,0 @@ -.observation-details.svelte-1wk8llw{font-size:12px}summary.svelte-1wk8llw{cursor:pointer;color:var(--accent)}.observation-table.svelte-1wk8llw{max-height:240px;overflow:auto}.zone-canvas.svelte-1wk8llw{flex:1;min-height:170px;margin-top:6px;position:relative}svg.svelte-1wk8llw{width:100%;height:100%;display:block;position:absolute;inset:0}text.svelte-1wk8llw{fill:var(--ink);font:14px Cascadia Code,Consolas,monospace}.axis-title.svelte-1wk8llw{letter-spacing:.04em;font-size:12px}.grid.svelte-1wk8llw{stroke:#fff;stroke-opacity:.14;stroke-width:1px}.axes.svelte-1wk8llw{stroke:var(--ink);stroke-width:1.5px;fill:none}.pace-line.svelte-1wk8llw{stroke:#fff;stroke-width:2px;stroke-dasharray:6 4}.position-halo.svelte-1wk8llw{fill:#101820;stroke:#fff;stroke-width:1.5px}.position-dot.svelte-1wk8llw{fill:#fff}.observation-trail.svelte-1wk8llw{fill:none;stroke:#fff;stroke-width:2.5px;stroke-linejoin:round}.trail-start.svelte-1wk8llw{fill:none;stroke:#fff;stroke-width:2px}.trail-caption.svelte-1wk8llw{text-align:center;font-size:11px}.zone-caption.svelte-1wk8llw{color:var(--accent);text-align:center}.file-diff.svelte-x4gtb6{border:1px solid var(--muted);max-height:60vh;padding:.5rem;font-family:monospace;overflow:auto}.diff-line.svelte-x4gtb6{white-space:pre;min-height:1.3em;display:flex}.line-number.svelte-x4gtb6{text-align:right;opacity:.75;-webkit-user-select:none;user-select:none;flex:0 0 5ch;padding-right:1ch;display:inline-block}.added.svelte-x4gtb6{color:#67d391;background:#67d39112}.removed.svelte-x4gtb6{color:#ff6b83;background:#ff6b8312}.heading.svelte-x4gtb6{padding:.5rem 0;font-weight:700}.saved-prompt.svelte-1oupzfc{white-space:pre-wrap;overflow-wrap:anywhere}fieldset.svelte-1oupzfc{border:1px solid;margin:.5rem 0;padding:.35rem .6rem}.decision.svelte-1oupzfc,.answer.svelte-1oupzfc{margin:.3rem 0;display:block}.decision.svelte-1oupzfc code:where(.svelte-1oupzfc),.decision.svelte-1oupzfc span:where(.svelte-1oupzfc){overflow-wrap:anywhere;margin:.3rem 0 .3rem 1.5rem;display:block}textarea.svelte-1oupzfc,.answer.svelte-1oupzfc input:where(.svelte-1oupzfc),.answer.svelte-1oupzfc select:where(.svelte-1oupzfc){box-sizing:border-box;background:var(--bg);width:100%;color:inherit;font:inherit;border:1px solid;margin-top:.4rem;padding:.5rem;display:block}textarea.svelte-1oupzfc{resize:vertical;background:color-mix(in srgb, currentColor 8%, var(--bg))}button.svelte-1oupzfc{margin:.3rem .5rem .3rem 0}h3.svelte-1oupzfc,p.svelte-1oupzfc{margin-block:.4rem}.sent.svelte-1oupzfc{color:var(--accent)}.session-copy.svelte-543j00{justify-content:flex-end;align-items:center;gap:8px;margin-top:auto;padding-top:6px;display:flex}span.svelte-543j00{color:var(--muted);overflow-wrap:anywhere;font-size:12px}button.svelte-543j00{color:var(--accent);flex-shrink:0;padding:3px 7px}button.svelte-543j00:hover,button.flashed.svelte-543j00{color:var(--bg);background:var(--accent)}h3.svelte-1fbui7h,.window-dates.svelte-1fbui7h,.report-value.svelte-1fbui7h{color:var(--accent);font-weight:700}.quality-warning.svelte-1fbui7h,.quota-near.svelte-1fbui7h{color:var(--approval)}.quota-full.svelte-1fbui7h{color:#ff6680}.alternate.svelte-1fbui7h .report-fill:where(.svelte-1fbui7h){background:radial-gradient(circle, var(--panel) .7px, transparent .9px) 0 0 / 3px 3px, var(--accent)}.unknown.svelte-1fbui7h,.unknown.svelte-1fbui7h .report-value:where(.svelte-1fbui7h){color:var(--muted)}.unknown.svelte-1fbui7h .report-fill:where(.svelte-1fbui7h){background:var(--muted);opacity:1}.report-bars.svelte-1fbui7h{grid-template-columns:minmax(5rem,1fr) minmax(2rem,3fr) auto;gap:.6rem;display:grid}.report-row.svelte-1fbui7h,.report-head.svelte-1fbui7h{grid-column:1/-1;grid-template-columns:subgrid;align-items:center;gap:.6rem;display:grid}.report-head.svelte-1fbui7h{color:var(--muted);font-size:.85rem}.report-head.svelte-1fbui7h>span:where(.svelte-1fbui7h):last-child,.report-value.svelte-1fbui7h{text-align:right}.report-row.svelte-1fbui7h>span:where(.svelte-1fbui7h):first-child{overflow-wrap:anywhere}.report-track.svelte-1fbui7h{background:var(--panel);height:1rem}.report-fill.svelte-1fbui7h{background:var(--accent);height:100%}:root{font-synthesis:none;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;background:#080e0c;font-family:Cascadia Code,SFMono-Regular,Consolas,monospace}*{box-sizing:border-box}body{margin:0}.shell{--accent:#80edac;--approval:#ffca58;--muted:#98aaa0;--edge:#31473a;--panel:#101c16;--bg:#080e0c;--ink:#e2eee5;background:var(--bg);height:100dvh;color:var(--ink);grid-template-rows:auto auto minmax(0,1fr) auto;padding:clamp(10px,1.2vw,20px);display:grid}.shell[data-theme=rust]{--approval:#ff8a3d;--accent:#ffbc75;--muted:#bfac95;--edge:#59412c;--panel:#261b14;--bg:#140e0a}.shell[data-theme=blue-steel]{--approval:#e8c46a;--accent:#85c8ff;--muted:#9cacc4;--edge:#334d69;--panel:#142235;--bg:#0b1421}.shell[data-theme=ultraviolet]{--approval:#f9a8d4;--accent:#c5adff;--muted:#b3a6c9;--edge:#4b3c68;--panel:#241a35;--bg:#140d22}.shell[data-theme=nightshade]{--approval:#8f7cff;--accent:#e59bff;--muted:#c6a7cf;--edge:#673976;--panel:#301739;--bg:#1c0b24}header,.spread,footer,.controls{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:10px;display:flex}header{margin-bottom:10px}.brand{letter-spacing:.08em;color:var(--accent);text-shadow:0 0 22px color-mix(in srgb, var(--accent) 20%, transparent);font-size:clamp(22px,2.5vw,32px);font-weight:800;text-decoration:none}header p{color:var(--muted);margin:3px 0 0;font-size:13px}.connection{color:var(--accent);text-align:right}small,.eyebrow{letter-spacing:.04em;font-size:12px}.connection small{color:var(--muted);margin-top:4px;display:block}.lamp{background:var(--muted);border-radius:50%;width:9px;height:9px;margin-right:9px;display:inline-block}.lamp.lit{background:var(--accent);box-shadow:0 0 8px color-mix(in srgb, var(--accent) 40%, transparent)}.working{animation:1.5s ease-in-out infinite pulse}@keyframes pulse{50%{opacity:.3}}nav{flex-wrap:wrap;gap:6px;margin-bottom:10px;display:flex}nav a,button,.button,select{font:inherit;color:var(--accent);border:1px solid var(--edge);background:var(--panel);cursor:pointer;border-radius:3px;padding:7px 11px;font-size:13px;text-decoration:none}button:hover,.button:hover,nav a:hover{border-color:var(--accent)}nav a.active,nav button.active{background:var(--accent);color:var(--bg);border-color:var(--accent)}:focus-visible{outline:2px solid var(--accent);outline-offset:4px}.secondary{margin-bottom:8px}.secondary a,.secondary button{padding:6px 10px;font-size:12px}main{scrollbar-gutter:stable;flex-direction:column;min-width:0;min-height:0;display:flex;overflow:auto}main>*{flex-shrink:0}.panel{border:1px solid var(--edge);border-top:2px solid var(--accent);background:var(--panel);border-radius:4px;min-width:0;padding:clamp(10px,1vw,16px)}h1{color:var(--accent);font-size:20px}h2{letter-spacing:.04em;color:var(--accent);overflow-wrap:anywhere;margin:0 0 10px;font-size:13px}h3{overflow-wrap:anywhere;font-size:13px}p{margin:6px 0;font-size:13px;line-height:1.45}a{color:var(--accent)}.muted,.eyebrow{color:var(--muted)}.notice{color:#ffe0aa;background:#332611;border-left:3px solid #ffc26e;padding:12px 16px}.empty{color:var(--muted);padding:35px 0}.quota-grid{flex:1 0 auto;grid-auto-rows:minmax(220px,1fr);gap:10px;display:grid}.quota-card{flex-direction:column;min-height:0;display:flex}.quota-card>:not(.meter-graphic){flex-shrink:0}.meter-graphic{flex-direction:column;flex:1;min-height:100px;display:flex}.bar-graphic{min-height:65px}.quota-card .gauge{flex:2;height:auto;min-height:16px;margin:7px 0}.quota-card .timeline{flex:1;min-height:8px}.quota-card .pace{flex:1;height:auto;min-height:20px;margin:20px 14px 8px}.quota-card .readout{margin:6px 0;font-size:clamp(18px,2vw,28px)}.quota-grid.zone{grid-template-columns:repeat(auto-fit,minmax(min(100%,420px),1fr));grid-auto-rows:minmax(330px,1fr)}.quota-grid.radial{grid-template-columns:repeat(auto-fit,minmax(min(100%,300px),1fr));grid-auto-rows:minmax(260px,1fr)}.gauge{background:var(--edge);height:35px;margin:15px 0;overflow:hidden}.gauge>div{background:repeating-linear-gradient(90deg, var(--accent) 0, var(--accent) 9px, transparent 9px, transparent 12px);height:100%;transition:width .5s}.gauge.timeline{height:14px}.pie-wrap{flex:1;min-height:100px;margin:6px 0;position:relative}.pie-wrap svg{width:100%;height:100%;position:absolute;inset:0}.pie-base{fill:var(--edge);stroke:var(--accent);stroke-width:1px}.pie-fill{fill:var(--accent)}.pace{background:linear-gradient(90deg, #6d3838, var(--edge) 50%, #376348);height:28px;margin:28px 14px 15px;position:relative}.pace-mid{border-left:2px solid var(--ink);height:100%;position:absolute;left:50%}.pace-marker{color:var(--accent);font-size:27px;position:absolute;top:-16px;transform:translate(-50%)}.readout{overflow-wrap:anywhere;color:var(--accent);margin:18px 0;font-size:clamp(22px,3vw,34px)}.credit+.credit{border-top:1px solid var(--edge);padding-top:20px}.credit{margin-top:24px}.session-row{grid-template-columns:minmax(210px,1.1fr) minmax(0,1.2fr) minmax(0,1.5fr);align-items:stretch;gap:14px;margin:18px 0;display:grid}.session-totals{grid-template-columns:repeat(auto-fit,minmax(min(145px,100%),1fr));gap:8px;margin:8px 0;display:grid}.session-totals>div{border:1px solid var(--edge);min-width:0;padding:10px}.session-totals dt{color:var(--muted);font-size:11px}.session-totals dd{color:var(--accent);overflow-wrap:anywhere;margin:6px 0 0;font-size:24px}.session-totals.stale dd{color:var(--muted)}.session-row.selected{outline:1px solid var(--accent);outline-offset:4px}.session-row.wide .context{grid-column:2/-1}.detail-controls{flex-wrap:wrap;gap:6px;display:flex}.attention-summary{flex-wrap:wrap;gap:8px;margin-block:8px;display:flex}.attention-summary .approval{color:var(--approval);border-color:var(--approval)}.attention-summary .approval:hover{background:color-mix(in srgb, var(--approval) 12%, var(--panel))}.attention-summary .approval:focus-visible{outline-color:var(--approval)}.attention-note,.attention-badge{color:var(--accent);border-left:3px solid;padding-left:8px}.attention-note.inferred{color:var(--muted)}.attention-badge{font-size:12px}.session-select{text-align:left;overflow-wrap:anywhere;max-width:100%}.status-detail{color:inherit;text-decoration:none}.status-detail:hover{text-decoration:underline}button:disabled{opacity:.4;cursor:default}.session-row .graph-panel{flex-direction:column;grid-column:2/-1;display:flex}.session-row.split .graph-panel{grid-column:auto}.session-row .button,.session-row button{margin-top:8px;display:inline-block}.context pre{max-height:230px;overflow:auto}.session-row .context{flex-direction:column;display:flex}pre{font:inherit;white-space:pre-wrap;overflow-wrap:anywhere;font-size:13px;line-height:1.7}.full-detail{margin-top:6px}.detail-heading{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:6px 12px;display:flex}.detail-heading h2{flex:250px;min-width:0;margin:0}.detail-metadata{overflow-wrap:anywhere}.detail-workspace{border-top:1px solid var(--edge);flex-direction:column;gap:10px;margin-top:8px;padding-top:4px;display:flex}.detail-context,.detail-workspace .session-actions{min-width:0}.detail-workspace .session-actions{border-top:1px solid var(--edge);padding-top:6px}.full-detail h3{margin-block:8px}.full-detail pre{margin-block:8px;line-height:1.5}.full-detail hr{margin-block:12px}.full-detail .command,.full-detail .notice{padding:8px 10px}.command{background:var(--bg);border-left:3px solid var(--accent);padding:16px}hr{border:0;border-top:1px solid var(--edge);margin:24px 0}.chart{border-bottom:1px solid var(--muted);align-items:flex-end;gap:2px;height:clamp(120px,22vh,320px);margin-top:18px;display:flex}.chart-bar{background:var(--accent);flex:1;min-width:0;max-height:100%}.controls{justify-content:flex-start;margin:20px 0}.summary-grid{grid-template-columns:repeat(auto-fit,minmax(min(100%,220px),1fr));gap:16px;margin:20px 0;display:grid}.thresholds-panel{padding:clamp(14px,2vw,22px)}.thresholds-panel h1{margin-top:0}.threshold-list{gap:8px;margin-top:16px;display:grid}.threshold-list article{border:1px solid var(--edge);grid-template-columns:minmax(64px,.5fr) minmax(220px,3fr) minmax(64px,.6fr) minmax(120px,1.2fr);align-items:center;gap:12px;padding:10px 12px;display:grid}.threshold-list article.active{border-color:var(--accent);box-shadow:inset 3px 0 0 var(--accent)}.threshold-list article.next{border-color:var(--approval);box-shadow:inset 3px 0 0 var(--approval)}.threshold-list p{color:var(--muted);margin:3px 0 0;font-size:12px}.threshold-trigger{color:var(--accent);font-size:22px;font-weight:800}.threshold-mode,.threshold-state{color:var(--muted);font-size:12px;font-weight:700}.threshold-list article.active .threshold-state{color:var(--accent)}.threshold-list article.next .threshold-state{color:var(--approval)}.heat-scroll{overflow-x:auto}.heatmap{grid-template-rows:repeat(7,13px);grid-auto-columns:minmax(9px,1fr);grid-auto-flow:column;gap:4px;min-width:620px;margin:25px 0;display:grid}.heat-cell{background:var(--accent);border-radius:2px}.heat-cell.zero{background:var(--edge)}.table-scroll{max-height:280px;overflow:auto}table{border-collapse:collapse;width:100%;margin-top:15px}th,td{text-align:left;border-bottom:1px solid var(--edge);padding:8px}summary{cursor:pointer;color:var(--accent);padding-top:14px}footer{border-top:1px solid var(--edge);color:var(--muted);margin-top:8px;padding-top:8px;font-size:11px}footer select{padding:6px}@media (width<=850px){.session-row{grid-template-columns:minmax(0,1fr)}.session-row.wide .context,.session-row .graph-panel{grid-column:auto}.connection{text-align:left}.threshold-list article{grid-template-columns:64px minmax(0,1fr)}.threshold-mode,.threshold-state{grid-column:2}}@media (prefers-reduced-motion:reduce){*,:before,:after{transition:none!important;animation:none!important}} diff --git a/internal/web/dist/assets/index-CQ2rxUyW.js b/internal/web/dist/assets/index-CQ2rxUyW.js new file mode 100644 index 0000000..4fa4764 --- /dev/null +++ b/internal/web/dist/assets/index-CQ2rxUyW.js @@ -0,0 +1,45 @@ +(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),t.credentials=e.crossOrigin===`use-credentials`?`include`:e.crossOrigin===`anonymous`?`omit`:`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var e=Array.isArray,t=Array.prototype.indexOf,n=Array.prototype.includes,r=Array.from,i=Object.defineProperty,a=Object.getOwnPropertyDescriptor,o=Object.getOwnPropertyDescriptors,s=Object.prototype,c=Array.prototype,l=Object.getPrototypeOf,u=Object.isExtensible;function d(e){return typeof e==`function`}var f=()=>{};function p(e){return e()}function m(e){for(var t=0;t{e=n,t=r}),resolve:e,reject:t}}function g(e,t,n=!1){return e===void 0?n?t():t:e}function _(e,t){if(Array.isArray(e))return e;if(t===void 0||!(Symbol.iterator in e))return Array.from(e);let n=[];for(let r of e)if(n.push(r),n.length===t)break;return n}var v=1024,y=2048,b=4096,x=8192,S=16384,C=32768,w=1<<25,T=65536,ee=1<<19,E=1<<20,D=1<<25,O=1<<21,te=1<<22,ne=1<<23,re=Symbol(`$state`),ie=Symbol(`component`),ae=Symbol(`legacy props`),oe=Symbol(``),se=Symbol(`attributes`),ce=Symbol(`class`),le=Symbol(`style`),ue=Symbol(`text`),de=Symbol(`form reset`),fe=new class extends Error{name=`StaleReactionError`;message="The reaction that called `getAbortSignal()` was re-run or destroyed"},pe=!!globalThis.document?.contentType&&globalThis.document.contentType.includes(`xml`),me={},he=Symbol(`uninitialized`),ge=`http://www.w3.org/1999/xhtml`;function _e(){console.warn(`https://svelte.dev/e/derived_inert`)}function ve(e){console.warn(`https://svelte.dev/e/hydration_mismatch`)}function ye(){console.warn(`https://svelte.dev/e/select_multiple_invalid_value`)}function be(){console.warn(`https://svelte.dev/e/svelte_boundary_reset_noop`)}var k=!1;function xe(e){k=e}var A;function Se(e){if(e===null)throw ve(),me;return A=e}function Ce(){return Se(cn(A))}function j(e){if(k){if(cn(A)!==null)throw ve(),me;A=e}}function we(e=1){if(k){for(var t=e,n=A;t--;)n=cn(n);A=n}}function Te(e=!0){for(var t=0,n=A;;){if(n.nodeType===8){var r=n.data;if(r===`]`){if(t===0)return n;--t}else(r===`[`||r===`[!`||r[0]===`[`&&!isNaN(Number(r.slice(1))))&&(t+=1)}var i=cn(n);e&&n.remove(),n=i}}function Ee(e){if(!e||e.nodeType!==8)throw ve(),me;return e.data}function De(e){return e===this.v}function Oe(e,t){return e==e?e!==t||typeof e==`object`&&!!e||typeof e==`function`:t==t}function ke(e){return!Oe(e,this.v)}function Ae(e){throw Error(`https://svelte.dev/e/lifecycle_outside_component`)}function je(){throw Error(`https://svelte.dev/e/async_derived_orphan`)}function Me(e,t,n){throw Error(`https://svelte.dev/e/each_key_duplicate`)}function Ne(e){throw Error(`https://svelte.dev/e/effect_in_teardown`)}function Pe(){throw Error(`https://svelte.dev/e/effect_in_unowned_derived`)}function Fe(e){throw Error(`https://svelte.dev/e/effect_orphan`)}function Ie(){throw Error(`https://svelte.dev/e/effect_update_depth_exceeded`)}function Le(e){throw Error(`https://svelte.dev/e/props_invalid_value`)}function Re(){throw Error(`https://svelte.dev/e/state_descriptors_fixed`)}function ze(){throw Error(`https://svelte.dev/e/state_prototype_fixed`)}function Be(){throw Error(`https://svelte.dev/e/state_unsafe_mutation`)}function Ve(){throw Error(`https://svelte.dev/e/svelte_boundary_reset_onerror`)}var He=!1;function Ue(){He=!0}var M=null;function We(e){M=e}function Ge(e,t=!1,n){M={p:M,i:!1,c:null,e:null,s:e,x:null,r:U,l:He&&!t?{s:null,u:null,$:[]}:null}}function Ke(e){var t=M,n=t.e;if(n!==null){t.e=null;for(var r of n)xn(r)}return e!==void 0&&(t.x=e),t.i=!0,M=t.p,qe(e)}function qe(e={}){return i(e,ie,{value:!0}),e}function Je(){return!He||M!==null&&M.l===null}var Ye=[];function Xe(){var e=Ye;Ye=[],m(e)}function Ze(e){if(Ye.length===0&&!Tt){var t=Ye;queueMicrotask(()=>{t===Ye&&Xe()})}Ye.push(e)}function Qe(){for(;Ye.length>0;)Xe()}var $e=~(y|b|v);function et(e,t){e.f=e.f&$e|t}function tt(e){e.f&512||e.deps===null?et(e,v):et(e,b)}function nt(e,t,n){e.f&2048?t.add(e):e.f&4096&&n.add(e),et(e,v)}function rt(e){k&&sn(e)!==null&&ln(e)}var it=!1;function at(){it||(it=!0,document.addEventListener(`reset`,e=>{Promise.resolve().then(()=>{if(!e.defaultPrevented)for(let t of e.target.elements)t[de]?.()})},{capture:!0}))}function ot(e){var t=H,n=U;Gn(null),Kn(null);try{return e()}finally{Gn(t),Kn(n)}}function st(e,t,n,r=n){e.addEventListener(t,()=>ot(n));let i=e[de];e[de]=i?()=>{i(),r(!0)}:()=>r(!0),at()}function ct(e,t,n,r){let i=Je()?ft:ht;var a=e.filter(e=>!e.settled),o=t.map(i);if(n.length===0&&a.length===0){r(o);return}var s=U,c=lt(),l=a.length===1?a[0].promise:a.length>1?Promise.all(a.map(e=>e.promise)):null;function u(e){if(!(s.f&16384)){c();try{r([...o,...e])}catch(e){mn(e,s)}ut()}}var d=dt();if(n.length===0){l.then(()=>u([])).finally(d);return}function f(){Promise.all(n.map(e=>mt(e))).then(u).catch(e=>mn(e,s)).finally(d)}l?l.then(()=>{c(),f(),ut()}):f()}function lt(){var e=U,t=H,n=M,r=P;return function(i=!0){Kn(e),Gn(t),We(n),i&&!(e.f&16384)&&(r?.activate(),r?.apply())}}function ut(e=!0){Kn(null),Gn(null),We(null),e&&P?.deactivate()}function dt(){var e=U,t=e.b,n=P,r=!!t?.is_rendered();return t?.update_pending_count(1,n),n.increment(r,e),()=>{t?.update_pending_count(-1,n),n.decrement(r,e)}}function ft(e){var t=2|y;return U!==null&&(U.f|=ee),{ctx:M,deps:null,effects:null,equals:De,f:t,fn:e,reactions:null,rv:0,v:he,wv:0,parent:U,ac:null}}var pt=Symbol(`obsolete`);function mt(e,t,n){let r=U;r===null&&je();var i=void 0,a=Ht(he),o=!H,s=new Set;return Tn(()=>{var t=U,n=h();i=n.promise;try{Promise.resolve(e()).then(n.resolve,e=>{e!==fe&&n.reject(e)}).finally(ut)}catch(e){n.reject(e),ut()}var c=P;if(o){if(t.f&32768)var l=dt();if(r.b?.is_rendered())c.async_deriveds.get(t)?.reject(pt);else for(let e of s.values())e.reject(pt);s.add(n),c.async_deriveds.set(t,n)}let u=(e,t=void 0)=>{l?.(),s.delete(n),t!==pt&&(c.activate(),t?(a.f|=ne,Kt(a,t)):(a.f&8388608&&(a.f^=ne),Kt(a,e)),c.deactivate())};n.promise.then(u,e=>u(null,e||`unknown`))}),yn(()=>{for(let e of s)e.reject(pt)}),new Promise(e=>{function t(n){function r(){n===i?e(a):t(i)}n.then(r,r)}t(i)})}function N(e){let t=ft(e);return Jn(t),t}function ht(e){let t=ft(e);return t.equals=ke,t}function gt(e){var t=e.effects;if(t!==null){e.effects=null;for(var n=0;n{t.ac.abort(fe),t.ac=null}),t.fn!==null&&(t.teardown=f),lr(t,0),An(t))}function bt(e){if(e.effects!==null)for(let t of e.effects)t.teardown&&t.fn!==null&&ur(t)}var xt=null,P=null,St=null,Ct=null,wt=null,Tt=!1,Et=!1,Dt=null,Ot=null,kt=0,At=1,jt=class e{id=At++;#e=!1;linked=!0;#t=null;#n=null;async_deriveds=new Map;current=new Map;previous=new Map;#r=new Set;#i=new Set;#a=0;#o=new Map;#s=null;#c=[];#l=[];#u=new Set;#d=new Set;#f=new Map;#p=new Set;is_fork=!1;#m=!1;constructor(){xt===null?xt=this:(xt.#n=this,this.#t=xt),xt=this}#h(){if(this.is_fork)return!0;for(let n of this.#o.keys()){for(var e=n,t=!1;e.parent!==null;){if(this.#f.has(e)){t=!0;break}e=e.parent}if(!t)return!0}return!1}skip_effect(e){this.#f.has(e)||this.#f.set(e,{d:[],m:[]}),this.#p.delete(e)}unskip_effect(e,t=e=>this.schedule(e)){var n=this.#f.get(e);if(n){this.#f.delete(e);for(var r of n.d)et(r,y),t(r);for(r of n.m)et(r,b),t(r)}this.#p.add(e)}#g(){var e=[];for(let i of this.#c)if(!(i.f&16384||!(i.f&6144))){for(var t=i,n=!1;t.parent!==null;){t=t.parent;var r=t.f;if(r&96){if(!(r&1024)){n=!0;break}t.f^=v}}n||e.push(t)}return this.#c=[],e}#_(){this.#e=!0;for(let e of this.#u)this.#d.delete(e),et(e,y),this.schedule(e);for(let e of this.#d)et(e,b),this.schedule(e);this.apply();for(var t=Dt=[],n=[],r=Ot=[];this.#c.length>0;){kt++>1e3&&(this.#S(),Nt());for(let e of this.#g())try{this.#v(e,t,n)}catch(t){throw Rt(e),this.#h()||this.discard(),t}}if(P=null,r.length>0){var i=e.ensure();for(let e of r)i.schedule(e)}if(Dt=null,Ot=null,this.#h()){this.#x(n),this.#x(t);for(let[e,t]of this.#f)Lt(e,t);r.length>0&&P.#_();return}let a=this.#y();if(a){this.#x(n),this.#x(t),a.#b(this);return}this.#u.clear(),this.#d.clear();for(let e of this.#r)e(this);this.#r.clear(),St=this,Ft(n),Ft(t),St=null,this.#s?.resolve();var o=P;if(this.#a===0&&(this.#c.length===0||o!==null)&&this.#S(),this.#c.length>0){if(o!==null){for(let e of this.#c)o.#c.push(e);this.#c=[]}else o=this}o!==null&&(Bt.clear(),o.#_())}#v(e,t,n){e.f^=v;for(var r=e.first;r!==null;){var i=r.f,a=!!(i&96);if(!(a&&i&1024||i&8192||this.#f.has(r))&&r.fn!==null){a?r.f^=v:i&4?t.push(r):ir(r)&&(i&16&&this.#d.add(r),ur(r));var o=r.first;if(o!==null){r=o;continue}}for(;r!==null;){var s=r.next;if(s!==null){r=s;break}r=r.parent}}}#y(){for(var e=this.#t;e!==null;){if(!e.is_fork){for(let[t,[,n]]of this.current)if(e.current.has(t)&&!n)return e}e=e.#t}return null}#b(e){for(let[t,n]of e.current)!this.previous.has(t)&&e.previous.has(t)&&this.previous.set(t,e.previous.get(t)),this.current.set(t,n);for(let[t,n]of e.async_deriveds){let e=this.async_deriveds.get(t);e&&n.promise.then(e.resolve).catch(e.reject)}e.async_deriveds.clear(),this.transfer_effects(e.#u,e.#d);let t=e=>{var n=e.reactions;if(n!==null&&!(e.f&2&&!(e.f&6144)))for(let e of n){var r=e.f;if(r&2)t(e);else{var i=e;r&4194320&&!this.async_deriveds.has(i)&&(this.#d.delete(i),et(i,y),this.schedule(i))}}};for(let e of this.current.keys())t(e);this.oncommit(()=>e.discard()),e.#S(),P=this,this.#_()}#x(e){for(var t=0;t{this.#m=!1,this.linked&&this.flush()}))}transfer_effects(e,t){for(let t of e)this.#u.add(t);for(let e of t)this.#d.add(e);e.clear(),t.clear()}oncommit(e){this.#r.add(e)}ondiscard(e){this.#i.add(e)}settled(){return(this.#s??=h()).promise}static ensure(){if(P===null){let t=P=new e;!Et&&!Tt&&Ze(()=>{t.#e||t.flush()})}return P}apply(){Ct=null}schedule(e){if(wt=e,e.b?.is_pending&&e.f&16777228&&!(e.f&32768)){e.b.defer_effect(e);return}this.#c.push(e)}#S(){if(this.linked){var e=this.#t,t=this.#n;e===null||(e.#n=t),t===null?xt=e:t.#t=e,this.linked=!1}}};function Mt(e){var t=Tt;Tt=!0;try{var n;for(e&&(P!==null&&!P.is_fork&&P.flush(),n=e());;){if(Qe(),P===null)return n;P.flush()}}finally{Tt=t}}function Nt(){try{Ie()}catch(e){mn(e,wt)}}var Pt=null;function Ft(e){var t=e.length;if(t!==0){for(var n=0;n0)){Bt.clear();for(let e of Pt){if(e.f&24576)continue;let t=[e],n=e.parent;for(;n!==null;)Pt.has(n)&&(Pt.delete(n),t.push(n)),n=n.parent;for(let e=t.length-1;e>=0;e--){let n=t[e];n.f&24576||ur(n)}}Pt.clear()}}Pt=null}}function It(e){P.schedule(e)}function Lt(e,t){if(!(e.f&32&&e.f&1024)){e.f&2048?t.d.push(e):e.f&4096&&t.m.push(e),et(e,v);for(var n=e.first;n!==null;)Lt(n,t),n=n.next}}function Rt(e){et(e,v);for(var t=e.first;t!==null;)Rt(t),t=t.next}var zt=new Set,Bt=new Map,Vt=!1;function Ht(e,t){return{f:0,v:e,reactions:null,equals:De,rv:0,wv:0}}function F(e,t){let n=Ht(e,t);return Jn(n),n}function Ut(e,t=!1,n=!0){let r=Ht(e);return t||(r.equals=ke),He&&n&&M!==null&&M.l!==null&&(M.l.s??=[]).push(r),r}function I(e,t,n=!1){return H!==null&&(!Wn||H.f&131072)&&Je()&&H.f&4325394&&(qn===null||!qn.has(e))&&Be(),Kt(e,n?Zt(t):t,Ot)}var Wt=null,Gt=0;function Kt(e,t,n=null){if(!e.equals(t)){Hn?Bt.set(e,t):Bt.has(e)||Bt.set(e,e.v);var r=jt.ensure();if(r.capture(e,t),e.f&2){let t=e;e.f&2048&&_t(t),Ct===null&&tt(t)}e.wv=rr(),Wt=null,Gt=0,Xt(e,y,n),Wt=null,Je()&&U!==null&&U.f&1024&&!(U.f&96)&&(Zn===null?Qn([e]):Zn.push(e)),!r.is_fork&&zt.size>0&&!Vt&&qt()}return t}function qt(){Vt=!1;for(let e of zt){e.f&1024&&et(e,b);let t;try{t=ir(e)}catch{t=!0}t&&ur(e)}zt.clear()}function Jt(e,t=1){var n=W(e),r=t===1?n++:n--;return I(e,n),r}function Yt(e){I(e,e.v+1)}function Xt(e,t,n){var r=e.reactions;if(r!==null){var i=Je(),a=r.length;if(Gt+=a,Gt>1e5&&Wt===null&&(Wt=new Set),Wt!==null){if(Wt.has(e))return;Wt.add(e)}for(var o=0;o{if(tr===d)return e();var t=H,n=tr;Gn(null),nr(d);var r=e();return Gn(t),nr(n),r};return i&&r.set(`length`,F(t.length,u)),new Proxy(t,{defineProperty(e,t,n){(!(`value`in n)||n.configurable===!1||n.enumerable===!1||n.writable===!1)&&Re();var i=r.get(t);return i===void 0?f(()=>{var e=F(n.value,u);return r.set(t,e),e}):I(i,n.value,!0),!0},deleteProperty(e,t){var n=r.get(t);if(n===void 0){if(t in e){let e=f(()=>F(he,u));r.set(t,e),Yt(o)}}else I(n,he),Yt(o);return!0},get(e,n,i){if(n===re)return t;var o=r.get(n),s=n in e;if(o===void 0&&(!s||a(e,n)?.writable)&&(o=f(()=>F(Zt(s?e[n]:he),u)),r.set(n,o)),o!==void 0){var c=W(o);return c===he?void 0:c}return Reflect.get(e,n,i)},getOwnPropertyDescriptor(e,t){this.has?.(e,t);var n=Reflect.getOwnPropertyDescriptor(e,t),i=r.get(t);if(i!==void 0){var a=W(i);if(a===he)return;if(n&&`value`in n)n.value=a;else return{enumerable:!0,configurable:!0,value:a,writable:!0}}return n},has(e,t){if(t===re)return!0;var n=r.get(t),i=n!==void 0&&n.v!==he||Reflect.has(e,t);return(n!==void 0||U!==null&&(!i||a(e,t)?.writable))&&(n===void 0&&(n=f(()=>F(i?Zt(e[t]):he,u)),r.set(t,n)),W(n)===he)?!1:i},set(e,t,n,s){var c=r.get(t),l=t in e;if(i&&t===`length`)for(var d=n;dF(he,u)),r.set(d+``,p)):I(p,he)}if(c===void 0)(!l||a(e,t)?.writable)&&(c=f(()=>F(void 0,u)),I(c,Zt(n)),r.set(t,c));else{l=c.v!==he;var m=f(()=>Zt(n));I(c,m)}var h=Reflect.getOwnPropertyDescriptor(e,t);if(h?.set&&h.set.call(s,n),!l){if(i&&typeof t==`string`){var g=r.get(`length`),_=Number(t);Number.isInteger(_)&&_>=g.v&&I(g,_+1)}Yt(o)}return!0},ownKeys(e){W(o);var t=Reflect.ownKeys(e).filter(e=>{var t=r.get(e);return t===void 0||t.v!==he});for(var[n,i]of r)i.v!==he&&!(n in e)&&t.push(n);return t},setPrototypeOf(){ze()}})}function Qt(e){try{if(typeof e==`object`&&e&&re in e)return e[re]}catch{}return e}function $t(e,t){return Object.is(Qt(e),Qt(t))}var en,tn,nn,rn;function an(){if(en===void 0){en=window,tn=/Firefox/.test(navigator.userAgent);var e=Element.prototype,t=Node.prototype,n=Text.prototype;nn=a(t,`firstChild`).get,rn=a(t,`nextSibling`).get,u(e)&&(e[ce]=void 0,e[se]=null,e[le]=void 0,e.__e=void 0),u(n)&&(n[ue]=void 0)}}function on(e=``){return document.createTextNode(e)}function sn(e){return nn.call(e)}function cn(e){return rn.call(e)}function L(e,t){if(!k)return sn(e);var n=sn(A);if(n===null)n=A.appendChild(on());else if(t&&n.nodeType!==3){var r=on();return n?.before(r),Se(r),r}return t&&fn(n),Se(n),n}function R(e,t=!1){if(!k){var n=sn(e);return n instanceof Comment&&n.data===``?cn(n):n}if(t){if(A?.nodeType!==3){var r=on();return A?.before(r),Se(r),r}fn(A)}return A}function z(e,t=!1){if(!k)return sn(e);var n=L(e,t);return j(e),n}function B(e,t=1,n=!1){let r=k?A:e;for(var i;t--;)i=r,r=cn(r);if(!k)return r;if(n){if(r?.nodeType!==3){var a=on();return r===null?i?.after(a):r.before(a),Se(a),a}fn(r)}return Se(r),r}function ln(e){e.textContent=``}function un(){return!1}function dn(e,t,n){return t==null||t===`http://www.w3.org/1999/xhtml`?n?document.createElement(e,{is:n}):document.createElement(e):n?document.createElementNS(t,e,{is:n}):document.createElementNS(t,e)}function fn(e){if(e.nodeValue.length<65536)return;let t=e.nextSibling;for(;t!==null&&t.nodeType===3;)t.remove(),e.nodeValue+=t.nodeValue,t=e.nextSibling}function pn(e){var t=U;if(t===null)return H.f|=ne,e;if(!(t.f&32768)&&!(t.f&4))throw e;mn(e,t)}function mn(e,t){if(!(t!==null&&t.f&16384)){for(;t!==null;){if(t.f&128&&!(t.f&33570816)){if(!(t.f&32768))throw e;try{t.b.error(e);return}catch(t){e=t}}t=t.parent}throw e}}function hn(e){U===null&&(H===null&&Fe(e),Pe()),Hn&&Ne(e)}function gn(e,t){var n=t.last;n===null?t.last=t.first=e:(n.next=e,e.prev=n,t.last=e)}function _n(e,t){var n=U;n!==null&&n.f&8192&&(e|=x);var r={ctx:M,deps:null,nodes:null,f:e|y|512,first:null,fn:t,last:null,next:null,parent:n,b:n&&n.b,prev:null,teardown:null,wv:0,ac:null};P?.register_created_effect(r);var i=r;if(e&4)Dt===null?jt.ensure().schedule(r):Dt.push(r);else if(t!==null){try{ur(r)}catch(e){throw Mn(r),e}i.deps===null&&i.teardown===null&&i.nodes===null&&i.first===i.last&&!(i.f&524288)&&(i=i.first,e&16&&e&65536&&i!==null&&(i.f|=T))}if(i!==null&&(i.parent=n,n!==null&&gn(i,n),H!==null&&H.f&2&&!(e&64))){var a=H;(a.effects??=[]).push(i)}return r}function vn(){return H!==null&&!Wn}function yn(e){let t=_n(8,null);return et(t,v),t.teardown=e,t}function bn(e){hn(`$effect`);var t=U.f;if(!H&&t&32&&M!==null&&!M.i){var n=M;(n.e??=[]).push(e)}else return xn(e)}function xn(e){return _n(4|E,e)}function Sn(e){return hn(`$effect.pre`),_n(8|E,e)}function Cn(e){jt.ensure();let t=_n(64|ee,e);return(e={})=>new Promise(n=>{e.outro?Fn(t,()=>{Mn(t),n(void 0)}):(Mn(t),n(void 0))})}function wn(e){return _n(4,e)}function Tn(e){return _n(te|ee,e)}function En(e,t=0){return _n(8|t,e)}function V(e,t=[],n=[],r=[]){ct(r,t,n,t=>{_n(8,()=>{e(...t.map(W))})})}function Dn(e,t=0){return _n(16|t,e)}function On(e){return _n(32|ee,e)}function kn(e){var t=e.teardown;if(t!==null){let n=Hn,r=H;Un(!0),Gn(null);try{t.call(null)}catch(t){mn(t,e.parent)}finally{Un(n),Gn(r)}}}function An(e,t=!1){var n=e.first;for(e.first=e.last=null;n!==null;){let e=n.ac;e!==null&&ot(()=>{e.abort(fe)});var r=n.next;n.f&64?n.parent=null:Mn(n,t),n=r}}function jn(e){for(var t=e.first;t!==null;){var n=t.next;t.f&32||Mn(t),t=n}}function Mn(e,t=!0){var n=!1;(t||e.f&262144)&&e.nodes!==null&&e.nodes.end!==null&&(Nn(e.nodes.start,e.nodes.end),n=!0),e.f|=w,An(e,t&&!n),lr(e,0);var r=e.nodes&&e.nodes.t;if(r!==null)for(let e of r)e.stop();kn(e),e.f^=w,e.f|=S;var i=e.parent;i!==null&&i.first!==null&&Pn(e),e.next=e.prev=e.teardown=e.ctx=e.deps=e.fn=e.nodes=e.ac=e.b=null}function Nn(e,t){for(;e!==null;){var n=e===t?null:cn(e);e.remove(),e=n}}function Pn(e){var t=e.parent,n=e.prev,r=e.next;n!==null&&(n.next=r),r!==null&&(r.prev=n),t!==null&&(t.first===e&&(t.first=r),t.last===e&&(t.last=n))}function Fn(e,t,n=!0){var r=[];e.f|=256,In(e,r,!0);var i=()=>{n&&Mn(e),t&&t()},a=r.length;if(a>0){var o=()=>--a||i();for(var s of r)s.out(o)}else i()}function In(e,t,n){if(!(e.f&8192)){e.f^=x;var r=e.nodes&&e.nodes.t;if(r!==null)for(let e of r)(e.is_global||n)&&t.push(e);for(var i=e.first;i!==null;){var a=i.next;if(!(i.f&64)){var o=!!(i.f&65536)||!!(i.f&32)&&!!(e.f&16);In(i,t,o?n:!1)}i=a}}}function Ln(e){e.f&=-257,Rn(e,!0)}function Rn(e,t){if(!(e.f&256)&&e.f&8192){e.f^=x,e.f&1024||(et(e,y),jt.ensure().schedule(e));for(var n=e.first;n!==null;){var r=n.next,i=!!(n.f&65536)||!!(n.f&32);Rn(n,i?t:!1),n=r}var a=e.nodes&&e.nodes.t;if(a!==null)for(let e of a)(e.is_global||t)&&e.in()}}function zn(e,t){if(e.nodes)for(var n=e.nodes.start,r=e.nodes.end;n!==null;){var i=n===r?null:cn(n);t.append(n),n=i}}var Bn=null,Vn=!1,Hn=!1;function Un(e){Hn=e}var H=null,Wn=!1;function Gn(e){H=e}var U=null;function Kn(e){U=e}var qn=null;function Jn(e){H!==null&&(H.f&2097152||H.f&2)&&(qn??=new Set).add(e)}var Yn=null,Xn=0,Zn=null;function Qn(e){Zn=e}var $n=1,er=0,tr=er;function nr(e){tr=e}function rr(){return++$n}function ir(e){var t=e.f;if(t&2048)return!0;if(t&4096){for(var n=e.deps,r=n.length,i=0;ie.wv)return!0}t&512&&Ct===null&&et(e,v)}return!1}function ar(e,t,n=!0){var r=e.reactions;if(r!==null&&!(qn!==null&&qn.has(e)))for(var i=0;i{e.ac.abort(fe)}),e.ac=null);try{e.f|=O;var u=e.fn,d=u();e.f|=C;var f=sr(e);if(Je()&&Zn!==null&&!Wn&&f!==null&&!(e.f&6146))for(var p=0;p0)for(t.length=Xn+Yn.length,r=0;r{s.ac.abort(fe),s.ac=null,et(s,y)}),yt(s),lr(s,0)}}function lr(e,t){var n=e.deps;if(n!==null)for(var r=t;rn?.call(this,e))}return e.startsWith(`pointer`)||e.startsWith(`touch`)||e===`wheel`?(i.__removed=!1,Ze(()=>{i.__removed||t.addEventListener(e,i,r)})):t.addEventListener(e,i,r),i}function Cr(e,t,n,r,i){var a={capture:r,passive:i},o=Sr(e,t,n,a);(t===document.body||t===window||t===document||t instanceof HTMLMediaElement)&&yn(()=>{o.__removed=!0,t.removeEventListener(e,o,a)})}function G(e,t,n){(t[yr]??={})[e]=n}function wr(e){for(var t=0;t{Er=!1,Tr=null}));var s=0,c=Tr===e&&e[yr];if(c){var l=a.indexOf(c);if(l!==-1&&(t===document||t===window)){e[yr]=t;return}var u=a.indexOf(t);if(u===-1)return;l<=u&&(s=l)}if(o=a[s]||e.target,o!==t){i(e,`currentTarget`,{configurable:!0,get(){return o||n}});var d=H,f=U;Gn(null),Kn(null);try{for(var p,m=[];o!==null&&o!==t;){try{var h=o[yr]?.[r];h!=null&&(!o.disabled||e.target===o)&&h.call(o,e)}catch(e){p?m.push(e):p=e}if(e.cancelBubble)break;s++,o=s{throw e});throw p}}finally{e[yr]=t,delete e.currentTarget,Gn(d),Kn(f)}}}var Or=globalThis?.window?.trustedTypes&&globalThis.window.trustedTypes.createPolicy(`svelte-trusted-html`,{createHTML:e=>e});function kr(e){return Or?.createHTML(e)??e}function Ar(e){var t=dn(`template`);return t.innerHTML=kr(e.replaceAll(``,``)),t.content}function jr(e,t){var n=U;n.nodes===null&&(n.nodes={start:e,end:t,a:null,t:null})}function K(e,t){var n=!!(t&1),r=!!(t&2),i,a=!e.startsWith(``);return()=>{if(k)return jr(A,null),A;i===void 0&&(i=Ar(a?e:``+e),n||(i=sn(i)));var t=r||tn?document.importNode(i,!0):i.cloneNode(!0);if(n){var o=sn(t),s=t.lastChild;jr(o,s)}else jr(t,t);return t}}function Mr(e,t,n=`svg`){var r=!e.startsWith(``),i=!!(t&1),a=`<${n}>${r?e:``+e}`,o;return()=>{if(k)return jr(A,null),A;if(!o){var e=sn(Ar(a));if(i)for(o=document.createDocumentFragment();sn(e);)o.appendChild(sn(e));else o=sn(e)}var t=o.cloneNode(!0);if(i){var n=sn(t),r=t.lastChild;jr(n,r)}else jr(t,t);return t}}function Nr(e,t){return Mr(e,t,`svg`)}function Pr(e=``){if(!k){var t=on(e+``);return jr(t,t),t}var n=A;return n.nodeType===3?fn(n):(n.before(n=on()),Se(n)),jr(n,n),n}function Fr(){if(k)return jr(A,null),A;var e=document.createDocumentFragment(),t=document.createComment(``),n=on();return e.append(t,n),jr(t,n),e}function q(e,t){if(k){var n=U;(!(n.f&32768)||n.nodes.end===null)&&(n.nodes.end=A),Ce();return}e!==null&&e.before(t)}function Ir(){if(k&&A&&A.nodeType===8&&A.textContent?.startsWith(`$`)){let e=A.textContent.substring(1);return Ce(),e}return(window.__svelte??={}).uid??=1,`c${window.__svelte.uid++}`}function Lr(e){let t=0,n=Ht(0),r;return()=>{vn()&&(W(n),En(()=>(t===0&&(r=mr(()=>e(()=>Yt(n)))),t+=1,()=>{Ze(()=>{--t,t===0&&(r?.(),r=void 0,Yt(n))})})))}}var Rr=T|ee;function zr(e,t,n,r){new Br(e,t,n,r)}var Br=class{parent;is_pending=!1;transform_error;#e;#t=k?A:null;#n;#r;#i;#a=null;#o=null;#s=null;#c=null;#l=0;#u=0;#d=!1;#f=new Set;#p=new Set;#m=null;#h=Lr(()=>(this.#m=Ht(this.#l),()=>{this.#m=null}));constructor(e,t,n,r){this.#e=e,this.#n=t,this.#r=e=>{var t=U;t.b=this,t.f|=128,n(e)},this.parent=U.b,this.transform_error=r??this.parent?.transform_error??(e=>e),this.#i=Dn(()=>{if(k){let e=this.#t;Ce();let t=e.data===`[!`;if(e.data.startsWith(`[?`)){let t=JSON.parse(e.data.slice(2));this.#_(t)}else t?this.#y():this.#g()}else this.#b()},Rr),k&&(this.#e=A)}#g(){try{this.#a=On(()=>this.#r(this.#e))}catch(e){this.error(e)}}#_(e){let t=this.#n.failed,{reset:n,invoke_onerror:r}=this.#v(e);Ze(r),t&&(this.#s=On(()=>{t(this.#e,()=>e,()=>n)}))}#v(e){var t=!1,n=!1;let r=()=>{if(t){be();return}t=!0,n&&Ve(),this.#s!==null&&Fn(this.#s,()=>{this.#s=null}),this.#S(()=>{this.#b()})};return{reset:r,invoke_onerror:()=>{try{n=!0,this.#n.onerror?.(e,r),n=!1}catch(e){mn(e,this.#i&&this.#i.parent)}}}}#y(){let e=this.#n.pending;e&&(this.is_pending=!0,this.#o=On(()=>e(this.#e)),Ze(()=>{var e=this.#c=document.createDocumentFragment(),t=on(),n=!1;if(e.append(t),this.#a=this.#S(()=>{try{return On(()=>this.#r(t))}catch(e){try{this.error(e),n=!0}catch(e){mn(e,this.#i.parent)}return null}}),this.#a===null){this.#c=null,n&&this.#x(P);return}this.#u===0&&(this.#e.before(e),this.#c=null,Fn(this.#o,()=>{this.#o=null}),this.#x(P))}))}#b(){try{if(this.is_pending=this.has_pending_snippet(),this.#u=0,this.#l=0,this.#a=On(()=>{this.#r(this.#e)}),this.#u>0){var e=this.#c=document.createDocumentFragment();zn(this.#a,e);let t=this.#n.pending;this.#o=On(()=>t(this.#e))}else this.#x(P)}catch(e){this.error(e)}}#x(e){this.is_pending=!1,e.transfer_effects(this.#f,this.#p)}defer_effect(e){nt(e,this.#f,this.#p)}is_rendered(){return!this.is_pending&&(!this.parent||this.parent.is_rendered())}has_pending_snippet(){return!!this.#n.pending}#S(e){var t=U,n=H,r=M;Kn(this.#i),Gn(this.#i),We(this.#i.ctx);try{return jt.ensure(),e()}finally{Kn(t),Gn(n),We(r)}}#C(e,t){if(!this.has_pending_snippet()){this.parent&&this.parent.#C(e,t);return}this.#u+=e,this.#u===0&&(this.#x(t),this.#o&&Fn(this.#o,()=>{this.#o=null}),this.#c&&=(this.#e.before(this.#c),null))}update_pending_count(e,t){this.#C(e,t),this.#l+=e,!(!this.#m||this.#d)&&(this.#d=!0,Ze(()=>{this.#d=!1,this.#m&&Kt(this.#m,this.#l)}))}get_effect_pending(){return this.#h(),W(this.#m)}error(e){if(!this.#n.onerror&&!this.#n.failed)throw e;P?.is_fork?(this.#a&&P.skip_effect(this.#a),this.#o&&P.skip_effect(this.#o),this.#s&&P.skip_effect(this.#s),P.oncommit(()=>{this.#w(e)})):this.#w(e)}#w(e){this.#a&&=(Mn(this.#a),null),this.#o&&=(Mn(this.#o),null),this.#s&&=(Mn(this.#s),null),k&&(Se(this.#t),we(),Se(Te()));let t=this.#n.failed,n=e=>{let{reset:n,invoke_onerror:r}=this.#v(e);r(),t&&(this.#s=this.#S(()=>{try{return On(()=>{var r=U;r.b=this,r.f|=128,t(this.#e,()=>e,()=>n)})}catch(e){return mn(e,this.#i.parent),null}}))};Ze(()=>{var t;try{t=this.transform_error(e)}catch(e){mn(e,this.#i&&this.#i.parent);return}typeof t==`object`&&t&&typeof t.then==`function`?t.then(n,e=>mn(e,this.#i&&this.#i.parent)):n(t)})}};function J(e,t){var n=t==null?``:typeof t==`object`?`${t}`:t;n!==(e[ue]??=e.nodeValue)&&(e[ue]=n,e.nodeValue=`${n}`)}function Vr(e,t){return Ur(e,t)}var Hr=new Map;function Ur(e,{target:t,anchor:n,props:i={},events:a,context:o,intro:s=!0,transformError:c}){an();var l=void 0,u=Cn(()=>{var s=n??t.appendChild(on());zr(s,{pending:()=>{}},t=>{Ge({});var n=M;if(o&&(n.c=o),a&&(i.$$events=a),k&&jr(t,null),l=e(t,i)||qe(),k&&(U.nodes.end=A,A===null||A.nodeType!==8||A.data!==`]`))throw ve(),me;Ke()},c);var u=new Set,d=e=>{for(var n=0;n{for(var e of u)for(let n of[t,document]){var r=Hr.get(n),i=r.get(e);--i==0?(n.removeEventListener(e,Dr),r.delete(e),r.size===0&&Hr.delete(n)):r.set(e,i)}xr.delete(d),s!==n&&s.parentNode?.removeChild(s)}});return Wr.set(l,u),l}var Wr=new WeakMap,Gr=class{anchor;#e=new Map;#t=new Map;#n=new Map;#r=new Set;#i=!0;constructor(e,t=!0){this.anchor=e,this.#i=t}#a=e=>{if(this.#e.has(e)){var t=this.#e.get(e),n=this.#t.get(t);if(n)Ln(n),this.#r.delete(t);else{var r=this.#n.get(t);r&&(Ln(r.effect),this.#t.set(t,r.effect),this.#n.delete(t),r.fragment.lastChild.remove(),this.anchor.before(r.fragment),n=r.effect)}for(let[t,n]of this.#e){if(this.#e.delete(t),t===e)break;let r=this.#n.get(n);r&&(Mn(r.effect),this.#n.delete(n))}for(let[e,r]of this.#t){if(e===t||this.#r.has(e))continue;let i=()=>{if(Array.from(this.#e.values()).includes(e)){var t=document.createDocumentFragment();zn(r,t),t.append(on()),this.#n.set(e,{effect:r,fragment:t})}else Mn(r);this.#r.delete(e),this.#t.delete(e)};this.#i||!n?(this.#r.add(e),Fn(r,i,!1)):i()}}};#o=e=>{this.#e.delete(e);let t=Array.from(this.#e.values());for(let[e,n]of this.#n)t.includes(e)||(Mn(n.effect),this.#n.delete(e))};ensure(e,t){var n=P,r=un();if(t&&!this.#t.has(e)&&!this.#n.has(e)){if(r){var i=document.createDocumentFragment(),a=on();i.append(a),this.#n.set(e,{effect:On(()=>t(a)),fragment:i})}else this.#t.set(e,On(()=>t(this.anchor)))}if(this.#e.set(n,e),r){for(let[t,r]of this.#t)t===e?n.unskip_effect(r):n.skip_effect(r);for(let[t,r]of this.#n)t===e?n.unskip_effect(r.effect):n.skip_effect(r.effect);n.oncommit(this.#a),n.ondiscard(this.#o)}else k&&(this.anchor=A),this.#a(n)}};function Y(e,t,n=!1){var r;k&&(r=A,Ce());var i=new Gr(e),a=n?T:0;function o(e,t){if(k){var n=Ee(r);if(e!==parseInt(n.substring(1))){var a=Te();Se(a),i.anchor=a,xe(!1),i.ensure(e,t),xe(!0);return}}i.ensure(e,t)}Dn(()=>{var e=!1;t((t,n=0)=>{e=!0,o(n,t)}),e||o(-1,null)},a)}var Kr=Symbol(`NaN`);function qr(e,t,n){k&&Ce();var r=new Gr(e),i=!Je();Dn(()=>{var e=t();e!==e&&(e=Kr),i&&typeof e==`object`&&e&&(e={}),r.ensure(e,n)})}function X(e,t){return t}function Jr(e,t,n){for(var i=[],a=t.length,o,s=t.length,c=0;c{if(o){if(o.pending.delete(n),o.done.add(n),o.pending.size===0){var t=e.outrogroups;Yr(e,r(o.done)),t.delete(o),t.size===0&&(e.outrogroups=null)}}else--s},!1)}if(s===0){var l=i.length===0&&n!==null&&e.pending.size===0;if(l){var u=n,d=u.parentNode;ln(d),d.append(u),e.items.clear()}Yr(e,t,!l)}else o={pending:new Set(t),done:new Set},(e.outrogroups??=new Set).add(o)}function Yr(e,t,n=!0){var r;if(e.pending.size>0){r=new Set;for(let t of e.pending.values())for(let n of t)r.add(e.items.get(n).e)}for(var i=0;i{var t=i();return e(t)?t:t==null?[]:r(t)}),p,m=new Map,h=!0;function g(e){v.effect.f&16384||(v.pending.delete(e),v.fallback=d,Qr(v,p,c,n,a),d!==null&&(p.length===0?d.f&33554432?(d.f^=D,ei(d,null,c)):Ln(d):Fn(d,()=>{d=null})))}function _(e){v.pending.delete(e)}var v={effect:Dn(()=>{p=W(f);var e=p.length;let t=!1;k&&Ee(c)===`[!`!=(e===0)&&(c=Te(),Se(c),xe(!1),t=!0);for(var r=new Set,u=P,v=un(),y=0;ys(c)):(d=On(()=>s(Xr??=on())),d.f|=D)),e>r.size&&Me(``,``,``),k&&e>0&&Se(Te()),!h){if(m.set(u,r),v){for(let[e,t]of l)r.has(e)||u.skip_effect(t.e);u.oncommit(g),u.ondiscard(_)}else g(u)}t&&xe(!0),W(f)}),flags:n,items:l,pending:m,outrogroups:null,fallback:d};h=!1,k&&(c=A)}function Zr(e){for(;e!==null&&!(e.f&32);)e=e.next;return e}function Qr(e,t,n,i,a){var o=!!(i&8),s=t.length,c=e.items,l=Zr(e.effect.first),u,d=null,f,p=[],m=[],h,g,_,v;if(o)for(v=0;v0){var ee=i&4&&s===0?n:null;if(o){for(v=0;v{if(f!==void 0)for(_ of f)_.nodes?.a?.apply()})}function $r(e,t,n,r,i,a,o,s){var c=o&1?o&16?Ht(n):Ut(n,!1,!1):null,l=o&2?Ht(i):null;return{v:c,i:l,e:On(()=>(a(t,c??n,l??i,s),()=>{e.delete(r)}))}}function ei(e,t,n){if(e.nodes)for(var r=e.nodes.start,i=e.nodes.end,a=t&&!(t.f&33554432)?t.nodes.start:n;r!==null;){var o=cn(r);if(a.before(r),r===i)return;r=o}}function ti(e,t,n){t===null?e.effect.first=n:t.next=n,n===null?e.effect.last=t:n.prev=t}function ni(e,t,n){var r;k&&(r=A,Ce());var i=new Gr(e);Dn(()=>{var e=t()??null;if(k&&Ee(r)===`[`!=(e!==null)){var a=Te();Se(a),i.anchor=a,xe(!1),i.ensure(e,e&&(t=>n(t,e))),xe(!0);return}i.ensure(e,e&&(t=>n(t,e)))},T)}var ri=[...` +\r\f\xA0\v`];function ii(e,t,n){var r=e==null?``:``+e;if(t&&(r=r?r+` `+t:t),n){for(var i of Object.keys(n))if(n[i])r=r?r+` `+i:i;else if(r.length)for(var a=i.length,o=0;(o=r.indexOf(i,o))>=0;){var s=o+a;(o===0||ri.includes(r[o-1]))&&(s===r.length||ri.includes(r[s]))?r=(o===0?``:r.substring(0,o))+r.substring(s+1):o=s}}return r===``?null:r}function ai(e,t=!1){var n=t?` !important;`:`;`,r=``;for(var i of Object.keys(e)){var a=e[i];a!=null&&a!==``&&(r+=` `+i+`: `+a+n)}return r}function oi(e){return e[0]!==`-`||e[1]!==`-`?e.toLowerCase():e}function si(e,t){if(t){var n=``,r,i;if(Array.isArray(t)?(r=t[0],i=t[1]):r=t,e){e=String(e).replaceAll(/\/\*.*?\*\//g,``).trim();var a=!1,o=0,s=!1,c=[];r&&c.push(...Object.keys(r).map(oi)),i&&c.push(...Object.keys(i).map(oi));var l=0,u=-1;let t=e.length;for(var d=0;d{t.every(_i)||(`__defaultValue`in e&&fi(e,!1),`__value`in e&&pi(e,e.__value))});t.observe(e,{childList:!0,subtree:!0,attributes:!0,attributeFilter:[`value`]}),yn(()=>{t.disconnect()})}function hi(e,t,n=t){var r=new WeakSet,i=!0;st(e,`change`,t=>{var i=t?`[selected]`:`:checked`,a;if(e.multiple)a=[].map.call(e.querySelectorAll(i),gi);else{var o=e.querySelector(i)??e.querySelector(`option:not([disabled])`);a=o&&gi(o)}n(a),e.__value=a,P!==null&&r.add(P)}),wn(()=>{var a=t();if(e===document.activeElement){var o=P;if(r.has(o))return}if(pi(e,a,i),i&&a===void 0){var s=e.querySelector(`:checked`);s!==null&&(a=gi(s),n(a))}e.__value=a,i=!1})}function gi(e){return`__value`in e?e.__value:e.value}function _i(e){if(e.target.closest(`selectedcontent`)!==null)return!0;if(e.type===`childList`){var t=[...e.addedNodes,...e.removedNodes];return t.length>0&&t.every(e=>e.nodeName===`SELECTEDCONTENT`)}return!1}var vi=Symbol(`is custom element`),yi=Symbol(`is html`),bi=pe?`link`:`LINK`;function xi(e){if(k){var t=!1,n=()=>{if(!t){if(t=!0,e.hasAttribute(`value`)){var n=e.value;Q(e,`value`,null),e.value=n}if(e.hasAttribute(`checked`)){var r=e.checked;Q(e,`checked`,null),e.checked=r}}};e[de]=n,Ze(n),at()}}function Si(e,t){var n=Ci(e);n.checked!==(n.checked=t??void 0)&&(e.checked=t)}function Q(e,t,n,r){var i=Ci(e);k&&(i[t]=e.getAttribute(t),t===`src`||t===`srcset`||t===`href`&&e.nodeName===bi)||i[t]!==(i[t]=n)&&(t===`loading`&&(e[oe]=n),n==null?e.removeAttribute(t):typeof n!=`string`&&Ti(e).has(t)?e[t]=n:e.setAttribute(t,n))}function Ci(e){return e[se]??={[vi]:e.nodeName.includes(`-`),[yi]:e.namespaceURI===ge}}var wi=new Map;function Ti(e){var t=e.getAttribute(`is`)||e.nodeName,n=wi.get(t);if(n)return n;wi.set(t,n=new Set);for(var r,i=e,a=Element.prototype;a!==i;){for(var s in r=o(i),r)r[s].set&&s!==`innerHTML`&&s!==`textContent`&&s!==`innerText`&&n.add(s);i=l(i)}return n}function Ei(e,t,n=t){var r=new WeakSet;st(e,`input`,async i=>{var a=i?e.defaultValue:e.value;if(a=Ai(e)?ji(a):a,n(a),P!==null&&r.add(P),await dr(),a!==(a=t())){var o=e.selectionStart,s=e.selectionEnd,c=e.value.length;if(e.value=a??``,s!==null){var l=e.value.length;o===s&&s===c&&l>c?(e.selectionStart=l,e.selectionEnd=l):(e.selectionStart=o,e.selectionEnd=Math.min(s,l))}}}),(k&&e.defaultValue!==e.value||mr(t)==null&&e.value)&&(n(Ai(e)?ji(e.value):e.value),P!==null&&r.add(P)),En(()=>{var n=t();if(e===document.activeElement){var i=P;if(r.has(i))return}Ai(e)&&n===ji(e.value)||(e.type!==`date`||n||e.value)&&n!==e.value&&(e.value=n??``)})}var Di=new Set;function Oi(e,t,n,r,i=r){var a=n.getAttribute(`type`)===`checkbox`,o=e;let s=!1;if(t!==null)for(var c of t)o=o[c]??=[];o.push(n),st(n,`change`,()=>{var e=n.__value;a&&(e=ki(o,e,n.checked)),i(e)},()=>i(a?[]:null)),En(()=>{var e=r();if(k&&n.defaultChecked!==n.checked){s=!0;return}a?(e||=[],n.checked=e.includes(n.__value)):n.checked=$t(n.__value,e)}),yn(()=>{var e=o.indexOf(n);e!==-1&&o.splice(e,1)}),Di.has(o)||(Di.add(o),Ze(()=>{o.sort((e,t)=>e.compareDocumentPosition(t)===4?-1:1),Di.delete(o)})),Ze(()=>{if(s){var e=a?ki(o,e,n.checked):o.find(e=>e.checked)?.__value;i(e)}})}function ki(e,t,n){for(var r=new Set,i=0;i{var n=this.#e.get(e);n.delete(t),n.size===0&&(this.#e.delete(e),this.#t.unobserve(e))}}#r(){return this.#t??=new ResizeObserver(t=>{for(var n of t){e.entries.set(n.target,n);for(var r of this.#e.get(n.target)||[])r(n)}})}}({box:`border-box`});function Ni(e,t,n){var r=Mi.observe(e,()=>n(e[t]));wn(()=>(mr(()=>n(e[t])),r))}function Pi(e,t){return e===t||e?.[re]===t}function Fi(e=qe(),t,n,r){var i=M.r,a=U;return wn(()=>{var o,s;return En(()=>{o=s,s=r?.()||[],mr(()=>{Pi(n(...s),e)||(t(e,...s),o&&Pi(n(...o),e)&&t(null,...o))})}),()=>{let r=a;for(;r!==i&&r.parent!==null&&r.parent.f&33554432;)r=r.parent;let o=()=>{s&&Pi(n(...s),e)&&t(null,...s)},c=r.teardown;r.teardown=()=>{o(),c?.()}}}),e}function Ii(e,t,n,r,i){var a=()=>{r(n[e])};n.addEventListener(t,a),i?En(()=>{n[e]=i()}):a(),(n===document.body||n===window||n===document)&&yn(()=>{n.removeEventListener(t,a)})}function Li(e=!1){let t=M,n=t.l.u;if(!n)return;let r=()=>hr(t.s);if(e){let e=0,n={},i=ft(()=>{let r=!1,i=t.s;for(let e in i)i[e]!==n[e]&&(n[e]=i[e],r=!0);return r&&e++,e});r=()=>W(i)}n.b.length&&Sn(()=>{Ri(t,r),m(n.b)}),bn(()=>{let e=mr(()=>n.m.map(p));return()=>{for(let t of e)typeof t==`function`&&t()}}),n.a.length&&bn(()=>{Ri(t,r),m(n.a)})}function Ri(e,t){if(e.l.s)for(let t of e.l.s)W(t);t()}var zi=!1;function Bi(e){var t=zi;try{return zi=!1,[e(),zi]}finally{zi=t}}var Vi={get(e,t){let n=e.props.length;for(;n--;){let r=e.props[n];if(d(r)&&(r=r()),typeof r==`object`&&r&&t in r)return r[t]}},set(e,t,n){let r=e.props.length;for(;r--;){let i=e.props[r];d(i)&&(i=i());let o=a(i,t);if(o&&o.set)return o.set(n),!0}return!1},getOwnPropertyDescriptor(e,t){let n=e.props.length;for(;n--;){let r=e.props[n];if(d(r)&&(r=r()),typeof r==`object`&&r&&t in r){let e=a(r,t);return e&&!e.configurable&&(e.configurable=!0),e}}},has(e,t){if(t===re||t===ae)return!1;for(let n of e.props)if(d(n)&&(n=n()),n!=null&&t in n)return!0;return!1},ownKeys(e){let t=[];for(let n of e.props)if(d(n)&&(n=n()),n){for(let e in n)t.includes(e)||t.push(e);for(let e of Object.getOwnPropertySymbols(n))t.includes(e)||t.push(e)}return t}};function Hi(...e){return new Proxy({props:e},Vi)}function Ui(e,t,n,r){var i=!He||!!(n&2),o=!!(n&8),s=!!(n&16),c=r,l=!0,u=void 0,d=()=>s&&i?(u??=ft(r),W(u)):(l&&(l=!1,c=s?mr(r):r),c);let f;if(o){var p=re in e||ae in e;f=a(e,t)?.set??(p&&t in e?n=>e[t]=n:void 0)}var m,h=!1;o?[m,h]=Bi(()=>e[t]):m=e[t],m===void 0&&r!==void 0&&(m=d(),f&&(i&&Le(t),f(m)));var g=i?()=>{var n=e[t];return n===void 0?d():(l=!0,n)}:()=>{var n=e[t];return n!==void 0&&(c=void 0),n===void 0?c:n};if(i&&!(n&4))return g;if(f){var _=e.$$legacy;return(function(e,t){return arguments.length>0?((!i||!t||_||h)&&f(t?g():e),e):g()})}var v=!1,y=(n&1?ft:ht)(()=>(v=!1,g()));o&&W(y);var b=U;return(function(e,t){if(arguments.length>0){let n=t?W(y):i&&o?Zt(e):e;return I(y,n),v=!0,c!==void 0&&(c=n),e}return Hn&&v||b.f&16384?y.v:W(y)})}function Wi(e){M===null&&Ae(`onMount`),He&&M.l!==null?Ki(M).m.push(e):bn(()=>{let t=mr(e);if(typeof t==`function`)return t})}function Gi(e){M===null&&Ae(`onDestroy`),Wi(()=>()=>mr(e))}function Ki(e){var t=e.l;return t.u??={a:[],b:[],m:[]}}typeof window<`u`&&((window.__svelte??={}).v??=new Set).add(`5`);function qi(e,t){if(e instanceof RegExp)return{keys:!1,pattern:e};var n,r,i,a,o=[],s=``,c=e.split(`/`);for(c[0]||c.shift();i=c.shift();)n=i[0],n===`*`?(o.push(`wild`),s+=`/(.*)`):n===`:`?(r=i.indexOf(`?`,1),a=i.indexOf(`.`,1),o.push(i.substring(1,~r?r:~a?a:i.length)),s+=~r&&!~a?`(?:/([^/]+?))?`:`/([^/]+?)`,~a&&(s+=(~r?`?`:``)+`\\`+i.substring(a))):s+=`/`+i;return{keys:o,pattern:RegExp(`^`+s+(t?`(?=$|/)`:`/?$`),`i`)}}var Ji=new class{#e=F(Yi());get _loc(){return W(this.#e)}set _loc(e){I(this.#e,e)}#t=N(()=>this._loc.location);get _location(){return W(this.#t)}set _location(e){I(this.#t,e)}#n=N(()=>this._loc.querystring);get _querystring(){return W(this.#n)}set _querystring(e){I(this.#n,e)}#r=F(void 0);get _params(){return W(this.#r)}set _params(e){I(this.#r,e)}get loc(){return this._loc}get location(){return this._location}get querystring(){return this._querystring}get params(){return this._params}constructor(){typeof window<`u`?window.addEventListener(`hashchange`,()=>{this._loc=Yi()}):console.warn(`[svelte-spa-router] window 'window' is not defined, skipping initiation`)}};function Yi(){let e=typeof window<`u`?window.location.href:``,t=e.indexOf(`#/`),n=t>-1?e.substr(t+1):`/`,r=n.indexOf(`?`),i=``;return r>-1&&(i=n.substr(r+1),n=n.substr(0,r)),{location:n,querystring:i}}function Xi(e){e?window.scrollTo(e.__svelte_spa_router_scrollX||0,e.__svelte_spa_router_scrollY||0):window.scrollTo(0,0)}function Zi(e,t){Ge(t,!0);let n=Ui(t,`routes`,19,()=>({})),r=Ui(t,`prefix`,3,``),i=Ui(t,`restoreScrollState`,3,!1),a=Ui(t,`onConditionsFailed`,3,()=>{}),o=Ui(t,`onRouteLoaded`,3,()=>{}),s=Ui(t,`onRouteLoading`,3,()=>{}),c=Ui(t,`onRouteEvent`,3,()=>{});class l{path;component;conditions;userData;props;_pattern;_keys;constructor(e,t){let n=e=>typeof e==`object`&&!!e&&e._sveltesparouter===!0;if(!t||typeof t!=`function`&&!n(t))throw Error(`Invalid component object`);if(!e||typeof e==`string`&&(e.length<1||e.charAt(0)!=`/`&&e.charAt(0)!=`*`)||typeof e==`object`&&!(e instanceof RegExp))throw Error(`Invalid value for "path" argument - strings must start with / or *`);let r=qi(e);if(this.path=e,n(t)){let e=t;this.component=e.component,this.conditions=e.conditions||[],this.userData=e.userData,this.props=e.props||{}}else{let e=t;this.component=()=>Promise.resolve(e),this.conditions=[],this.props={}}this._pattern=r.pattern,this._keys=r.keys}match(e){if(r()){if(typeof r()==`string`){if(e.startsWith(r()))e=e.substr(r().length)||`/`;else return null}else if(r()instanceof RegExp){let t=e.match(r());if(t&&t[0])e=e.substr(t[0].length)||`/`;else return null}}let t=this._pattern.exec(e);if(t===null)return null;if(this._keys===!1)return t;let n={},i=0;for(;i{u.push(new l(t,e))}):Object.keys(n()).forEach(e=>{let t=n();u.push(new l(e,t[e]))});let d=F(null),f=F(null),p=F({}),m=null,h=null;bn(()=>{history.scrollRestoration=i()?`manual`:`auto`}),bn(()=>{if(!i())return;let e=e=>{m=e.state&&(e.state.__svelte_spa_router_scrollY||e.state.__svelte_spa_router_scrollX)?e.state:null};return window.addEventListener(`popstate`,e),()=>window.removeEventListener(`popstate`,e)});async function g(e,t){await dr(),e(t)}bn(()=>{let e=Ji.loc,t=!1;return mr(async()=>{let n=0;for(;n{t=!0}});function _(e){return e&&typeof e==`object`&&Object.keys(e).length?e:null}var v=Fr(),y=R(v),b=e=>{let t=N(()=>W(d));var n=Fr(),r=R(n),i=e=>{var n=Fr();ni(R(n),()=>W(t),(e,t)=>{t(e,Hi({get params(){return W(f)},get onRouteEvent(){return c()}},()=>W(p)))}),q(e,n)},a=e=>{var n=Fr();ni(R(n),()=>W(t),(e,t)=>{t(e,Hi({get onRouteEvent(){return c()}},()=>W(p)))}),q(e,n)};Y(r,e=>{W(f)?e(i):e(a,-1)}),q(e,n)};Y(y,e=>{W(d)&&e(b)}),q(e,v),Ke()}var Qi=[`bars`,`pace`,`zone`,`pie`,`fuel`,`resets`,`thresholds`],$i=`codexometer.web.preferences.v1`,ea={statusLine:null,tab:`quota`,view:`bars`,selected:``,defaultDetail:0,layouts:[]};function ta(){try{let e=JSON.parse(localStorage.getItem($i)||`null`);return!e||typeof e!=`object`?ea:{statusLine:Array.isArray(e.statusLine)?[...new Set(e.statusLine.filter(e=>typeof e==`string`&&e.length<64))].slice(0,32):null,tab:[`quota`,`sessions`,`usage`].includes(e.tab)?e.tab:`quota`,view:Qi.includes(e.view)?e.view:`bars`,selected:typeof e.selected==`string`?e.selected.slice(0,256):``,defaultDetail:+(e.defaultDetail===1),layouts:Array.isArray(e.layouts)?e.layouts.filter(e=>{if(!e||typeof e!=`object`)return!1;let t=e;return typeof t.id==`string`&&t.id.length<=256&&[0,1,2].includes(t.level)}).slice(-100):[]}}catch{return ea}}var na=Zt(ta()),ra=Zt({open:!1});function ia(){let e=JSON.stringify(na);try{localStorage.setItem($i,e)}catch{}}function aa(){return na.tab===`quota`?`/quota/`+na.view:`/`+na.tab}function oa(e){return na.layouts.find(t=>t.id===e)?.level??na.defaultDetail}function sa(e){na.defaultDetail=e,na.layouts=[]}function ca(e,t){na.layouts=[...na.layouts.filter(t=>t.id!==e),{id:e,level:Math.max(0,Math.min(2,t))}].slice(-100)}var $=Zt({data:null,connected:!1,error:``}),la=e=>e==null?`Unavailable`:e.toLocaleString(`en-GB`),ua=e=>!e||String(e).startsWith(`0001-`)?`Not yet available`:new Date(typeof e==`number`?e*1e3:e).toLocaleString(`en-GB`),da=`codexometer.web.session`,fa=class extends Error{},pa;async function ma(e,t,n){if(!pa||!$.connected||!$.data?.control||$.data.sessionsError)throw Error(`Session controls unavailable. Check the live connection.`);return await pa(e,t,n)}function ha(){let e=new AbortController,t,n=``;pa=async(t,r,i)=>{let a=await fetch(`/api/control/`+t,{method:`POST`,headers:{Authorization:`Bearer ${n}`,"Content-Type":`application/json`},body:JSON.stringify(r),signal:AbortSignal.any([e.signal,AbortSignal.timeout(8e3),...i?[i]:[]]),cache:`no-store`});if(!a.ok)throw a.status===502?Error(`Outcome uncertain. Check Codex before taking another action; nothing was retried.`):new fa(`Action rejected, expired or changed. Refresh and check Codex; nothing was retried.`);return a.json()};try{n=sessionStorage.getItem(da)||``}catch{}let r=location.hash,i=r.startsWith(`#pair=`)?r.slice(6):``;(i||!r||r===`#`)&&(history.replaceState(null,``,`/#`+aa()),window.dispatchEvent(new HashChangeEvent(`hashchange`)));async function a(){if(i)try{let t=await fetch(`/api/pair`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({secret:i}),signal:e.signal});if(!t.ok)throw Error(`Pairing link expired or already used. Restart --web for a fresh link.`);n=(await t.json()).token;try{sessionStorage.setItem(da,n)}catch{}}catch(t){e.signal.aborted||($.error=t instanceof Error?t.message:`Pairing failed`);return}if(!n){$.error=`Open the private pairing link printed by codexometer --web in your terminal.`;return}await o()}async function o(){try{let t=await fetch(`/api/events`,{headers:{Authorization:`Bearer ${n}`},signal:e.signal,cache:`no-store`});if(t.status===401){try{sessionStorage.removeItem(da)}catch{}$.error=`Browser session expired. Restart --web and open its new pairing link.`;return}if(!t.ok||!t.body)throw Error(`Live connection unavailable`);let r=t.body.getReader(),i=new TextDecoder,a=``;for(;!e.signal.aborted;){let{done:e,value:t}=await r.read();if(e)break;a+=i.decode(t,{stream:!0});let n;for(;(n=a.indexOf(` + +`))>=0;){let e=a.slice(0,n);a=a.slice(n+2),e.startsWith(`data: `)&&($.data=JSON.parse(e.slice(6)),$.connected=!0,$.error=``)}}}catch{}finally{$.connected=!1}e.signal.aborted||($.error=`Connection lost — showing last observation. Reconnecting…`,t=setTimeout(o,2500))}return a(),()=>{pa=void 0,e.abort(),clearTimeout(t),$.connected=!1}}Ue();var ga=K(`

      Quota refresh failed. Policy state is based on the last successful + observation.

      `),_a=K(`

      `),va=K(`

      The longest Codex quota window selects one active model profile. ASK + creates a per-session review; AUTO applies the profile on the next + eligible check.

      `,1),ya=K(`

      No threshold-based model steps were configured at launch.

      `);function ba(e,t){Ge(t,!1),Li();var n=Fr(),r=R(n),i=e=>{var t=va(),n=R(t),r=z(n),i=B(n,2),a=e=>{q(e,ga())};Y(i,e=>{$.data.quotaError&&e(a)});var o=B(i,2),s=L(o),c=z(s),l=B(s,4);Z(l,5,()=>$.data.thresholds,X,(e,t)=>{var n=_a();let r;var i=L(n),a=z(i),o=B(i,2),s=L(o),c=z(s,!0),l=z(B(s,2));j(o);var u=B(o,2),d=z(u,!0),f=z(B(u,2));j(n),V((e,i)=>{r=ci(n,1,``,null,r,{active:W(t).state===`ACTIVE`,next:W(t).state===`NEXT`}),J(a,`${W(t).threshold??``}%`),J(c,W(t).model),J(l,`${e??``} REASONING // ${i??``} + SPEED`),J(d,W(t).mode),J(f,`${W(t).state??``}${W(t).state===`NEXT`?` // ${W(t).remaining||0} PP TO GO`:``}`)},[()=>W(t).effort.toUpperCase(),()=>W(t).speed.toUpperCase()]),q(e,n)}),j(l),j(o),V(e=>{J(r,`MODEL STEP POLICY // OBSERVED ${e??``}`),J(c,`THRESHOLDS // ${$.data.thresholds.length??``} CONFIGURED`)},[()=>ua($.data.quotaAt)]),q(e,t)},a=e=>{q(e,ya())};Y(r,e=>{$.data?.thresholds?.length?e(i):e(a,-1)}),q(e,n),Ke()}var xa=Nr(` `,1),Sa=Nr(` `,1),Ca=K(` `),wa=K(`
      Quota observations
      Observed atPeriod elapsedConsumedTrail segment
      `),Ta=K(`


      Observed quota + path, not individual session usage. Gaps are not interpolated. Expand the + observation table for times, positions and gaps.

      OBSERVATION TABLE
      `,1),Ea=K(`
      CONSUMPTIONQUOTA PERIOD ELAPSED


      `,1);function Da(e,t){let n=Ir();Ge(t,!0);let r=Ui(t,`trail`,19,()=>[]),i=F(!1),a=[0,25,50,75,100],o=F(400),s=F(240),c=N(()=>W(o)-24),l=N(()=>W(s)-48),u=N(()=>Math.max(1,W(c)-48)),d=N(()=>Math.max(1,W(l)-20)),f=N(()=>48+Math.max(0,Math.min(100,t.elapsed))/100*W(u)),p=N(()=>W(l)-Math.max(0,Math.min(100,t.used))/100*W(d)),m=N(()=>t.used-t.elapsed),h=N(()=>r().map((e,t)=>`${t===0||e.break?`M`:`L`}${48+e.elapsed/100*W(u)} ${W(l)-e.used/100*W(d)}`).join(` `));var g=Ea(),_=R(g),v=L(_),y=L(v),b=z(y),x=B(y),S=B(x);Z(S,17,()=>a,X,(e,t)=>{var n=xa(),r=R(n),i=B(r),a=B(i),o=z(a),s=B(a),f=z(s);V(()=>{Q(r,`x1`,48+W(t)/100*W(u)),Q(r,`x2`,48+W(t)/100*W(u)),Q(r,`y2`,W(l)),Q(i,`y1`,W(l)-W(t)/100*W(d)),Q(i,`x2`,W(c)),Q(i,`y2`,W(l)-W(t)/100*W(d)),Q(a,`x`,48+W(t)/100*W(u)),Q(a,`y`,W(l)+20),J(o,`${W(t)??``}%`),Q(s,`y`,W(l)+4-W(t)/100*W(d)),J(f,`${W(t)??``}%`)}),q(e,n)});var C=B(S),w=B(C),T=B(w),ee=e=>{var t=Sa(),n=R(t),i=B(n),a=z(L(i));j(i),V(e=>{Q(n,`d`,W(h)),Q(i,`cx`,48+r()[0].elapsed/100*W(u)),Q(i,`cy`,W(l)-r()[0].used/100*W(d)),J(a,`First observation: ${e??``}`)},[()=>ua(r()[0].at)]),q(e,t)};Y(T,e=>{r().length&&e(ee)});var E=B(T,2),D=B(E),O=B(D),te=z(L(O));j(O),j(v),j(_);var ne=B(_,2),re=L(ne),ie=z(B(re,3),!0);j(ne);var ae=B(ne,2),oe=e=>{var t=Ta(),a=R(t),o=L(a);we(2),j(a);var s=B(a,2),c=B(L(s),2),l=e=>{var t=wa(),n=L(t),i=B(L(n),2);Z(i,21,r,X,(e,t,n)=>{var r=Ca(),i=L(r),a=L(i),o=z(a,!0);j(i);var s=B(i),c=z(s),l=B(s),u=z(l),d=z(B(l),!0);j(r),V((e,r)=>{Q(a,`datetime`,W(t).at),J(o,e),J(c,`${r??``}%`),J(u,`${W(t).used??``}%`),J(d,n===0?`First observation`:W(t).break?`Gap before this observation`:`Connected to previous observation`)},[()=>ua(W(t).at),()=>W(t).elapsed.toFixed(1)]),q(e,r)}),j(i),j(n),j(t),q(e,t)};Y(c,e=>{W(i)&&e(l)}),j(s),V(e=>{Q(a,`id`,n+`-trail-summary`),J(o,`○ START ${e??``} // ${r().length??``} + ${r().length===1?`OBSERVATION`:`OBSERVATIONS`}`)},[()=>ua(r()[0].at)]),Ii(`open`,`toggle`,s,e=>I(i,e),()=>W(i)),q(e,t)};Y(ae,e=>{r().length&&e(oe)}),V((e,i,a,m)=>{Q(v,`viewBox`,`0 0 ${W(o)} ${W(s)}`),Q(v,`aria-describedby`,r().length?n+`-trail-summary`:void 0),Q(v,`aria-label`,e),Q(b,`id`,n),Q(x,`width`,W(u)),Q(x,`height`,W(d)),Q(x,`fill`,`url(#${n})`),Q(C,`d`,`M48 20 V${W(l)} H${W(c)}`),Q(w,`y1`,W(l)),Q(w,`x2`,W(c)),Q(E,`x`,48+W(u)/2),Q(E,`y`,W(s)-5),Q(D,`cx`,W(f)),Q(D,`cy`,W(p)),Q(O,`cx`,W(f)),Q(O,`cy`,W(p)),J(te,`${t.used??``}% consumed // ${i??``}% of period elapsed`),J(re,`${t.used??``}% USED // ${a??``}% TIME ELAPSED`),J(ie,m)},[()=>`Consumption zone: ${t.used}% consumed, ${t.elapsed.toFixed(1)}% of quota period elapsed. ${W(m)>0?`Above`:W(m)<0?`Below`:`On`} the steady-consumption line.`,()=>t.elapsed.toFixed(1),()=>t.elapsed.toFixed(1),()=>Math.abs(W(m))<.05?`ON PACE`:W(m)>0?`ABOVE THE LINE — CONSUMING FASTER THAN TIME`:`BELOW THE LINE — WITHIN PACE`]),Ni(_,`clientWidth`,e=>I(o,e)),Ni(_,`clientHeight`,e=>I(s,e)),q(e,g),Ke()}var Oa=K(` `),ka=K(`

      Quota refresh failed. Values below are the last successful observation.

      `),Aa=K(`

      Expiry details unavailable. No listed expiry does not mean no expiry.

      `),ja=K(`

      `),Ma=K(`

      Read-only preview. Use the terminal to redeem a reset.

      The backend may return only some credits. This list does not establish + redemption order.

      `),Na=K(` `,1),Pa=Nr(``),Fa=Nr(``),Ia=K(`
      `),La=K(`

      Cycle duration or reset date unavailable — position cannot be + plotted.

      `),Ra=K(`
      ▼
      −100 // OVER BUDGET+100 // HEADROOM

      `,1),za=K(`

      Cycle duration unavailable — pace cannot be calculated.

      `),Ba=K(`
      EMPTYFULL
      `),Va=K(`

      `,1),Ha=K(`
      `,1),Ua=K(`

      `),Wa=K(`

      `),Ga=K(`

      No quota windows reported yet.

      `),Ka=K(`
      `,1),qa=K(`

      All reported windows are shown. API-equivalent learning and quota status + scoring remain in the terminal for this first preview.

      `,1),Ja=K(` `,1);function Ya(e,t){Ge(t,!0);let n=Ui(t,`params`,19,()=>({})),r=N(()=>Qi.filter(e=>e!==`thresholds`||!!$.data?.thresholds?.length)),i=F(Zt(Date.now())),a=N(()=>W(r).includes(n().view||``)?n().view:`bars`);Wi(()=>{let e=setInterval(()=>I(i,Date.now(),!0),1e3);return()=>clearInterval(e)});function o(e){return!e.duration||e.duration<=0||!e.reset?null:Math.max(0,Math.min(100,100*(1-(e.reset*1e3-W(i))/(e.duration*6e4))))}bn(()=>{na.view=W(a)});function s(e){let t=e/100*Math.PI*2;return`M60 60 L60 14 A46 46 0 ${+(e>50)} 1 ${60+46*Math.sin(t)} ${60-46*Math.cos(t)} Z`}var c=Ja(),l=R(c);Z(l,21,()=>W(r),X,(e,t)=>{var n=Oa();let r;var i=z(n,!0);V(e=>{Q(n,`href`,`#/quota/`+W(t)),Q(n,`aria-current`,W(a)===W(t)?`page`:void 0),r=ci(n,1,``,null,r,{active:W(a)===W(t)}),J(i,e)},[()=>W(t)===`pace`?`CONSUMPTION PACE`:W(t)===`zone`?`CONSUMPTION ZONE`:W(t)===`fuel`?`FUEL TANK`:W(t).toUpperCase()]),q(e,n)}),j(l);var u=B(l,2),d=e=>{var t=qa(),n=R(t),r=e=>{q(e,ka())};Y(n,e=>{$.data.quotaError&&e(r)});var i=B(n,2),c=z(i),l=B(i,2),u=e=>{ba(e,{})},d=e=>{var t=Ma(),n=L(t),r=z(n),i=B(n,4),a=e=>{q(e,Aa())};Y(i,e=>{$.data.credits.length||e(a)}),Z(B(i,2),17,()=>$.data.credits,X,(e,t)=>{var n=ja(),r=L(n),i=z(r),a=z(B(r,2),!0);j(n),V(e=>{J(i,`${(W(t).title||`Quota reset`)??``} // ${W(t).status??``}`),J(a,e)},[()=>W(t).expiryKnown?W(t).expires?`EXPIRES `+ua(W(t).expires):`Does not expire`:`Expiry information unavailable`]),q(e,n)}),we(2),j(t),V(()=>J(r,`RESET INVENTORY // ${$.data.creditCount??``} AVAILABLE`)),q(e,t)},f=e=>{var t=Ka(),n=R(t);let r;Z(n,21,()=>$.data.meters,X,(e,t)=>{let n=N(()=>o(W(t))),r=N(()=>W(n)===null?null:W(n)-W(t).used);var i=Wa(),c=L(i),l=z(c,!0),u=B(c,2),d=L(u),f=e=>{var n=Na(),r=R(n),i=z(r),a=z(B(r));V(()=>{J(i,`FREE ${100-W(t).used}%`),J(a,`USED ${W(t).used??``}%`)}),q(e,n)},p=e=>{var n=Na(),r=R(n),i=z(r),a=z(B(r));V(()=>{J(i,`USED ${W(t).used??``}%`),J(a,`FREE ${100-W(t).used}%`)}),q(e,n)};Y(d,e=>{W(a)===`fuel`?e(f):e(p,-1)}),j(u);var m=B(u,2);let h;var g=L(m),_=e=>{var n=Ia(),r=L(n),i=B(L(r)),a=e=>{q(e,Pa())},o=e=>{var n=Fa();V(e=>Q(n,`d`,e),[()=>s(W(t).used)]),q(e,n)};Y(i,e=>{W(t).used>=100?e(a):W(t).used>0&&e(o,1)}),j(r),j(n),V(()=>Q(r,`aria-label`,`${W(t).used}% quota used`)),q(e,n)},v=e=>{var r=Fr(),i=R(r),a=e=>{{let r=N(()=>W(t).trail||[]);Da(e,{get used(){return W(t).used},get elapsed(){return W(n)},get trail(){return W(r)}})}},o=e=>{q(e,La())};Y(i,e=>{W(n)===null?e(o,-1):e(a)}),q(e,r)},y=e=>{var t=Fr(),n=R(t),i=e=>{var t=Ra(),n=R(t),i=B(L(n),2);let a;j(n);var o=B(n,4),s=L(o),c=z(B(s),!0);j(o),V(e=>{a=ui(i,``,a,{left:`${(W(r)+100)/2}%`}),J(s,`${W(r)>=0?`+`:``}${e??``} PP `),J(c,W(r)>=0?`WITHIN PACE`:`USING FASTER THAN TIME`)},[()=>W(r).toFixed(1)]),q(e,t)},a=e=>{q(e,za())};Y(n,e=>{W(r)===null?e(a,-1):e(i)}),q(e,t)},b=e=>{var r=Ha(),i=R(r),o=L(i);let s;j(i);var c=B(i,2),l=e=>{q(e,Ba())};Y(c,e=>{W(a)===`fuel`&&e(l)});var u=B(c,2),d=e=>{var t=Va(),r=R(t),i=z(r),o=B(r,2),s=L(o);let c;j(o),V(e=>{J(i,`RESET CYCLE // ${e??``}% ELAPSED`),c=ui(s,``,c,{width:`${W(a)===`fuel`?100-W(n):W(n)}%`})},[()=>Math.floor(W(n))]),q(e,t)};Y(u,e=>{W(n)!==null&&e(d)}),V(()=>{Q(i,`aria-label`,W(a)===`fuel`?`Fuel remaining`:`Quota used`),Q(i,`aria-valuenow`,W(a)===`fuel`?100-W(t).used:W(t).used),s=ui(o,``,s,{width:`${W(a)===`fuel`?100-W(t).used:W(t).used}%`})}),q(e,r)};Y(g,e=>{W(a)===`pie`?e(_):W(a)===`zone`?e(v,1):W(a)===`pace`?e(y,2):e(b,-1)}),j(m);var x=B(m,2),S=z(x),C=B(x,2),w=e=>{var n=Ua(),r=z(n,!0);V(()=>J(r,W(t).details)),q(e,n)};Y(C,e=>{W(t).details&&e(w)}),j(i),V(e=>{J(l,W(t).name),h=ci(m,1,`meter-graphic`,null,h,{"bar-graphic":W(a)===`bars`||W(a)===`fuel`}),J(S,`RESETS // ${e??``}`)},[()=>ua(W(t).reset)]),q(e,i)}),j(n);var i=B(n,2),c=e=>{q(e,Ga())};Y(i,e=>{$.data.meters.length||e(c)}),V(()=>r=ci(n,1,`quota-grid`,null,r,{radial:W(a)===`pie`,zone:W(a)===`zone`})),q(e,t)};Y(l,e=>{W(a)===`thresholds`?e(u):W(a)===`resets`?e(d,1):e(f,-1)}),we(2),V(e=>J(c,`QUOTA // OBSERVED ${e??``}`),[()=>ua($.data.quotaAt)]),q(e,t)};Y(u,e=>{$.data&&e(d)}),q(e,c),Ke()}var Xa=K(`
      `),Za=K(`

      `,1);function Qa(e,t){Ge(t,!0);let n=Ui(t,`values`,19,()=>[]),r=Ui(t,`label`,3,`Token activity`),i=Ui(t,`capacity`,3,0),a=N(()=>Math.max(0,...n())),o=N(()=>i()>n().length?[...Array(i()-n().length).fill(0),...n()]:n());var s=Za(),c=R(s),l=z(c),u=B(c,2);Z(u,21,()=>W(o),X,(e,t)=>{var n=Xa();let r;V((e,t)=>{Q(n,`title`,e),r=ui(n,``,r,{height:t})},[()=>W(t).toLocaleString(`en-GB`)+` tokens`,()=>`${100*W(t)/Math.max(1,W(a))}%`]),q(e,n)}),j(u),V((e,t)=>{J(l,`SCALE // 0 — ${e??``} TOKENS`),Q(u,`aria-label`,t)},[()=>W(a).toLocaleString(`en-GB`),()=>`${r()}. Peak ${W(a).toLocaleString(`en-GB`)} tokens.`]),q(e,s),Ke()}var $a=K(`

      `),eo=K(`

      `,1),to=K(`

      `),no=K(`

      Loading / sending…

      `),ro=K(`

      Confirmation expired. Reopen the option.

      `),io=K(`

      `,1),ao=K(`

       
      `,1),oo=K(` `),so=K(``),co=K(`

      No available matching commands.

      `),lo=K(`

      ↑/↓ select · Tab complete · Enter options · Escape dismiss

      `),uo=K(`
      `,1),fo=K(` `,1),po=K(`
      `),mo=K(`
      `);function ho(e,t){Ge(t,!0);let n=Ui(t,`query`,3,``),r=Ui(t,`onchange`,3,()=>{}),i=Ui(t,`oncomplete`,3,e=>{}),a=F(!1),o=F(!1),s=F(``),c=F(null),l=F(null),u=F(``),d=F(0),f=F(Zt(Date.now())),p=0,m=F(0),h=F(null),g=F(void 0),_=setInterval(()=>{I(f,Date.now(),!0)},1e3);Gi(()=>{clearInterval(_),p++});let v=N(()=>n().trim().startsWith(`/`)&&!n().trim().startsWith(`//`)),y=N(()=>W(c)?.path===``&&W(v)?n().trim().slice(1).toLowerCase():``),b=N(()=>(W(c)?.choices||[]).filter(e=>!W(y)||e.label.toLowerCase().startsWith(`/`+W(y)))),x=N(()=>!$.connected||!!$.data?.sessionsError),S=N(()=>W(a)&&W(v)&&(!W(c)||W(c).path===``)&&!W(l));bn(()=>{n(),I(m,0)}),bn(()=>{let e=W(m);W(b),W(S)&&dr().then(()=>{let t=W(g)?.children[e];if(!t||!W(g))return;let n=W(g).getBoundingClientRect(),r=t.getBoundingClientRect();r.topn.bottom&&(W(g).scrollTop+=r.bottom-n.bottom)})});function C(e){if(!W(v)||!W(a)||W(c)&&W(c).path!==``||W(l)||W(x)||e.isComposing||e.ctrlKey||e.metaKey||e.altKey||![`ArrowUp`,`ArrowDown`,`Tab`,`Enter`,`Escape`].includes(e.key)||e.key===`Tab`&&e.shiftKey)return;if(e.preventDefault(),e.key===`Escape`){I(a,!1),I(h,n()),I(u,``);return}if(e.key===`ArrowUp`||e.key===`ArrowDown`){I(m,Math.min(Math.max(W(m)+(e.key===`ArrowUp`?-1:1),0),Math.max(W(b).length-1,0)),!0);return}let t=W(b)[W(m)];t&&!W(o)&&(e.key===`Tab`?i()(t.label):T(t))}bn(()=>{W(x)&&(p++,I(o,!1),I(u,``),W(a)&&(I(a,!1),I(h,n())),I(c,null),I(l,null))}),bn(()=>{W(v)&&!W(a)&&n()!==W(h)&&!W(x)&&(I(a,!0),w(``))});async function w(e){let n=++p;I(o,!0),I(l,null),I(u,``),I(s,``);try{let r=await ma(`commands`,{session:t.session,command:{mode:`list`,path:e}});n===p&&((e===``||e===`help`)&&r.choices.push({id:`local-statusline`,label:`/statusline`,help:`Choose multiple fields for Codexometer’s detail footer.`,next:`statusline`}),I(c,r,!0))}catch{n===p&&(I(s,`Command catalogue unavailable. Use Codex.`),(e===``||e===`help`)&&I(c,{title:`/ COMMANDS`,help:`Local display settings remain available.`,path:``,revision:``,choices:[{id:`local-statusline`,label:`/statusline`,help:`Choose multiple fields for Codexometer’s detail footer.`,next:`statusline`}]},!0))}finally{n===p&&I(o,!1)}}async function T(e){if(e.next===`statusline`){ra.open=!0,I(a,!1),I(h,n());return}if(e.next){await w(e.next);return}if(I(l,e,!0),I(u,``),I(s,``),!e.action||!W(c))return;let r=++p;I(o,!0);try{let n=await ma(`commands`,{session:t.session,command:{mode:`prepare`,path:W(c).path,revision:W(c).revision,choice:e.id}});r===p&&(I(u,n.confirmation,!0),I(d,Date.parse(n.expires),!0))}catch{r===p&&I(s,`Option changed or unavailable. Refresh before retrying.`)}finally{r===p&&I(o,!1)}}async function ee(){if(!W(u)||W(x)||W(o)||W(f)>=W(d)||!W(c))return;let e=W(u);I(u,``),I(o,!0);let n=++p;try{let i=await ma(`commands`,{session:t.session,confirmation:e,command:{mode:`commit`,path:W(c).path}});n===p&&(I(s,i.message,!0),I(a,!1),I(c,null),I(l,null),I(d,0),I(h,null),r()())}catch{n===p&&I(s,`Change unconfirmed. Check Codex before retrying.`)}finally{n===p&&I(o,!1)}}var E={handleKey:C},D=mo(),O=L(D),te=B(O,2),ne=e=>{var t=$a(),n=z(t,!0);V(()=>J(n,W(s))),q(e,t)};Y(te,e=>{W(s)&&!W(a)&&e(ne)});var re=B(te,2),ie=e=>{var n=po();let r;var i=L(n),a=e=>{var t=eo(),n=R(t),r=z(n,!0),i=z(B(n,2),!0);V(()=>{J(r,W(c)?.title||`/ COMMANDS`),J(i,W(c)?.help||`Loading live options…`)}),q(e,t)};Y(i,e=>{W(S)||e(a)});var p=B(i,2),h=e=>{var t=to(),n=z(t,!0);V(()=>J(n,W(s))),q(e,t)};Y(p,e=>{W(s)&&e(h)});var _=B(p,2),y=e=>{q(e,no())};Y(_,e=>{W(o)&&e(y)});var C=B(_,2),E=e=>{var n=ao(),r=R(n),i=z(r,!0),a=B(r,2),s=z(a,!0),c=B(a,2),p=e=>{var n=io(),r=R(n),i=z(r),a=B(r,2),s=B(a,2),c=e=>{q(e,ro())};Y(s,e=>{W(u)&&W(f)>=W(d)&&e(c)}),V(()=>{J(i,`TARGET // ${t.session??``}. Changes affect subsequent turns of this + session, not global defaults. Automatic quota thresholds may later + supersede model settings.`),a.disabled=W(o)||W(x)||!W(u)||W(f)>=W(d)}),G(`click`,a,ee),q(e,n)};Y(c,e=>{W(l).action&&e(p)});var m=B(c,2);V(()=>{J(i,W(l).label),J(s,W(l).help||`No additional help supplied by Codex.`),m.disabled=W(o)}),G(`click`,m,()=>{I(l,null),I(u,``)}),q(e,n)},D=e=>{var t=uo(),n=R(t);let r;Z(n,23,()=>W(b),e=>e.id,(e,t,n)=>{var r=so();let i;var a=L(r),s=z(a,!0),l=B(a),u=z(l,!0),d=B(l),f=e=>{var n=oo(),r=z(n,!0);V(()=>J(r,W(t).help)),q(e,n)};Y(d,e=>{W(v)&&W(c)?.path===``&&e(f)}),j(r),V(()=>{r.disabled=W(o)||W(x),Q(r,`title`,W(t).help),Q(r,`aria-label`,W(t).label+(W(t).next?` →`:W(t).action?``:` // HELP`)),i=ci(r,1,`svelte-7bfwr9`,null,i,{suggested:W(v)&&W(c)?.path===``&&W(m)===W(n)}),J(s,W(t).label),J(u,W(t).next?` →`:W(t).action?``:` // HELP`)}),G(`click`,r,()=>T(W(t))),q(e,r)}),j(n),Fi(n,e=>I(g,e),()=>W(g));var i=B(n,2),a=e=>{q(e,co())};Y(i,e=>{!W(o)&&!W(b).length&&e(a)});var s=B(i,2),l=e=>{q(e,lo())};Y(s,e=>{W(v)&&W(c)?.path===``&&e(l)}),V(()=>r=ci(n,1,`command-options svelte-7bfwr9`,null,r,{vertical:W(S)})),q(e,t)};Y(C,e=>{W(l)?e(E):e(D,-1)});var O=B(C,2),te=e=>{var t=fo(),n=R(t),r=B(n,2);V(()=>{n.disabled=W(o),r.disabled=W(o)}),G(`click`,n,()=>w(``)),G(`click`,r,()=>w(W(c)?.path||``)),q(e,t)};Y(O,e=>{W(S)||e(te)}),j(n),V(()=>{Q(n,`aria-label`,W(S)?`Slash command suggestions`:void 0),r=ci(n,1,`svelte-7bfwr9`,null,r,{"command-popup":W(S)})}),q(e,n)};return Y(re,e=>{W(a)&&e(ie)}),j(D),V(()=>O.disabled=W(x)||W(o)),G(`click`,O,()=>{I(a,!W(a)),I(h,W(a)?null:n(),!0),I(u,``),W(a)&&w(``)}),q(e,D),Ke(E)}wr([`click`]);var go=K(` `,1),_o=K(`
      `),vo=K(`
      /
      `);function yo(e,t){Ge(t,!0);let n=N(()=>t.lines.filter(e=>e.kind===`addition`).length),r=N(()=>t.lines.filter(e=>e.kind===`removal`).length);var i=vo(),a=L(i),o=L(a),s=z(o),c=z(B(o,2));j(a),Z(B(a,2),17,()=>t.lines,X,(e,t)=>{var n=_o();let r;var i=L(n),a=e=>{var n=go(),r=R(n),i=z(r,!0),a=z(B(r),!0);V(()=>{J(i,W(t).old||``),J(a,W(t).new||``)}),q(e,n)};Y(i,e=>{W(t).kind!==`heading`&&W(t).kind!==`metadata`&&e(a)});var o=z(B(i),!0);j(n),V(()=>{r=ci(n,1,`diff-line svelte-x4gtb6`,null,r,{added:W(t).kind===`addition`,removed:W(t).kind===`removal`,heading:W(t).kind===`heading`}),J(o,W(t).text)}),q(e,n)}),j(i),V(()=>{Q(a,`aria-label`,`${W(n)} lines added, ${W(r)} lines removed`),J(s,`+${W(n)??``}`),J(c,`−${W(r)??``}`)}),q(e,i),Ke()}var bo=K(`

      `),xo=K(`

      Send this saved prompt now? This starts Codex work and consumes quota.

      `,1),So=K(``),Co=K(`

       

      In memory only. Follows session model, reasoning and speed. Closing + Codexometer cancels pending work.

      `),wo=K(`

      CURRENT PROFILE

      MODEL / REASONING LEVEL / SPEED

       

      PROPOSED PROFILE

      MODEL / REASONING LEVEL / SPEED

       

      Applied settings remain after Codexometer closes.

      `,1),To=K(`

      `),Eo=K(`

       
      `,1),Do=K(`

      Command unavailable from this observation. Open Codex to inspect the + request.

      `),Oo=K(`

      Session controls temporarily unavailable. Check Codex for current state.

      `),ko=K(`

      Checking session controls…

      `),Ao=K(`

      `),jo=K(`
      About browser controls

      Controls require a supported live request from a connected shared + app-server session. Local observations alone cannot provide them.

      `),Mo=K(` `,1),No=K(` `),Po=K(`Grants permission beyond this one action. Check the scope + carefully.`),Fo=K(``),Io=K(``),Lo=K(``),Ro=K(``),zo=K(`

      Not before this time; when quota and idle allow.

      `),Bo=K(`

      `,1),Vo=K(`

      The existing trigger remains active until saved or deleted. If + it changes or sends while you edit, saving is blocked.

      `),Ho=K(` `,1),Uo=K(``),Wo=K(``),Go=K(``),Ko=K(` `,1),qo=K(`
    • `),Jo=K(`
      View fixed choices

      Type one of these choices exactly. Your answer stays masked.

        `),Yo=K(` `,1),Xo=K(`

        `),Zo=K(`

        `,1),Qo=K(` `,1),$o=K(``),es=K(`

        `,1),ts=K(`

        `);function ns(e,t){Ge(t,!0);let n=[],r=Ui(t,`observedCommand`,3,``),i=Ui(t,`observedFileChanges`,19,()=>[]),a=Ui(t,`review`,3,``),o=Ui(t,`suspended`,3,!1),s=Ui(t,`openScheduler`,3,!1),c=Ui(t,`onProtectedChange`,3,e=>{}),l=F(null),u=F(Zt([])),d=F(null),f=F(``),p=F(0),m=F(Zt(Date.now())),h=F(!1),g=F(``),_=F(!1),v=F(!1),y=F(``),b=F(!1),x=F(``),S=F(!1),C=F(``),w=F(`now`),T=F(60),ee=F(``),E=F(!1),D=F(``),O=F(Zt([])),te=N(()=>W(O).find(e=>e.status!==`sent`)),ne=F(void 0),re=F(void 0);function ie(e){if(W(b)){dr().then(()=>W(ne)?.focus());return}if(e.status!==`pending`||W(h)||!W(l)?.id||W(l).kind!==`prompt`||W(l).questions?.length)return;I(b,!0),I(x,e.id,!0),dr().then(()=>W(ne)?.focus()),I(u,[e.text],!0),I(w,e.trigger===`quota`?`quota`:`at`,!0),I(f,``),I(C,``);let t=new Date(e.at);e.trigger!==`quota`&&I(ee,new Date(t.getTime()-t.getTimezoneOffset()*6e4).toISOString().slice(0,16),!0)}bn(()=>{s()&&W(te)&&!W(S)&&W(l)?.id&&W(l).kind===`prompt`&&!W(l).questions?.length&&(I(S,!0),ie(W(te)))});async function ae(){if(!a())try{I(O,await ma(`schedules`,{session:t.session},be.signal),!0),W(b)&&!W(O).some(e=>e.id===W(x)&&e.status===`pending`)&&I(f,``),W(C)&&!W(O).some(e=>e.id===W(C)&&e.status===`pending`)&&(I(f,``),I(C,``))}catch{}}async function oe(e){try{I(O,await ma(`schedules`,{session:t.session,cancelId:e},be.signal),!0),I(b,!1),I(f,``),I(C,``),I(u,[],!0)}catch(e){I(g,e instanceof Error?e.message:`Unable to cancel trigger.`,!0)}}bn(()=>(c()(W(h)||W(u).some(e=>e.length>0)),()=>c()(!1)));let se=N(()=>$.data?.sessions.find(e=>e.id===t.session)?.status),ce=N(()=>!$.connected||!!$.data?.sessionsError),le=N(()=>a()!==`profile`&&W(l)?.kind===`prompt`&&!W(l).questions?.length&&!!$.data?.profiles?.some(e=>e.session===t.session&&e.pending)),ue=N(()=>!W(ce)&&!W(v)&&!!W(l)?.id&&W(l).kind===`approval`&&W(y)!==W(l).id),de=N(()=>W(ue)?W(l).command:r()),fe=N(()=>!!W(f)&&W(m)W(l)?.questions?.length?W(l).questions:[{text:`Follow-up message`,secret:!1,freeText:!0,options:[]}]),me=N(()=>W(l)?.kind===`approval`||W(l)?.kind===`profile`?W(d)!==null:W(u).length===W(pe).length&&W(u).every((e,t)=>e.trim().length>0&&(W(pe)[t].freeText||W(pe)[t].options?.includes(e)))),he=N(()=>W(l)?.kind===`prompt`&&!W(l).questions?.length&&W(w)!==`now`),ge=N(()=>W(w)===`delay`?W(m)+W(T)*6e4:Date.parse(W(ee))),_e=N(()=>{if(!W(he))return``;if(W(b)&&!W(O).some(e=>e.id===W(x)&&e.status===`pending`))return`Trigger changed, removed or already sent. Go back and reopen it; your draft has not been saved.`;if(!W(u)[0]?.trim())return`Enter a message to schedule.`;if(W(w)===`quota`)return``;if(!Number.isFinite(W(ge))||W(ge)<=W(m))return`Choose a future date/time or a delay greater than zero.`;let e=new Date(W(m));return e.setFullYear(e.getFullYear()+1),W(ge)>=e.getTime()?`Choose a time within one year.`:``});function ve(e){return new Date(e).toLocaleString([],{timeZoneName:`short`})}function ye(e){if(e.status!==`pending`)return e.status;if(e.trigger===`at`&&Date.parse(e.at)>W(m))return`Due in `+Math.ceil((Date.parse(e.at)-W(m))/6e4)+` min`;let t=W(ce)?`waiting for a fresh connection`:e.waitingReason||(e.canSend?`ready; awaiting dispatch`:`waiting for quota and an eligible idle session`);return(e.trigger===`at`?`Time reached — `:``)+t}bn(()=>{(W(ce)||o()||W(le)||W(m)>=W(p))&&I(f,``)});let be=new AbortController;Wi(()=>{let e=e=>{e.ctrlKey&&e.key.toLowerCase()===`s`&&!a()&&W(te)&&(e.preventDefault(),ie(W(te)))};window.addEventListener(`keydown`,e);let n,r=setInterval(()=>{I(m,Date.now(),!0)},1e3);async function i(){await ae();try{let e=await ma(`offer`,{session:t.session,...a()?{review:a()}:{}},be.signal);if(be.signal.aborted)return;I(v,!1),W(l)?.id!==e.id&&(I(f,``),I(d,null),I(u,Array(Math.max(1,e.questions?.length||0)).fill(``),!0)),I(l,e,!0),W(_)&&e.id&&e.id!==W(y)&&(I(g,``),I(_,!1))}catch{be.signal.aborted||(I(l,null),I(v,!0),I(f,``))}finally{be.signal.aborted||(n=setTimeout(i,2e3))}}return i(),()=>{window.removeEventListener(`keydown`,e),be.abort(),clearTimeout(n),clearInterval(r)}});async function k(e){if(!e&&W(l)?.kind===`prompt`&&!W(l).questions?.length&&W(u)[0]?.trim().startsWith(`/`)&&!W(u)[0]?.trim().startsWith(`//`)){I(g,`Choose a slash command from COMMANDS; use // for literal slash text.`);return}if(!W(l)?.id||W(h)||W(ce)||o()||W(le)||(e?!e.canSend:!W(me)||W(_e)))return;let n=W(l).id;I(h,!0),I(g,``),I(_,!1),I(f,``),I(C,e?.id||``,!0);try{let r=!e&&W(l).kind===`prompt`&&!W(l).questions?.length&&W(w)!==`now`?{trigger:W(w)===`quota`?`quota`:`at`,zone:Intl.DateTimeFormat().resolvedOptions().timeZone,...W(w)===`quota`?{}:{at:(W(w)===`delay`?new Date(Date.now()+W(T)*6e4):new Date(W(ee))).toISOString()}}:void 0;I(E,!!r),I(D,r?r.trigger===`quota`?`After fresh quota becomes available, when the session is idle`:ve(r.at)+` — not before this time; when quota and idle allow`:``,!0);let i=await ma(`prepare`,{session:t.session,...a()?{review:a()}:{},offer:n,...e?{sendId:e.id}:{},...r?{schedule:r}:{},...r&&W(b)?{editId:W(x)}:{},...W(l).kind===`approval`||W(l).kind===`profile`?{choice:W(d)}:{answers:e?[e.text]:[...W(u)]}},be.signal);W(l)?.id===n&&!be.signal.aborted&&!W(ce)&&!o()&&!W(le)&&(I(f,i.confirmation,!0),I(p,Date.parse(i.expires),!0))}catch(e){be.signal.aborted||I(g,e instanceof Error?e.message:`Unable to prepare action.`,!0)}finally{I(h,!1)}}async function xe(){if(!W(l)?.id||W(h)||o()||W(le)||!W(fe))return;let e=W(l).id,n=W(l).kind,r=W(f);I(f,``),I(h,!0),I(y,e,!0);try{await ma(`commit`,{session:t.session,...a()?{review:a()}:{},offer:e,confirmation:r},be.signal),I(_,!0),I(b,!1),W(E)&&(I(y,``),await ae()),I(g,W(E)?`Trigger set. Closing Codexometer cancels it.`:n===`profile`?`Profile decision completed.`:n===`approval`?`Decision sent.`:`Text sent.`,!0)}catch(e){I(g,e instanceof fa?e.message:`Outcome uncertain. Check Codex before taking another action; nothing was retried.`,!0)}finally{I(h,!1),I(C,``),await ae(),I(u,[],!0),I(d,null)}}var A=Mo(),Se=R(A),Ce=e=>{var t=Fr();Z(R(t),17,()=>W(O).filter(e=>e.status!==`sent`),e=>e.id,(e,t)=>{var n=Co(),r=L(n),i=z(r,!0),a=B(r,2),s=z(a),c=B(a,2),u=z(c,!0),d=B(c,2),p=z(d,!0),m=B(d,2),_=z(m,!0),v=B(m,2),y=e=>{var t=bo(),n=z(t,!0);V(()=>J(n,W(g))),q(e,t)};Y(v,e=>{W(g)&&e(y)});var b=B(v,4),x=B(b,2),S=B(x,2),w=e=>{var n=xo(),r=B(R(n),2),i=B(r,2);V(()=>r.disabled=W(h)||W(ce)||!W(t).canSend),G(`click`,r,xe),G(`click`,i,()=>{I(f,``),I(C,``)}),q(e,n)},T=e=>{var n=So();V(()=>n.disabled=W(h)||W(ce)||!W(t).canSend||o()),G(`click`,n,()=>k(W(t))),q(e,n)};Y(S,e=>{W(fe)&&W(C)===W(t).id?e(w):e(T,-1)}),j(n),V((e,n)=>{J(i,W(t).status===`uncertain`?`CHECK TRIGGER — outcome uncertain; not retried`:`SCHEDULED FOLLOW-UP`),J(s,`TARGET // ${W(t).session??``}`),J(u,e),J(p,n),J(_,W(t).text),b.disabled=W(h)||!W(l)?.id||W(l).kind!==`prompt`||!!W(l).questions?.length||W(t).status!==`pending`,x.disabled=W(h)||W(t).status===`sending`},[()=>W(t).trigger===`quota`?`After confirmed quota recovery, when idle`:`Not before ${ve(W(t).at)}`,()=>ye(W(t))]),G(`click`,b,()=>ie(W(t))),G(`click`,x,()=>oe(W(t).id)),q(e,n)}),q(e,t)},Te=N(()=>!a()&&W(l)?.kind!==`approval`&&!W(l)?.questions?.length&&![`APPROVAL NEEDED`,`INPUT NEEDED`].includes(W(se)||``));Y(Se,e=>{W(Te)&&e(Ce)});var Ee=B(Se,2),De=e=>{var r=ts(),o=L(r),s=z(o,!0),c=B(o,2),x=e=>{var t=wo(),n=R(t),r=z(n),i=B(n,6),a=z(i,!0),o=z(B(i,6),!0);we(2),V(()=>{J(r,`Your ${W(l).profile.threshold??``}% quota threshold has been reached. Review + the profile for subsequent turns.`),J(a,W(l).profile.current),J(o,W(l).profile.proposed)}),q(e,t)};Y(c,e=>{W(l)?.profile&&!W(ce)&&!W(v)&&e(x)});var S=B(c,2),C=e=>{var t=To();let n;var r=z(t,!0);V(()=>{n=ci(t,1,`svelte-1oupzfc`,null,n,{notice:!W(_),sent:W(_)}),J(r,W(g))}),q(e,t)};Y(S,e=>{W(g)&&e(C)});var O=B(S,2),te=e=>{yo(e,{get lines(){return W(l).fileChanges}})},ie=e=>{yo(e,{get lines(){return i()}})},ae=e=>{var t=Eo(),n=R(t),r=z(n,!0),i=z(B(n,2),!0);V(()=>{J(r,W(ue)?`EXACT COMMAND`:`LAST OBSERVED COMMAND`),J(i,W(de))}),q(e,t)},oe=e=>{q(e,Do())};Y(O,e=>{W(l)?.fileChanges?.length&&!W(ce)&&!W(v)?e(te):i().length?e(ie,1):W(de)?e(ae,2):a()!==`profile`&&W(se)===`APPROVAL NEEDED`&&!W(_)&&e(oe,3)});var le=B(O,2),he=e=>{q(e,Oo())},ye=e=>{q(e,ko())},be=e=>{var t=Mo(),n=R(t),r=e=>{var t=Ao(),n=z(t,!0);V(e=>J(n,e),[()=>W(se)===`WORKING`?`Codex is working — nothing to respond to.`:[`APPROVAL NEEDED`,`INPUT NEEDED`].includes(W(se)||``)?`Respond in Codex for this request.`:`Nothing needs a response right now.`]),q(e,t)};Y(n,e=>{(W(se)===`WORKING`||!W(_)&&!W(h))&&e(r)});var i=B(n,2),a=e=>{q(e,jo())},o=N(()=>[`APPROVAL NEEDED`,`INPUT NEEDED`].includes(W(se)||``)&&!W(_));Y(i,e=>{W(o)&&e(a)}),q(e,t)},A=e=>{var r=es(),i=R(r),o=z(i),s=B(i,2),c=L(s),g=z(c,!0),_=B(c,2),v=e=>{var r=Fr();Z(R(r),17,()=>W(l).choices||[],X,(e,r,i)=>{var o=Fo(),s=L(o);xi(s),s.value=s.__value=i;var c=B(s),l=B(c),u=e=>{var t=No(),n=z(t,!0);V(()=>J(n,W(r).detail)),q(e,t)};Y(l,e=>{W(r).detail&&e(u)});var f=B(l,2),p=e=>{q(e,Po())};Y(f,e=>{W(r).persistent&&e(p)}),j(o),V(()=>{Q(s,`name`,`decision-`+t.session+`-`+a()),J(c,` ${W(r).label??``} `)}),Oi(n,[],s,()=>W(d),e=>I(d,e)),q(e,o)}),q(e,r)},y=e=>{var n=Mo(),r=R(n),i=e=>{var t=Ho(),n=R(t),r=B(L(n)),i=L(r),a=e=>{var t=Io();t.value=t.__value=`now`,q(e,t)};Y(i,e=>{W(b)||e(a)});var o=B(i);o.value=o.__value=`quota`;var s=B(o);s.value=s.__value=`delay`;var c=B(s);c.value=c.__value=`at`,j(r),mi(r),j(n);var l=B(n,2),u=e=>{var t=Lo(),n=B(L(t));xi(n),j(t),Ei(n,()=>W(T),e=>I(T,e)),q(e,t)};Y(l,e=>{W(w)===`delay`&&e(u)});var d=B(l,2),f=e=>{var t=Ro(),n=B(L(t));xi(n),j(t),Ei(n,()=>W(ee),e=>I(ee,e)),q(e,t)};Y(d,e=>{W(w)===`at`&&e(f)});var p=B(d,2),m=e=>{var t=Bo(),n=R(t),r=z(n),i=B(n,2),a=e=>{q(e,zo())};Y(i,e=>{W(w)!==`quota`&&e(a)});var o=B(i,2);o.textContent=`Timezone: ${Intl.DateTimeFormat().resolvedOptions().timeZone??``}. + One pending request per session; saving replaces it. Memory + only; Codexometer must stay running. Uses session model, + reasoning and speed.`,V(e=>J(r,`Will send: ${e??``}`),[()=>W(w)===`quota`?`when quota is available and this session is idle`:Number.isFinite(W(ge))?ve(W(ge)):`choose a date and time`]),q(e,t)};Y(p,e=>{W(w)!==`now`&&e(m)});var h=B(p,2),g=e=>{q(e,Vo())};Y(h,e=>{W(b)&&e(g)}),hi(r,()=>W(w),e=>I(w,e)),q(e,t)};Y(r,e=>{W(l).questions?.length||e(i)}),Z(B(r,2),17,()=>W(pe),X,(e,n,r)=>{var i=Yo(),o=R(i),s=e=>{{let n=N(()=>W(u)[0]||``);Fi(ho(e,{get session(){return t.session},get query(){return W(n)},oncomplete:e=>{I(u,[e],!0),dr().then(()=>W(ne)?.focus())},onchange:()=>{I(u,[``],!0),dr().then(()=>W(ne)?.focus())}}),e=>I(re,e,!0),()=>W(re))}};Y(o,e=>{r===0&&a()!==`profile`&&W(l).kind===`prompt`&&!W(l).questions?.length&&e(s)});var c=B(o,2),d=L(c),f=B(d),p=e=>{var t=Uo();xi(t),Ei(t,()=>W(u)[r],e=>W(u)[r]=e),q(e,t)},m=e=>{var t=Go(),i=L(t);i.value=i.__value=``,Z(B(i),17,()=>W(n).options||[],X,(e,t)=>{var n=Wo(),r=z(n,!0),i={};V(()=>{J(r,W(t)),i!==(i=W(t))&&(n.value=(n.__value=i)??``)}),q(e,n)}),j(t),mi(t),hi(t,()=>W(u)[r],e=>W(u)[r]=e),q(e,t)},h=e=>{var t=Ko(),i=R(t);rt(i),Fi(i,e=>I(ne,e),()=>W(ne));var a=B(i,2),o=e=>{var t=Ao(),r=z(t);V(e=>J(r,`Suggested answers: ${e??``}`),[()=>W(n).options.join(` · `)]),q(e,t)};Y(a,e=>{W(n).options?.length&&e(o)}),G(`keydown`,i,e=>{W(l)?.kind===`prompt`&&!W(l).questions?.length&&W(re)?.handleKey(e)}),Ei(i,()=>W(u)[r],e=>W(u)[r]=e),q(e,t)};Y(f,e=>{W(n).secret?e(p):W(n).freeText?e(h,-1):e(m,1)}),j(c);var g=B(c,2),_=e=>{var t=Jo(),r=B(L(t),4);Z(r,21,()=>W(n).options||[],X,(e,t)=>{var n=qo(),r=z(n,!0);V(()=>J(r,W(t))),q(e,n)}),j(r),j(t),q(e,t)};Y(g,e=>{W(n).secret&&!W(n).freeText&&e(_)}),V(()=>J(d,`${W(n).text??``} `)),q(e,i)}),q(e,n)};Y(_,e=>{W(l).kind===`approval`||W(l).kind===`profile`?e(v):e(y,-1)}),j(s);var x=B(s,2),S=e=>{var t=Zo(),n=R(t),r=e=>{var t=Xo(),n=z(t);V(()=>J(n,`TRIGGER // ${W(D)??``}`)),q(e,t)};Y(n,e=>{W(E)&&e(r)});var i=B(n,2),a=z(i),o=B(i,2),s=z(o),c=B(o,2);V(e=>{J(a,`Check the target and ${W(l).kind===`profile`?`profile above`:W(l).kind===`approval`?W(l).fileChanges?.length?`proposed file changes above`:`exact command and permission scope`:`message above`}. ${W(E)?`This schedules a follow-up that will send automatically when eligible`:`This will send to Codex`}; it may start work using your quota. + Confirmation expires in ${e??``}s.`),o.disabled=W(h)||W(ce),J(s,`CONFIRM ${(W(l).kind===`approval`||W(l).kind===`profile`?W(l).choices?.[W(d)]?.label:W(E)?W(b)?`SAVE CHANGES`:`SCHEDULE`:`SEND`)??``}`)},[()=>Math.max(0,Math.ceil((W(p)-W(m))/1e3))]),G(`click`,o,xe),G(`click`,c,()=>{I(f,``)}),q(e,t)},C=e=>{var t=Qo(),n=R(t),r=e=>{var t=bo(),n=z(t,!0);V(()=>J(n,W(_e))),q(e,t)};Y(n,e=>{W(_e)&&e(r)});var i=B(n,2),a=z(i,!0);V(()=>{i.disabled=W(h)||W(ce)||!W(me)||!!W(_e),J(a,W(h)?`SENDING…`:W(b)?`REVIEW CHANGES`:`REVIEW BEFORE SENDING`)}),G(`click`,i,()=>k()),q(e,t)};Y(x,e=>{W(fe)?e(S):e(C,-1)});var O=B(x,2),te=e=>{var t=$o();G(`click`,t,()=>{I(b,!1),I(f,``),I(u,[],!0)}),q(e,t)};Y(O,e=>{W(b)&&e(te)}),V(()=>{J(o,`TARGET // ${W(l).thread??``} // ${(W(l).directory||`Directory unavailable`)??``}`),s.disabled=W(h)||W(fe)||W(ce),J(g,W(l).kind===`approval`||W(l).kind===`profile`?`Choose a decision`:W(b)?`EDIT SCHEDULED FOLLOW-UP`:`Reply to this session`)}),q(e,r)};Y(le,e=>{W(ce)||W(v)?e(he):W(l)?W(l).id?W(y)!==W(l).id&&e(A,3):e(be,2):e(ye,1)}),j(r),V(()=>J(s,a()===`profile`?`QUOTA THRESHOLD`:`SESSION CONTROL // EXPERIMENTAL`)),q(e,r)};Y(Ee,e=>{!W(le)&&(!W(te)||W(b)||a()||W(l)?.kind!==`prompt`||W(l).questions?.length)&&e(De)}),q(e,A),Ke()}wr([`click`,`keydown`]);var rs=K(` `,1),is=K(`
        `),as=K(`

        STATUS LINE // CODEXOMETER

        Choose multiple fields and their order. These display preferences do not + change Codex CLI configuration. Missing values are omitted.

        `),os=K(`

        `,1);function ss(e,t){Ge(t,!0);let n=Ui(t,`stale`,3,!1),r=F(Zt([])),i=F(!1),a=N(()=>$.data?.statusLineFields||[]),o=e=>[...new Set(e??W(a).filter(e=>e.default).map(e=>e.id))].filter(e=>W(a).some(t=>t.id===e)),s=e=>o(e).map(e=>e===`run-state`&&(n()||!$.connected||$.data?.sessionsError)?`STALE`:t.session.statusLine?.[e]).filter(Boolean).join(` · `),c=N(()=>[...W(r).map(e=>W(a).find(t=>t.id===e)).filter(Boolean),...W(a).filter(e=>!W(r).includes(e.id))]);bn(()=>{ra.open&&!W(i)?(I(r,o(na.statusLine),!0),I(i,!0)):ra.open||I(i,!1)});function l(e,t){I(r,t?[...W(r),e]:W(r).filter(t=>t!==e),!0)}function u(e,t){let n=W(r).indexOf(e),i=n+t;if(n<0||i<0||i>=W(r).length)return;let a=[...W(r)];[a[n],a[i]]=[a[i],a[n]],I(r,a,!0)}var d=os(),f=R(d),p=e=>{var t=as(),n=B(L(t),4);Z(n,21,()=>W(c),e=>e.id,(e,t)=>{var n=is(),i=L(n),a=L(i);xi(a);var o=B(a,1,!0),s=z(B(o),!0);j(i);var c=B(i,2),d=e=>{var n=rs(),i=R(n),a=B(i,2);V((e,n)=>{Q(i,`aria-label`,`Move `+W(t).label+` earlier`),i.disabled=e,Q(a,`aria-label`,`Move `+W(t).label+` later`),a.disabled=n},[()=>W(r).indexOf(W(t).id)===0,()=>W(r).indexOf(W(t).id)===W(r).length-1]),G(`click`,i,()=>u(W(t).id,-1)),G(`click`,a,()=>u(W(t).id,1)),q(e,n)},f=N(()=>W(r).includes(W(t).id));Y(c,e=>{W(f)&&e(d)}),j(n),V(e=>{Q(i,`title`,W(t).help),Si(a,e),J(o,W(t).label),J(s,W(t).help)},[()=>W(r).includes(W(t).id)]),G(`change`,a,e=>l(W(t).id,e.currentTarget.checked)),q(e,n)}),j(n);var i=B(n,2),a=z(i),o=B(i,2),d=B(o,2);j(t),V(e=>J(a,`Preview // ${e??``}`),[()=>s(W(r))||`—`]),G(`click`,o,()=>{na.statusLine=[...W(r)],ia(),ra.open=!1}),G(`click`,d,()=>{ra.open=!1}),q(e,t)};Y(f,e=>{ra.open&&e(p)});var m=B(f,2),h=L(m),g=z(h,!0),_=B(h,2);j(m),V((e,t)=>{Q(h,`title`,e),J(g,t)},[()=>s(na.statusLine),()=>s(na.statusLine)]),G(`click`,_,()=>{ra.open=!ra.open}),q(e,d),Ke()}wr([`change`,`click`]);var cs=K(`
        `);function ls(e,t){Ge(t,!0);let n=Ui(t,`active`,3,!1),r=N(()=>t.session.text||t.session.contextKind===`LAST ACTIVITY`&&t.session.workingCommand||``),i=N(()=>[`LAST REPLY`,`LAST ACTIVITY`].includes(t.session.contextKind)&&!!W(r).trim()),a=F(!1),o=F(``),s=F(!1);bn(()=>{if(!W(s))return;let e=setTimeout(()=>{I(s,!1)},150);return()=>clearTimeout(e)}),bn(()=>{W(r),t.session.status,I(o,``)});async function c(){if(!W(i)||W(a))return;let e=W(r);I(a,!0),I(s,!0),I(o,``);try{await navigator.clipboard.writeText(e),W(r)===e&&I(o,`Copied.`)}catch{W(r)===e&&I(o,`Clipboard unavailable. Select the visible text and copy it manually.`)}finally{I(a,!1)}}function l(e){!n()||!W(i)||e.defaultPrevented||e.repeat||e.ctrlKey||e.metaKey||e.altKey||e.key.toLowerCase()!==`c`||e.target instanceof Element&&e.target.closest(`input, textarea, select, [contenteditable]`)||(e.preventDefault(),c())}var u=Fr();Cr(`keydown`,en,l);var d=R(u),f=e=>{var t=cs(),n=L(t),r=z(n,!0),i=B(n,2);let l;j(t),V(()=>{J(r,W(o)),i.disabled=W(a),l=ci(i,1,`svelte-543j00`,null,l,{flashed:W(s)})}),G(`click`,i,c),q(e,t)};Y(d,e=>{W(i)&&e(f)}),q(e,u),Ke()}wr([`click`]);var us=K(`

         
        `,1),ds=K(`

        LATEST GUIDANCE

         
        `,1),fs=K(`

        CONTEXT

         

        `,1),ps=K(`

        `),ms=K(`

        `),hs=K(`
         
        `),gs=K(`

         
        `,1),_s=K(`
         
        `),vs=K(`

        Command unavailable from this observation. Open Codex to inspect the + request.

        `),ys=K(`

        `,1),bs=K(`

        `),xs=K(` `,1),Ss=K(`
        `),Cs=K(`

        Session connection or refresh unavailable. Context and telemetry may be + stale.

        `),ws=K(`

        Some quota profile checks are unavailable. Only sessions with freshly + verified quota and settings can be updated; previous outcome notices remain + visible.

        `),Ts=K(` `),Es=K(` `),Ds=K(` `),Os=K(``),ks=K(`

        Read only — reply or approve in Codex.

        `),As=K(`

        `),js=K(`QUOTA THRESHOLD // REVIEW PROFILE ↗`),Ms=K(`
        `),Ns=K(`
        `),Ps=K(`

        This session is no longer in the current observation. Return to sessions.

        `),Fs=K(`

        `),Is=K(``),Ls=K(`

        `),Rs=K(` `),zs=K(`

        `),Bs=K(`QUOTA THRESHOLD // REVIEW PROFILE ↗`),Vs=K(` `,1),Hs=K(`

        `),Us=K(`

        TOKEN ACTIVITY // 30 SECOND SAMPLES

        `),Ws=K(`

        TOKENS

        FULL DETAIL →
        `),Gs=K(`

        No locally observed sessions yet. Keep Codex running alongside + Codexometer.

        `),Ks=K(`

        ↑ ↓ SELECT SESSION // ← LESS DETAIL // → MORE DETAIL // ESC BACK

        `,1),qs=K(`

        SESSION TOTALS

        `,1);function Js(e,t){Ge(t,!0);let n=(e,t=f,n,r)=>{let i=ht(()=>g(n?.(),!0)),a=ht(()=>g(r?.(),!1));var o=xs(),s=R(o),c=e=>{var n=us(),r=R(n),i=z(r,!0),a=z(B(r,2),!0);V(()=>{J(i,t().contextKind===`LAST REPLY`&&!t().streaming?`TASK`:`CURRENT TASK`),J(a,t().currentTask)}),q(e,n)};Y(s,e=>{t().currentTask&&e(c)});var l=B(s,2),u=e=>{var n=ds(),r=z(B(R(n),2),!0);V(()=>J(r,t().latestGuidance)),q(e,n)};Y(l,e=>{t().latestGuidance&&e(u)});var d=B(l,2),p=e=>{var n=fs(),r=z(B(R(n),2),!0);we(2),V(()=>J(r,t().approvalContext)),q(e,n)};Y(d,e=>{W(a)&&t().contextKind===`APPROVAL REQUEST`&&t().approvalContext&&e(p)});var m=B(d,2),_=e=>{var n=ps();let r;var i=z(n,!0);V(e=>{r=ci(n,1,`attention-note`,null,r,{inferred:t().status===`CHECK SESSION`}),J(i,e)},[()=>C(t())]),q(e,n)},v=N(()=>C(t())&&(!W(a)||W(h)||t().status===`CHECK SESSION`));Y(m,e=>{W(v)&&e(_)});var y=B(m,2),b=e=>{var n=ms(),r=z(n,!0);V(e=>J(r,e),[()=>w(t())]),q(e,n)};Y(y,e=>{W(i)&&e(b)});var x=B(y,2),S=e=>{var n=hs(),r=z(n,!0);V(()=>J(r,t().text||`No session context available.`)),q(e,n)};Y(x,e=>{(t().text||!t().workingCommand)&&e(S)});var T=B(x,2),ee=e=>{var n=gs(),r=B(R(n),2),i=L(r),a=B(i),o=e=>{var n=Pr();V(()=>J(n,`// +${(t().runningCommands||0)-+(t().commandStatus===`running`)} RUNNING`)),q(e,n)};Y(a,e=>{!W(h)&&(t().runningCommands||0)>+(t().commandStatus===`running`)&&e(o)});var s=B(a,2),c=e=>{q(e,Pr(`+`))};Y(s,e=>{t().runningLimited&&e(c)}),j(r);var l=z(B(r,2),!0);V(e=>{J(i,`COMMAND // ${e??``} `),J(l,t().workingCommand)},[()=>W(h)?`UNKNOWN`:(t().commandStatus||`unknown`).toUpperCase()]),q(e,n)};Y(T,e=>{t().workingCommand&&e(ee)});var E=B(T,2),D=e=>{yo(e,{get lines(){return t().fileChanges}})},O=e=>{var n=ys(),r=B(R(n),2),i=z(r,!0),a=B(r,2),o=e=>{var n=_s(),r=z(n,!0);V(()=>J(r,t().command)),q(e,n)},s=e=>{q(e,vs())};Y(a,e=>{t().command?e(o):e(s,-1)}),V(()=>J(i,W(h)||t().status!==`APPROVAL NEEDED`?`LAST OBSERVED COMMAND`:$.data?.control?`COMMAND REQUEST`:`COMMAND TO APPROVE IN CODEX`)),q(e,n)};Y(E,e=>{(!W(a)||!$.data?.control)&&t().fileChanges?.length?e(D):(!W(a)||!$.data?.control)&&(t().command||t().status===`APPROVAL NEEDED`)&&e(O,1)});var te=B(E,2),ne=e=>{var n=bs(),r=z(n);V(()=>J(r,`CONTEXT SOURCE // ${(t().source||`LOCAL`)??``} // OBSERVATION`)),q(e,n)};Y(te,e=>{W(a)||e(ne)}),q(e,o)},r=Ui(t,`params`,19,()=>({})),i=N(()=>$.data?.sessions||[]),a=N(()=>$.data?.control&&$.data.profiles||[]),o=N(()=>($.data?.triggers||[]).filter(e=>W(i).some(t=>t.id===e.session)));function s(e,t){if(r().id&&r().id!==t&&W(l)){e.preventDefault();return}b(t)}function c(e){return[`TURN COMPLETE`,`IDLE`,`ACTIVE`].includes(e.status)&&W(o).some(t=>t.session===e.id)?`TRIGGER SET`:e.status}let l=F(!1);function u(e,t){if(r().id&&r().id!==t&&W(l)){e.preventDefault();return}b(t)}bn(()=>{let e=r().id;e&&mr(()=>{na.selected=e,ca(e,2)})});let d=N(()=>W(i).find(e=>e.id===r().id)),p=N(()=>new URLSearchParams(Ji.querystring).get(`review`)===`profile`&&W(a).some(e=>e.session===r().id&&e.pending)),m=N(()=>W(i).some(e=>e.id===na.selected)?na.selected:W(i)[0]?.id),h=N(()=>!$.connected||!!$.data?.sessionsError),v=N(()=>[[`OBSERVED TOKENS`,la(W(i).reduce((e,t)=>e+t.tokens,0))],[`LISTED SESSIONS`,la(W(i).length)],...[[`WORKING`,`WORKING`],[`AWAITING APPROVAL`,`APPROVAL NEEDED`],[`AWAITING INPUT`,`INPUT NEEDED`],[`CHECK · INFERRED`,`CHECK SESSION`]].map(([e,t])=>[e,W(h)?`—`:la(W(i).filter(e=>e.status===t).length)])]),y=N(()=>W(i).filter(e=>[`INPUT NEEDED`,`APPROVAL NEEDED`,`CHECK SESSION`].includes(e.status)));function b(e){na.selected=e,dr().then(()=>document.querySelector(`.session-row.selected`)?.scrollIntoView({block:`nearest`}))}function x(e,t){if(b(e),r().id){t<0&&(ca(e,2),location.hash=`/sessions`);return}let n=oa(e)+t;n>2?location.hash=`/sessions/`+encodeURIComponent(e):ca(e,n)}function S(e){let t=e.target;if(!(e.altKey||e.ctrlKey||e.metaKey||e.shiftKey||t.closest(`input, textarea, select, [contenteditable="true"]`))){if(e.key===`Escape`&&r().id)e.preventDefault(),x(r().id,-1);else if([`ArrowLeft`,`ArrowRight`].includes(e.key)&&(r().id||W(m)))e.preventDefault(),x(r().id||W(m),e.key===`ArrowLeft`?-1:1);else if(!r().id&&[`ArrowUp`,`ArrowDown`].includes(e.key)&&W(i).length){e.preventDefault();let t=W(i).findIndex(e=>e.id===W(m));b(W(i)[Math.max(0,Math.min(W(i).length-1,t+(e.key===`ArrowUp`?-1:1)))].id)}}}function C(e){return W(h)?`Last observation only — refresh unavailable. Check Codex for current state.`:e.status===`CHECK SESSION`?`INFERRED INACTIVITY — a quiet session, not a confirmed input or approval request. Local tools may still be running.`:$.data?.control&&[`APPROVAL NEEDED`,`INPUT NEEDED`].includes(e.status)?`Open full detail for supported live controls; otherwise reply in Codex.`:e.status===`APPROVAL NEEDED`?`OBSERVED APPROVAL SIGNAL — approve or decline in Codex.`:e.status===`INPUT NEEDED`?`OBSERVED INPUT SIGNAL — reply in Codex.`:e.status===`TURN COMPLETE`?`OBSERVED TURN COMPLETION — informational, not an approval request.`:``}function w(e){return e.streaming&&e.contextKind===`LAST REPLY`?`REPLY // STREAMING`:e.contextKind||`LAST ACTIVITY`}var T=qs();Cr(`keydown`,en,S);var ee=R(T),E=z(B(L(ee),2));j(ee);var D=B(ee,2);let O;Z(D,21,()=>W(v),X,(e,t)=>{var n=N(()=>_(W(t),2));let r=()=>W(n)[0],i=()=>W(n)[1];var a=Ss(),o=L(a),s=z(o,!0),c=z(B(o,2),!0);j(a),V(()=>{J(s,r()),J(c,i())}),q(e,a)}),j(D);var te=B(D,2),ne=z(te),re=B(te,2),ie=e=>{q(e,Cs())};Y(re,e=>{W(h)&&e(ie)});var ae=B(re,2),oe=e=>{q(e,ws())};Y(ae,e=>{$.data?.profileError&&e(oe)});var se=B(ae,2),ce=e=>{var t=Os(),n=L(t);Z(n,17,()=>W(y),X,(e,t)=>{var n=Ts();let r;var i=z(n);V((e,a)=>{r=ci(n,1,`button`,null,r,{approval:W(t).status===`APPROVAL NEEDED`}),Q(n,`href`,e),J(i,`${W(t).status??``} + ${a??``} // ${(W(t).name||W(t).directory||W(t).id)??``}`)},[()=>`#/sessions/`+encodeURIComponent(W(t).id),()=>Array.from(W(t).id).slice(-5).join(``).toUpperCase()]),G(`click`,n,()=>b(W(t).id)),q(e,n)});var c=B(n,2);Z(c,17,()=>W(a).filter(e=>e.pending&&W(i).some(t=>t.id===e.session)),X,(e,t)=>{var n=Es(),a=z(n);V((e,i,o)=>{Q(n,`title`,r().id&&r().id!==W(t).session&&W(l)?`Finish sending or clear your current draft before switching sessions.`:`Review this session’s quota threshold`),Q(n,`href`,e),J(a,`QUOTA THRESHOLD ${i??``} // ${o??``}`)},[()=>`#/sessions/`+encodeURIComponent(W(t).session)+`?review=profile`,()=>Array.from(W(t).session).slice(-5).join(``).toUpperCase(),()=>W(i).find(e=>e.id===W(t).session)?.name||W(i).find(e=>e.id===W(t).session)?.directory||W(t).session]),G(`click`,n,e=>u(e,W(t).session)),q(e,n)}),Z(B(c,2),17,()=>W(o),X,(e,t)=>{var n=Ds(),r=z(n);V((e,t,i)=>{Q(n,`href`,e),J(r,`TRIGGER SET ${t??``} // ${i??``}`)},[()=>`#/sessions/`+encodeURIComponent(W(t).session),()=>Array.from(W(t).session).slice(-5).join(``).toUpperCase(),()=>W(i).find(e=>e.id===W(t).session)?.name||W(i).find(e=>e.id===W(t).session)?.directory]),G(`click`,n,e=>s(e,W(t).session)),q(e,n)}),j(t),q(e,t)},le=N(()=>(W(y).length||W(a).some(e=>e.pending)||W(o).length)&&!W(h));Y(se,e=>{W(le)&&e(ce)});var ue=B(se,2),de=e=>{var t=Fr(),i=R(t),o=e=>{var t=Ns(),i=L(t),o=L(i),s=L(o);let u;var f=B(s);j(o);var m=B(o,2);j(i);var g=B(i,2),_=e=>{var t=bs(),n=z(t,!0);V(()=>J(n,W(d).directory)),q(e,t)};Y(g,e=>{W(d).name&&W(d).directory&&e(_)});var v=B(g,2),y=z(v),x=B(v,2);let S;var C=L(x),w=L(C);n(w,()=>W(d),()=>!0,()=>!0),j(C);var T=B(C,2),ee=e=>{var t=Fr();qr(R(t),()=>W(d).id,e=>{{let t=N(()=>new URLSearchParams(Ji.querystring).get(`schedule`)===`edit`),n=N(()=>W(d).fileChanges||[]);ns(e,{get session(){return W(d).id},get openScheduler(){return W(t)},get observedCommand(){return W(d).command},get observedFileChanges(){return W(n)},get suspended(){return W(p)},onProtectedChange:e=>{I(l,e,!0)}})}}),q(e,t)},E=e=>{q(e,ks())};Y(T,e=>{$.data?.control?e(ee):e(E,-1)}),j(x);var D=B(x,2);Z(D,17,()=>W(a).filter(e=>e.session===W(d).id),e=>e.session,(e,t)=>{var n=Ms(),r=L(n),i=e=>{var n=As(),r=z(n,!0);V(()=>J(r,W(t).notice)),q(e,n)};Y(r,e=>{W(t).notice&&e(i)});var a=B(r,2),o=e=>{ns(e,{get session(){return W(d).id},review:`profile`})},s=e=>{var t=js();V(e=>Q(t,`href`,e),[()=>`#/sessions/`+encodeURIComponent(W(d).id)+`?review=profile`]),q(e,t)};Y(a,e=>{W(t).pending&&W(p)?e(o):W(t).pending&&e(s,1)}),j(n),V(()=>Q(n,`id`,`quota-profile-`+W(d).id)),q(e,n)});var O=B(D,2),te=e=>{var t=Fr();qr(R(t),()=>W(d).id,e=>{ls(e,{get session(){return W(d)},active:!0})}),q(e,t)};Y(O,e=>{W(p)||e(te)}),qr(B(O,2),()=>W(d).id,e=>{ss(e,{get session(){return W(d)},get stale(){return W(h)}})}),j(t),V((e,t)=>{u=ci(s,1,`lamp lit`,null,u,{working:W(d).status===`WORKING`&&!W(h)}),J(f,`${e??``} // ${(W(d).name||W(d).directory)??``}`),J(y,`${t??``} TOKENS // ${W(d).id??``} // CONTEXT SOURCE // ${(W(d).source||`LOCAL`)??``}`),S=ui(x,``,S,{display:W(p)?`none`:void 0})},[()=>W(h)?`STALE`:W(p)?`QUOTA THRESHOLD`:c(W(d)),()=>la(W(d).tokens)]),G(`click`,m,()=>{b(r().id),ca(r().id,2)}),q(e,t)},s=e=>{q(e,Ps())};Y(i,e=>{W(d)?e(o):e(s,-1)}),q(e,t)},fe=e=>{var t=Ks(),r=R(t),o=B(L(r),2),s=L(o),l=B(s,2);j(o),j(r);var d=B(r,2);Z(d,17,()=>W(i),e=>e.id,(e,t)=>{let r=N(()=>oa(W(t).id));var i=Ws();let o;var s=L(i),l=L(s),d=e=>{var n=Fs(),r=L(n),i=z(r);j(n),V(e=>{Q(r,`aria-pressed`,W(m)===W(t).id),J(i,`${e??``} // ${W(t).name??``}`)},[()=>Array.from(W(t).id).slice(-5).join(``).toUpperCase()]),G(`click`,r,()=>b(W(t).id)),q(e,n)};Y(l,e=>{W(t).name&&e(d)});var f=B(l,2),p=L(f),g=L(p);let _;var v=B(g,1,!0);j(p),j(f);var y=B(f,2),S=e=>{var n=Is(),r=z(n,!0);V(()=>{Q(n,`aria-pressed`,W(m)===W(t).id),J(r,W(t).directory||W(t).id)}),G(`click`,n,()=>b(W(t).id)),q(e,n)};Y(y,e=>{W(t).name||e(S)});var C=B(y,2),T=L(C);we(),j(C);var ee=B(C,2),E=e=>{var n=bs(),r=z(n,!0);V(()=>J(r,W(t).directory)),q(e,n)};Y(ee,e=>{W(t).name&&W(t).directory&&e(E)});var D=B(ee,2),O=z(D),te=B(D,2),ne=z(te),re=B(te,2),ie=L(re),ae=B(ie,2),oe=z(ae,!0),se=B(ae,2);j(re);var ce=B(re,2),le=B(ce,2),ue=e=>{var n=Ls(),r=z(n,!0);V(()=>J(r,W(t).status===`CHECK SESSION`?`INFERRED INACTIVITY`:$.data?.control?`OPEN FULL DETAIL OR REPLY IN CODEX`:W(t).status===`APPROVAL NEEDED`?`APPROVE OR DECLINE IN CODEX`:`REPLY IN CODEX`)),q(e,n)},de=N(()=>!W(h)&&[`APPROVAL NEEDED`,`INPUT NEEDED`,`CHECK SESSION`].includes(W(t).status));Y(le,e=>{W(de)&&e(ue)}),j(s);var fe=B(s,2),pe=e=>{var r=Hs(),i=L(r),o=L(i),s=z(o,!0),c=B(o,2),l=e=>{var n=Rs(),r=z(n,!0);V(e=>{Q(n,`href`,e),J(r,$.data?.control?`REVIEW IN FULL DETAIL ↗`:`APPROVAL IN CODEX ↗`)},[()=>`#/sessions/`+encodeURIComponent(W(t).id)]),q(e,n)};Y(c,e=>{!W(h)&&W(t).status===`APPROVAL NEEDED`&&e(l)}),j(i);var d=B(i,2);n(d,()=>W(t),()=>!1);var f=B(d,2);Z(f,17,()=>W(a).filter(e=>e.session===W(t).id),X,(e,n)=>{var r=Vs(),i=R(r),a=e=>{var t=zs(),r=z(t,!0);V(()=>J(r,W(n).notice)),q(e,t)};Y(i,e=>{W(n).notice&&e(a)});var o=B(i,2),s=e=>{var n=Bs();V(e=>Q(n,`href`,e),[()=>`#/sessions/`+encodeURIComponent(W(t).id)+`?review=profile`]),G(`click`,n,e=>u(e,W(t).id)),q(e,n)};Y(o,e=>{W(n).pending&&e(s)}),q(e,r)});var p=B(f,2);{let e=N(()=>W(m)===W(t).id);ls(p,{get session(){return W(t)},get active(){return W(e)}})}j(r),V(e=>J(s,e),[()=>w(W(t))]),q(e,r)};Y(fe,e=>{W(r)>0&&e(pe)});var me=B(fe,2),he=e=>{var n=Us(),r=B(L(n),2);{let e=N(()=>(W(t).samples||[]).map(e=>e.tokens));Qa(r,{get values(){return W(e)},capacity:120})}var i=z(B(r,2));j(n),V(()=>J(i,`LAST ${(W(t).samples?.length||0)??``} SAMPLES // AUTO SCALE`)),q(e,n)};Y(me,e=>{W(r)<2&&e(he)}),j(i),V((e,n,a,s,c)=>{o=ci(i,1,`session-row`,null,o,{selected:W(m)===W(t).id,wide:W(r)===2,split:W(r)===1}),Q(i,`aria-label`,`Session `+(W(t).name||W(t).directory||W(t).id)),Q(p,`href`,e),_=ci(g,1,`lamp lit`,null,_,{working:W(t).status===`WORKING`&&!W(h)}),J(v,n),J(T,`${a??``} `),J(O,`${W(t).agents??``} LINKED AGENTS`),J(ne,`ACTIVE // ${s??``}`),ie.disabled=W(r)===0,Q(ae,`aria-expanded`,W(r)>0),J(oe,W(r)?`HIDE DETAIL`:`SHOW DETAIL`),Q(ce,`href`,c)},[()=>`#/sessions/`+encodeURIComponent(W(t).id),()=>W(h)?`STALE`:c(W(t)),()=>la(W(t).tokens),()=>ua(W(t).activity),()=>`#/sessions/`+encodeURIComponent(W(t).id)]),G(`click`,p,()=>b(W(t).id)),G(`click`,ie,()=>x(W(t).id,-1)),G(`click`,ae,()=>{b(W(t).id),ca(W(t).id,+!W(r))}),G(`click`,se,()=>x(W(t).id,1)),G(`click`,ce,()=>b(W(t).id)),q(e,i)});var f=B(d,2),p=e=>{q(e,Gs())};Y(f,e=>{W(i).length||e(p)}),G(`click`,s,()=>sa(1)),G(`click`,l,()=>sa(0)),q(e,t)};Y(ue,e=>{r().id?e(de):e(fe,-1)}),V(e=>{J(E,`OBSERVED ${e??``}`),O=ci(D,1,`session-totals`,null,O,{stale:W(h)}),J(ne,`${W(h)?`LAST KNOWN TOTALS — live state counts unavailable. `:``}Tokens + observed since this server started for currently listed sessions; linked + agents are already included. Totals can decrease when a session leaves the + list. History samples every 30 seconds. Not account-wide totals.`)},[()=>ua($.data?.sessionsAt)]),q(e,T),Ke()}wr([`click`]);var Ys=K(`

        `),Xs=K(`

        Relative usage/credits are not tokens or quota percentages. Groupings + describe the same usage, not additional consumption.

        `,1),Zs=K(`// APPROXIMATE`),Qs=K(`

        Partial accounting: totals may still change.

        `),$s=K(`

        Percentage of this period's historical allowance, not today's limit. May + exceed 100%. This is not a manual/automatic reset log.

        `,1),ec=K(`

        `,1),tc=K(`

        This optional account report is unavailable. Token history remains + available; missing data does not mean zero usage.

        `),nc=K(`
        `),rc=K(`
        `),ic=K(`

        No usage reported for this day.

        `),ac=K(`

        Selected breakdown unavailable; missing does not mean zero.

        `),oc=K(`

        `,1);function sc(e,t){Ge(t,!0);let n=F(`surface`),r=F(0),i=N(()=>t.reports?.daily),a=N(()=>t.reports?.plan),o=N(()=>t.mode===`breakdown`?W(i)?.days.length||0:W(a)?.periods.length||0),s=N(()=>Math.min(W(r),Math.max(0,W(o)-1))),c=N(()=>W(i)?.days[(W(i)?.days.length||0)-1-W(s)]),l=N(()=>W(a)?.periods[W(s)]),u=N(()=>t.mode===`breakdown`?W(i)?.units||``:`%`),d=N(()=>{let e=Object.create(null);if(t.mode===`breakdown`)e=W(c)?.groups[W(n)]||{};else{let t=W(n)===`feature`?`thread_source`:W(n)===`task start`?`turn_trigger`:W(n);for(let n of W(l)?.breakdowns||[])if(n.dimension===t)for(let t of n.rows)e[t.key]=(e[t.key]||0)+t.basis_points/100}return Object.entries(e).sort((e,t)=>t[1]-e[1]||e[0].localeCompare(t[0]))}),f=N(()=>t.mode===`breakdown`?W(c)?.total:W(l)?.used_basis_points==null?null:W(l).used_basis_points/100),p=N(()=>Math.max(1,W(f)||0,...W(d).map(([,e])=>e))),m=e=>e==null?`UNKNOWN`:e.toLocaleString(`en-GB`,{maximumFractionDigits:2});var h=oc(),g=R(h),v=L(g),y=B(L(v)),b=L(y);b.value=b.__value=`surface`;var x=B(b);x.value=x.__value=`model`;var S=B(x);S.value=S.__value=`feature`;var C=B(S);C.value=C.__value=`task start`,j(y),mi(y),j(v);var w=B(v,2),T=B(w,2),ee=z(B(T,2),!0);j(g);var E=B(g,2),D=L(E),O=z(D,!0),te=B(D,2),ne=z(te),re=B(te,2),ie=e=>{var t=Xs(),n=R(t),r=z(n),a=B(n,2),o=e=>{var t=Ys(),n=z(t);V(()=>J(n,`Data as of ${W(i).dataAsOf??``}`)),q(e,t)};Y(a,e=>{W(i).dataAsOf&&e(o)});var s=z(B(a,2));we(2),V((e,t)=>{J(r,`Latest fetch ${e??``} // queried ${W(i).from??``} → ${W(i).through??``}. + Reports are fetched from OpenAI and are not saved between runs.`),J(s,`${(W(c)?.date||`No daily buckets reported`)??``} // ${t??``} + ${W(u)??``}`)},[()=>ua(W(i).fetchedAt),()=>m(W(f))]),q(e,t)},ae=e=>{var t=ec(),n=R(t),r=L(n);let i;var o=z(r,!0),s=B(r),c=e=>{q(e,Zs())};Y(s,e=>{W(a).approximate&&e(c)});var u=B(s);j(n);var d=B(n,2),p=L(d),h=B(p),g=e=>{var t=Pr();V(()=>J(t,`// Boundary tolerance ${W(a).boundary_tolerance_seconds??``}s`)),q(e,t)};Y(h,e=>{W(a).boundary_tolerance_seconds!=null&&e(g)}),j(d);var _=B(d,2),v=e=>{var t=$s(),n=R(t),r=L(n),i=B(r);let a;var o=z(i);j(n);var s=B(n,2),c=z(s),u=B(s,2),d=e=>{q(e,Qs())};Y(u,e=>{W(l).accounting_complete||e(d)}),we(2),V(e=>{J(r,`${W(l).window_minutes??``} MIN // ${W(l).plan_type??``} // `),a=ci(i,1,`svelte-1fbui7h`,null,a,{muted:W(f)==null,"quota-near":W(f)!=null&&W(f)>=80&&W(f)<100,"quota-full":W(f)!=null&&W(f)>=100}),J(o,`USED ${e??``}${W(f)==null?``:`%`}`),J(c,`${W(l).starts_at??``} → ${W(l).ends_at??``}`)},[()=>m(W(f))]),q(e,t)};Y(_,e=>{W(l)&&e(v)}),V(e=>{i=ci(r,1,`svelte-1fbui7h`,null,i,{"quality-warning":!W(a).coverage_complete}),J(o,W(a).coverage_complete?`Reported range complete`:`Partial coverage`),J(u,` // Latest fetch ${e??``}`),J(p,`Coverage starts ${(W(a).coverage_start||`unknown`)??``} // Data as of ${(W(a).data_as_of||`unknown`)??``}`)},[()=>ua(W(a).fetchedAt)]),q(e,t)},oe=e=>{q(e,tc())};Y(re,e=>{t.mode===`breakdown`&&W(i)?e(ie):t.mode===`periods`&&W(a)?e(ae,1):e(oe,-1)});var se=B(re,2),ce=e=>{var n=rc(),r=L(n),i=z(B(L(r),2),!0);j(r),Z(B(r,2),17,()=>W(d),X,(e,n,r)=>{var i=N(()=>_(W(n),2));let a=()=>W(i)[0],o=()=>W(i)[1];var s=nc();let c;var l=L(s),u=z(l,!0),d=B(l,2),f=L(d);let h;j(d);var g=z(B(d,2));j(s),V((e,n,i)=>{c=ci(s,1,`report-row svelte-1fbui7h`,null,c,{unknown:e,alternate:r%2==1}),Q(l,`title`,a()),J(u,a()),h=ui(f,``,h,{width:n}),J(g,`${i??``}${t.mode===`periods`?`%`:``}`)},[()=>a().trim().toLowerCase()===`unknown`,()=>`${Math.max(0,100*o()/W(p))}%`,()=>m(o())]),q(e,s)}),j(n),V(()=>J(i,t.mode===`breakdown`?W(u):`QUOTA USED`)),q(e,n)},le=e=>{q(e,ic())},ue=e=>{q(e,ac())};Y(se,e=>{W(d).length?e(ce):t.mode===`breakdown`&&W(c)?.total===0&&W(c)?.groups[W(n)]!=null?e(le,1):(W(i)&&t.mode===`breakdown`||W(a)&&t.mode===`periods`)&&e(ue,2)}),j(E),V(()=>{w.disabled=W(s)>=W(o)-1,T.disabled=W(s)===0,J(ee,W(o)?`${W(s)+1} / ${W(o)}`:`NO DATA`),J(O,t.mode===`breakdown`?`DAILY BREAKDOWN`:`QUOTA WINDOWS`),J(ne,`${(t.mode===`breakdown`?t.reports?.dailyStatus||`UNAVAILABLE`:t.reports?.planStatus||`UNAVAILABLE`)??``} // UTC`)}),hi(y,()=>W(n),e=>I(n,e)),G(`click`,w,()=>I(r,W(s)+1)),G(`click`,T,()=>I(r,W(s)-1)),q(e,h),Ke()}wr([`click`]);var cc=864e5;function lc(e,t){let n=new Date(e);n.setUTCDate(1),n.setUTCMonth(n.getUTCMonth()+t);let r=new Date(Date.UTC(n.getUTCFullYear(),n.getUTCMonth()+1,0)).getUTCDate();return n.setUTCDate(Math.min(e.getUTCDate(),r)),n}function uc(e,t,n){let r=new Date(Date.UTC(e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate()));for(let e=0;;e++){let i=lc(r,-t),a=new Date(i.getTime()+cc);if(e===n)return{start:a,end:r};r=i}}var dc=K(``),fc=K(``,1),pc=K(`

        History refresh failed. Any displayed history is the last successful + observation.

        `),mc=K(`

        Token history unavailable; other reports may still be available.

        `),hc=K(``),gc=K(`
        `),_c=K(`

        LESS ░ ▒ ▓ █ MORE // HOVER FOR DATE AND TOKENS

        `,1),vc=K(`
        `,1),yc=K(` `),bc=K(`

        LIFETIME TOKENS

        PEAK DAY

        CURRENT STREAK

        DAYS

        LONGEST TURN

        SECONDS

        LONGEST STREAK

        DAYS

        OPENAI // UTC

        Accessible data table
        Date (UTC)Tokens

        `,1),xc=K(`

        History unavailable or awaiting a matching account observation. Missing + history is not treated as zero usage.

        `),Sc=K(`

        USAGE // ACCOUNT HISTORY

        Account-wide history reported by Codex, not the local Sessions counter. Dates + use UTC. Allowance history is approximate where indicated; reset causes are + not provided.

        `,1);function Cc(e,t){Ge(t,!0);let n=F(`daily`),r=F(12),i=F(0),a=N(()=>{let e=$.data?.usage?.dailyUsageBuckets;if(!e)return[];let{start:t,end:n}=uc(new Date,W(r),W(i)),a=new Map;for(let t of e)!Number.isFinite(t.tokens)||t.tokens<0||a.set(t.startDate,(a.get(t.startDate)||0)+t.tokens);let o=[];for(let e=t;e<=n;e=new Date(e.getTime()+864e5)){let t=e.toISOString().slice(0,10);o.push({date:t,tokens:a.get(t)||0})}return o}),o=N(()=>Math.max(1,...W(a).map(e=>e.tokens))),s=N(()=>W(a).length?new Date(W(a)[0].date+`T00:00:00Z`).getUTCDay():0),c=N(()=>{if(W(n)===`monthly`){let e=new Map;for(let t of W(a))e.set(t.date.slice(0,7),(e.get(t.date.slice(0,7))||0)+t.tokens);return[...e].map(([e,t])=>({date:e,tokens:t}))}let e=0;return W(a).map(t=>({date:t.date,tokens:e+=t.tokens}))});var l=Sc(),u=B(R(l),4);Z(u,20,()=>[[`daily`,`Daily`],[`monthly`,`Monthly`],[`cumulative`,`Cumulative`],[`breakdown`,`Breakdown`],[`periods`,`Windows`]],X,(e,t)=>{var r=N(()=>_(t,2));let a=()=>W(r)[0],o=()=>W(r)[1];var s=dc();let c;var l=z(s,!0);V(()=>{Q(s,`aria-pressed`,W(n)===a()),c=ci(s,1,``,null,c,{active:W(n)===a()}),J(l,o())}),G(`click`,s,()=>{I(n,a(),!0),I(i,0)}),q(e,s)}),j(u);var d=B(u,2),f=L(d),p=e=>{var t=fc(),n=R(t),a=B(L(n)),o=L(a);o.value=o.__value=6;var s=B(o);s.value=s.__value=12,j(a),mi(a),j(n);var c=B(n),l=B(c);V(()=>l.disabled=W(i)===0),G(`change`,a,()=>I(i,0)),hi(a,()=>W(r),e=>I(r,e)),G(`click`,c,()=>Jt(i)),G(`click`,l,()=>Jt(i,-1)),q(e,t)};Y(f,e=>{W(n)!==`breakdown`&&W(n)!==`periods`&&e(p)}),j(d);var m=B(d,2),h=e=>{q(e,pc())};Y(m,e=>{$.data?.usageError&&e(h)});var g=B(m,2),v=e=>{q(e,mc())};Y(g,e=>{$.data?.usage?.tokenStatus===`UNAVAILABLE`&&W(n)!==`breakdown`&&W(n)!==`periods`&&e(v)});var y=B(g,2),b=e=>{var t=Fr();qr(R(t),()=>W(n),e=>{{let t=N(()=>$.data?.usage?.reports);sc(e,{get reports(){return W(t)},get mode(){return W(n)}})}}),q(e,t)},x=e=>{var t=bc(),r=R(t),i=L(r),l=z(B(L(i),2),!0);j(i);var u=B(i,2),d=z(B(L(u),2),!0);j(u);var f=B(u,2),p=B(L(f),2),m=L(p);we(),j(p),j(f);var h=B(f,2),g=B(L(h),2),_=L(g);we(),j(g),j(h);var v=B(h,2),y=B(L(v),2),b=L(y);we(),j(y),j(v),j(r);var x=B(r,4),S=L(x),C=z(S),w=B(S,2),T=e=>{var t=_c(),n=R(t),r=L(n),i=L(r);Z(i,17,()=>Array(W(s)),X,(e,t)=>{q(e,hc())}),Z(B(i,2),17,()=>W(a),X,(e,t)=>{var n=gc();let r,i;V(e=>{r=ci(n,1,`heat-cell`,null,r,{zero:W(t).tokens===0}),Q(n,`title`,e),i=ui(n,``,i,{opacity:W(t).tokens?.25+.75*W(t).tokens/W(o):1})},[()=>`${W(t).date}: ${la(W(t).tokens)} tokens`]),q(e,n)}),j(r),j(n),we(2),q(e,t)},ee=e=>{var t=vc(),r=R(t);{let e=N(()=>W(c).map(e=>e.tokens)),t=N(()=>W(n)+` usage`);Qa(r,{get values(){return W(e)},get label(){return W(t)}})}var i=B(r,2),a=L(i),o=z(a,!0),s=z(B(a),!0);j(i),V(e=>{J(o,W(c)[0]?.date),J(s,e)},[()=>W(c).at(-1)?.date]),q(e,t)};Y(w,e=>{W(n)===`daily`?e(T):e(ee,-1)});var E=B(w,2),D=B(L(E),2),O=L(D),te=B(L(O));Z(te,21,()=>W(n)===`daily`?W(a):W(c),X,(e,t)=>{var n=yc(),r=L(n),i=z(r,!0),a=z(B(r),!0);j(n),V(e=>{J(i,W(t).date),J(a,e)},[()=>la(W(t).tokens)]),q(e,n)}),j(te),j(O),j(D),j(E),j(x);var ne=z(B(x,2));V((e,t,n,r,i,o,s)=>{J(l,e),J(d,t),J(m,`${n??``} `),J(_,`${r??``} `),J(b,`${i??``} `),J(C,`${W(a)[0]?.date??``} — ${o??``}`),J(ne,`OBSERVED ${s??``} // CUMULATIVE IS FOR THE SELECTED PERIOD`)},[()=>la($.data.usage.summary.lifetimeTokens),()=>la($.data.usage.summary.peakDailyTokens),()=>la($.data.usage.summary.currentStreakDays),()=>la($.data.usage.summary.longestRunningTurnSec),()=>la($.data.usage.summary.longestStreakDays),()=>W(a).at(-1)?.date,()=>ua($.data.usageAt)]),q(e,t)},S=e=>{q(e,xc())};Y(y,e=>{W(n)===`breakdown`||W(n)===`periods`?e(b):$.data?.usage&&$.data.usage.dailyUsageBuckets!==null?e(x,1):e(S,-1)}),q(e,l),Ke()}wr([`click`,`change`]);var wc=K(`

        Page not found

        Return to Quota

        `,1);function Tc(e){var t=wc();we(2),q(e,t)}var Ec=K(` `),Dc=K(`

        `),Oc=K(`

        Connecting to your local Codexometer…

        `),kc=K(``),Ac=K(`
        CODEXOMETER

        Your quota. Your sessions. Your command centre.

        `);function jc(e,t){Ge(t,!0);let n={"/":Ya,"/quota/:view?":Ya,"/sessions/:id?":Js,"/usage":Cc,"*":Tc},r={quota:/^(?:\/|\/quota(?:\/[^/]+)?\/?)$/,sessions:/^\/sessions(?:\/[^/]+)?\/?$/,usage:/^\/usage\/?$/},i=F(`hacker`),a=[`hacker`,`rust`,`blue-steel`,`ultraviolet`,`nightshade`];bn(()=>{ia()}),bn(()=>{if($.data)for(let[e,t]of Object.entries(r))t.test(Ji.location)&&(na.tab=e)}),Wi(()=>{try{let e=localStorage.getItem(`codexometer.web.theme`);e&&a.includes(e)&&I(i,e,!0)}catch{}return ha()});function o(){try{localStorage.setItem(`codexometer.web.theme`,W(i))}catch{}}var s=Ac(),c=L(s),l=B(L(c),2),u=L(l);let d;var f=B(u,1,!0),p=z(B(f));j(l),j(c);var m=B(c,2);Z(m,21,()=>Object.entries(r),X,(e,t)=>{var n=N(()=>_(W(t),2));let r=()=>W(n)[0],i=()=>W(n)[1],a=N(()=>i().test(Ji.location));var o=Ec();let s;var c=z(o,!0);V(e=>{Q(o,`href`,r()===`quota`?`#/quota/`+na.view:`#/`+r()),Q(o,`aria-current`,W(a)?`page`:void 0),s=ci(o,1,``,null,s,{active:W(a)}),J(c,e)},[()=>r().toUpperCase()]),q(e,o)}),j(m);var h=B(m,2),g=L(h),v=e=>{var t=Dc(),n=z(t,!0);V(()=>J(n,$.error)),q(e,t)};Y(g,e=>{$.error&&e(v)});var y=B(g,2),b=e=>{Zi(e,{get routes(){return n}})},x=e=>{q(e,Oc())};Y(y,e=>{$.data?e(b):$.error||e(x,1)}),j(h);var S=B(h,2),C=L(S),w=z(C),T=B(C,2),ee=z(T,!0),E=B(T,2),D=B(L(E));Z(D,21,()=>a,X,(e,t)=>{var n=kc(),r=z(n,!0),i={};V(e=>{J(r,e),i!==(i=W(t))&&(n.value=(n.__value=i)??``)},[()=>W(t).replace(`-`,` `).toUpperCase()]),q(e,n)}),j(D),mi(D),j(E),j(S),j(s),V(()=>{Q(s,`data-theme`,W(i)),d=ci(u,1,`lamp`,null,d,{lit:$.connected}),J(f,$.connected?`CONNECTED`:`OFFLINE`),J(p,`EXPERIMENTAL // ${$.data?.control?`SESSION CONTROL`:`READ ONLY`}`),J(w,`v${($.data?.version||`…`)??``} // LOCAL ONLY`),J(ee,$.data?.control?`Session control enabled. Stop the server to revoke access.`:`No actions can be sent from this preview.`)}),G(`change`,D,o),hi(D,()=>W(i),e=>I(i,e)),q(e,s),Ke()}wr([`change`]),Vr(jc,{target:document.getElementById(`app`)}); \ No newline at end of file diff --git a/internal/web/dist/assets/index-f6yvkeCn.css b/internal/web/dist/assets/index-f6yvkeCn.css new file mode 100644 index 0000000..ad4be38 --- /dev/null +++ b/internal/web/dist/assets/index-f6yvkeCn.css @@ -0,0 +1 @@ +.observation-details.svelte-1wk8llw{font-size:12px}summary.svelte-1wk8llw{cursor:pointer;color:var(--accent)}.observation-table.svelte-1wk8llw{max-height:240px;overflow:auto}.zone-canvas.svelte-1wk8llw{flex:1;min-height:170px;margin-top:6px;position:relative}svg.svelte-1wk8llw{width:100%;height:100%;display:block;position:absolute;inset:0}text.svelte-1wk8llw{fill:var(--ink);font:14px Cascadia Code,Consolas,monospace}.axis-title.svelte-1wk8llw{letter-spacing:.04em;font-size:12px}.grid.svelte-1wk8llw{stroke:#fff;stroke-opacity:.14;stroke-width:1px}.axes.svelte-1wk8llw{stroke:var(--ink);stroke-width:1.5px;fill:none}.pace-line.svelte-1wk8llw{stroke:#fff;stroke-width:2px;stroke-dasharray:6 4}.position-halo.svelte-1wk8llw{fill:#101820;stroke:#fff;stroke-width:1.5px}.position-dot.svelte-1wk8llw{fill:#fff}.observation-trail.svelte-1wk8llw{fill:none;stroke:#fff;stroke-width:2.5px;stroke-linejoin:round}.trail-start.svelte-1wk8llw{fill:none;stroke:#fff;stroke-width:2px}.trail-caption.svelte-1wk8llw{text-align:center;font-size:11px}.zone-caption.svelte-1wk8llw{color:var(--accent);text-align:center}.slash-commands.svelte-7bfwr9{margin:.4rem 0;position:relative}.command-popup.svelte-7bfwr9{z-index:5;box-sizing:border-box;border:1px solid var(--accent);background:var(--panel);width:min(40rem,100%);padding:.35rem;position:absolute;bottom:100%;left:0;box-shadow:0 .3rem 1rem #0006}.command-popup.svelte-7bfwr9 p:where(.svelte-7bfwr9){margin:.3rem 0;font-size:.8em}.command-options.vertical.svelte-7bfwr9{flex-flow:column;gap:.15rem;display:flex}.vertical.svelte-7bfwr9 button:where(.svelte-7bfwr9){text-align:left;white-space:nowrap;width:100%;min-width:0;color:var(--muted);background:var(--panel);border-color:#0000;flex:none;align-items:baseline;gap:.4rem;display:flex}.vertical.svelte-7bfwr9 .command-name:where(.svelte-7bfwr9),.vertical.svelte-7bfwr9 .command-tail:where(.svelte-7bfwr9){flex:none}.vertical.svelte-7bfwr9 .command-name:where(.svelte-7bfwr9){text-overflow:ellipsis;max-width:100%;overflow:hidden}.vertical.svelte-7bfwr9 button.suggested:where(.svelte-7bfwr9){outline:none}.vertical.svelte-7bfwr9 button.suggested:where(.svelte-7bfwr9) .command-name:where(.svelte-7bfwr9),.vertical.svelte-7bfwr9 button:where(.svelte-7bfwr9):hover .command-name:where(.svelte-7bfwr9){background:var(--accent);color:var(--bg)}.vertical.svelte-7bfwr9 .command-help:where(.svelte-7bfwr9){min-width:0;max-width:none;color:var(--muted);flex:1}.command-options.svelte-7bfwr9{flex-wrap:wrap;gap:.35rem;max-height:35vh;display:flex;overflow:auto}.command-options.svelte-7bfwr9 .suggested:where(.svelte-7bfwr9){outline:1px solid var(--accent)}.command-help.svelte-7bfwr9{text-overflow:ellipsis;white-space:nowrap;opacity:.75;max-width:32ch;font-size:.8em;display:block;overflow:hidden}pre.svelte-7bfwr9{white-space:pre-wrap;overflow-wrap:anywhere;max-height:35vh;overflow:auto}.file-diff.svelte-x4gtb6{border:1px solid var(--muted);max-height:60vh;padding:.5rem;font-family:monospace;overflow:auto}.diff-line.svelte-x4gtb6{white-space:pre;min-height:1.3em;display:flex}.line-number.svelte-x4gtb6{text-align:right;opacity:.75;-webkit-user-select:none;user-select:none;flex:0 0 5ch;padding-right:1ch;display:inline-block}.added.svelte-x4gtb6{color:#67d391;background:#67d39112}.removed.svelte-x4gtb6{color:#ff6b83;background:#ff6b8312}.heading.svelte-x4gtb6{padding:.5rem 0;font-weight:700}.saved-prompt.svelte-1oupzfc{white-space:pre-wrap;overflow-wrap:anywhere}fieldset.svelte-1oupzfc{border:1px solid;margin:.5rem 0;padding:.35rem .6rem}.decision.svelte-1oupzfc,.answer.svelte-1oupzfc{margin:.3rem 0;display:block}.decision.svelte-1oupzfc code:where(.svelte-1oupzfc),.decision.svelte-1oupzfc span:where(.svelte-1oupzfc){overflow-wrap:anywhere;margin:.3rem 0 .3rem 1.5rem;display:block}textarea.svelte-1oupzfc,.answer.svelte-1oupzfc input:where(.svelte-1oupzfc),.answer.svelte-1oupzfc select:where(.svelte-1oupzfc){box-sizing:border-box;background:var(--bg);width:100%;color:inherit;font:inherit;border:1px solid;margin-top:.4rem;padding:.5rem;display:block}textarea.svelte-1oupzfc{resize:vertical;background:color-mix(in srgb, currentColor 8%, var(--bg))}button.svelte-1oupzfc{margin:.3rem .5rem .3rem 0}h3.svelte-1oupzfc,p.svelte-1oupzfc{margin-block:.4rem}.sent.svelte-1oupzfc{color:var(--accent)}.status-line.svelte-gprptn{align-items:center;gap:.5rem;min-width:0;margin-top:.5rem;display:flex}.status-line.svelte-gprptn p:where(.svelte-gprptn){white-space:nowrap;text-overflow:ellipsis;flex:1;min-width:0;margin:0;font-size:.8rem;overflow:hidden}.configure.svelte-gprptn{padding:.15rem .35rem;font-size:.75rem}.status-picker.svelte-gprptn{border:1px solid var(--edge);margin:.5rem 0;padding:.6rem}.fields.svelte-gprptn{max-height:35vh;overflow:auto}.field.svelte-gprptn{align-items:center;gap:.35rem;display:flex}.field.svelte-gprptn label:where(.svelte-gprptn){flex:1;align-items:center;gap:.4rem;display:flex}.field.svelte-gprptn label:where(.svelte-gprptn) span:where(.svelte-gprptn){font-size:.75rem}.field.svelte-gprptn button:where(.svelte-gprptn){padding:.1rem .35rem}.session-copy.svelte-543j00{justify-content:flex-end;align-items:center;gap:8px;margin-top:auto;padding-top:6px;display:flex}span.svelte-543j00{color:var(--muted);overflow-wrap:anywhere;font-size:12px}button.svelte-543j00{color:var(--accent);flex-shrink:0;padding:3px 7px}button.svelte-543j00:hover,button.flashed.svelte-543j00{color:var(--bg);background:var(--accent)}h3.svelte-1fbui7h,.window-dates.svelte-1fbui7h,.report-value.svelte-1fbui7h{color:var(--accent);font-weight:700}.quality-warning.svelte-1fbui7h,.quota-near.svelte-1fbui7h{color:var(--approval)}.quota-full.svelte-1fbui7h{color:#ff6680}.alternate.svelte-1fbui7h .report-fill:where(.svelte-1fbui7h){background:radial-gradient(circle, var(--panel) .7px, transparent .9px) 0 0 / 3px 3px, var(--accent)}.unknown.svelte-1fbui7h,.unknown.svelte-1fbui7h .report-value:where(.svelte-1fbui7h){color:var(--muted)}.unknown.svelte-1fbui7h .report-fill:where(.svelte-1fbui7h){background:var(--muted);opacity:1}.report-bars.svelte-1fbui7h{grid-template-columns:minmax(5rem,1fr) minmax(2rem,3fr) auto;gap:.6rem;display:grid}.report-row.svelte-1fbui7h,.report-head.svelte-1fbui7h{grid-column:1/-1;grid-template-columns:subgrid;align-items:center;gap:.6rem;display:grid}.report-head.svelte-1fbui7h{color:var(--muted);font-size:.85rem}.report-head.svelte-1fbui7h>span:where(.svelte-1fbui7h):last-child,.report-value.svelte-1fbui7h{text-align:right}.report-row.svelte-1fbui7h>span:where(.svelte-1fbui7h):first-child{overflow-wrap:anywhere}.report-track.svelte-1fbui7h{background:var(--panel);height:1rem}.report-fill.svelte-1fbui7h{background:var(--accent);height:100%}:root{font-synthesis:none;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;background:#080e0c;font-family:Cascadia Code,SFMono-Regular,Consolas,monospace}*{box-sizing:border-box}body{margin:0}.shell{--accent:#80edac;--approval:#ffca58;--muted:#98aaa0;--edge:#31473a;--panel:#101c16;--bg:#080e0c;--ink:#e2eee5;background:var(--bg);height:100dvh;color:var(--ink);grid-template-rows:auto auto minmax(0,1fr) auto;padding:clamp(10px,1.2vw,20px);display:grid}.shell[data-theme=rust]{--approval:#ff8a3d;--accent:#ffbc75;--muted:#bfac95;--edge:#59412c;--panel:#261b14;--bg:#140e0a}.shell[data-theme=blue-steel]{--approval:#e8c46a;--accent:#85c8ff;--muted:#9cacc4;--edge:#334d69;--panel:#142235;--bg:#0b1421}.shell[data-theme=ultraviolet]{--approval:#f9a8d4;--accent:#c5adff;--muted:#b3a6c9;--edge:#4b3c68;--panel:#241a35;--bg:#140d22}.shell[data-theme=nightshade]{--approval:#8f7cff;--accent:#e59bff;--muted:#c6a7cf;--edge:#673976;--panel:#301739;--bg:#1c0b24}header,.spread,footer,.controls{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:10px;display:flex}header{margin-bottom:10px}.brand{letter-spacing:.08em;color:var(--accent);text-shadow:0 0 22px color-mix(in srgb, var(--accent) 20%, transparent);font-size:clamp(22px,2.5vw,32px);font-weight:800;text-decoration:none}header p{color:var(--muted);margin:3px 0 0;font-size:13px}.connection{color:var(--accent);text-align:right}small,.eyebrow{letter-spacing:.04em;font-size:12px}.connection small{color:var(--muted);margin-top:4px;display:block}.lamp{background:var(--muted);border-radius:50%;width:9px;height:9px;margin-right:9px;display:inline-block}.lamp.lit{background:var(--accent);box-shadow:0 0 8px color-mix(in srgb, var(--accent) 40%, transparent)}.working{animation:1.5s ease-in-out infinite pulse}@keyframes pulse{50%{opacity:.3}}nav{flex-wrap:wrap;gap:6px;margin-bottom:10px;display:flex}nav a,button,.button,select{font:inherit;color:var(--accent);border:1px solid var(--edge);background:var(--panel);cursor:pointer;border-radius:3px;padding:7px 11px;font-size:13px;text-decoration:none}button:hover,.button:hover,nav a:hover{border-color:var(--accent)}nav a.active,nav button.active{background:var(--accent);color:var(--bg);border-color:var(--accent)}:focus-visible{outline:2px solid var(--accent);outline-offset:4px}.secondary{margin-bottom:8px}.secondary a,.secondary button{padding:6px 10px;font-size:12px}main{scrollbar-gutter:stable;flex-direction:column;min-width:0;min-height:0;display:flex;overflow:auto}main>*{flex-shrink:0}.panel{border:1px solid var(--edge);border-top:2px solid var(--accent);background:var(--panel);border-radius:4px;min-width:0;padding:clamp(10px,1vw,16px)}h1{color:var(--accent);font-size:20px}h2{letter-spacing:.04em;color:var(--accent);overflow-wrap:anywhere;margin:0 0 10px;font-size:13px}h3{overflow-wrap:anywhere;font-size:13px}p{margin:6px 0;font-size:13px;line-height:1.45}a{color:var(--accent)}.muted,.eyebrow{color:var(--muted)}.notice{color:#ffe0aa;background:#332611;border-left:3px solid #ffc26e;padding:12px 16px}.empty{color:var(--muted);padding:35px 0}.quota-grid{flex:1 0 auto;grid-auto-rows:minmax(220px,1fr);gap:10px;display:grid}.quota-card{flex-direction:column;min-height:0;display:flex}.quota-card>:not(.meter-graphic){flex-shrink:0}.meter-graphic{flex-direction:column;flex:1;min-height:100px;display:flex}.bar-graphic{min-height:65px}.quota-card .gauge{flex:2;height:auto;min-height:16px;margin:7px 0}.quota-card .timeline{flex:1;min-height:8px}.quota-card .pace{flex:1;height:auto;min-height:20px;margin:20px 14px 8px}.quota-card .readout{margin:6px 0;font-size:clamp(18px,2vw,28px)}.quota-grid.zone{grid-template-columns:repeat(auto-fit,minmax(min(100%,420px),1fr));grid-auto-rows:minmax(330px,1fr)}.quota-grid.radial{grid-template-columns:repeat(auto-fit,minmax(min(100%,300px),1fr));grid-auto-rows:minmax(260px,1fr)}.gauge{background:var(--edge);height:35px;margin:15px 0;overflow:hidden}.gauge>div{background:repeating-linear-gradient(90deg, var(--accent) 0, var(--accent) 9px, transparent 9px, transparent 12px);height:100%;transition:width .5s}.gauge.timeline{height:14px}.pie-wrap{flex:1;min-height:100px;margin:6px 0;position:relative}.pie-wrap svg{width:100%;height:100%;position:absolute;inset:0}.pie-base{fill:var(--edge);stroke:var(--accent);stroke-width:1px}.pie-fill{fill:var(--accent)}.pace{background:linear-gradient(90deg, #6d3838, var(--edge) 50%, #376348);height:28px;margin:28px 14px 15px;position:relative}.pace-mid{border-left:2px solid var(--ink);height:100%;position:absolute;left:50%}.pace-marker{color:var(--accent);font-size:27px;position:absolute;top:-16px;transform:translate(-50%)}.readout{overflow-wrap:anywhere;color:var(--accent);margin:18px 0;font-size:clamp(22px,3vw,34px)}.credit+.credit{border-top:1px solid var(--edge);padding-top:20px}.credit{margin-top:24px}.session-row{grid-template-columns:minmax(210px,1.1fr) minmax(0,1.2fr) minmax(0,1.5fr);align-items:stretch;gap:14px;margin:18px 0;display:grid}.session-totals{grid-template-columns:repeat(auto-fit,minmax(min(145px,100%),1fr));gap:8px;margin:8px 0;display:grid}.session-totals>div{border:1px solid var(--edge);min-width:0;padding:10px}.session-totals dt{color:var(--muted);font-size:11px}.session-totals dd{color:var(--accent);overflow-wrap:anywhere;margin:6px 0 0;font-size:24px}.session-totals.stale dd{color:var(--muted)}.session-row.selected{outline:1px solid var(--accent);outline-offset:4px}.session-row.wide .context{grid-column:2/-1}.detail-controls{flex-wrap:wrap;gap:6px;display:flex}.attention-summary{flex-wrap:wrap;gap:8px;margin-block:8px;display:flex}.attention-summary .approval{color:var(--approval);border-color:var(--approval)}.attention-summary .approval:hover{background:color-mix(in srgb, var(--approval) 12%, var(--panel))}.attention-summary .approval:focus-visible{outline-color:var(--approval)}.attention-note,.attention-badge{color:var(--accent);border-left:3px solid;padding-left:8px}.attention-note.inferred{color:var(--muted)}.attention-badge{font-size:12px}.session-select{text-align:left;overflow-wrap:anywhere;max-width:100%}.status-detail{color:inherit;text-decoration:none}.status-detail:hover{text-decoration:underline}button:disabled{opacity:.4;cursor:default}.session-row .graph-panel{flex-direction:column;grid-column:2/-1;display:flex}.session-row.split .graph-panel{grid-column:auto}.session-row .button,.session-row button{margin-top:8px;display:inline-block}.context pre{max-height:230px;overflow:auto}.session-row .context{flex-direction:column;display:flex}pre{font:inherit;white-space:pre-wrap;overflow-wrap:anywhere;font-size:13px;line-height:1.7}.full-detail{margin-top:6px}.detail-heading{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:6px 12px;display:flex}.detail-heading h2{flex:250px;min-width:0;margin:0}.detail-metadata{overflow-wrap:anywhere}.detail-workspace{border-top:1px solid var(--edge);flex-direction:column;gap:10px;margin-top:8px;padding-top:4px;display:flex}.detail-context,.detail-workspace .session-actions{min-width:0}.detail-workspace .session-actions{border-top:1px solid var(--edge);padding-top:6px}.full-detail h3{margin-block:8px}.full-detail pre{margin-block:8px;line-height:1.5}.full-detail hr{margin-block:12px}.full-detail .command,.full-detail .notice{padding:8px 10px}.command{background:var(--bg);border-left:3px solid var(--accent);padding:16px}hr{border:0;border-top:1px solid var(--edge);margin:24px 0}.chart{border-bottom:1px solid var(--muted);align-items:flex-end;gap:2px;height:clamp(120px,22vh,320px);margin-top:18px;display:flex}.chart-bar{background:var(--accent);flex:1;min-width:0;max-height:100%}.controls{justify-content:flex-start;margin:20px 0}.summary-grid{grid-template-columns:repeat(auto-fit,minmax(min(100%,220px),1fr));gap:16px;margin:20px 0;display:grid}.thresholds-panel{padding:clamp(14px,2vw,22px)}.thresholds-panel h1{margin-top:0}.threshold-list{gap:8px;margin-top:16px;display:grid}.threshold-list article{border:1px solid var(--edge);grid-template-columns:minmax(64px,.5fr) minmax(220px,3fr) minmax(64px,.6fr) minmax(120px,1.2fr);align-items:center;gap:12px;padding:10px 12px;display:grid}.threshold-list article.active{border-color:var(--accent);box-shadow:inset 3px 0 0 var(--accent)}.threshold-list article.next{border-color:var(--approval);box-shadow:inset 3px 0 0 var(--approval)}.threshold-list p{color:var(--muted);margin:3px 0 0;font-size:12px}.threshold-trigger{color:var(--accent);font-size:22px;font-weight:800}.threshold-mode,.threshold-state{color:var(--muted);font-size:12px;font-weight:700}.threshold-list article.active .threshold-state{color:var(--accent)}.threshold-list article.next .threshold-state{color:var(--approval)}.heat-scroll{overflow-x:auto}.heatmap{grid-template-rows:repeat(7,13px);grid-auto-columns:minmax(9px,1fr);grid-auto-flow:column;gap:4px;min-width:620px;margin:25px 0;display:grid}.heat-cell{background:var(--accent);border-radius:2px}.heat-cell.zero{background:var(--edge)}.table-scroll{max-height:280px;overflow:auto}table{border-collapse:collapse;width:100%;margin-top:15px}th,td{text-align:left;border-bottom:1px solid var(--edge);padding:8px}summary{cursor:pointer;color:var(--accent);padding-top:14px}footer{border-top:1px solid var(--edge);color:var(--muted);margin-top:8px;padding-top:8px;font-size:11px}footer select{padding:6px}@media (width<=850px){.session-row{grid-template-columns:minmax(0,1fr)}.session-row.wide .context,.session-row .graph-panel{grid-column:auto}.connection{text-align:left}.threshold-list article{grid-template-columns:64px minmax(0,1fr)}.threshold-mode,.threshold-state{grid-column:2}}@media (prefers-reduced-motion:reduce){*,:before,:after{transition:none!important;animation:none!important}} diff --git a/internal/web/dist/index.html b/internal/web/dist/index.html index 4bf5b01..cc955b1 100644 --- a/internal/web/dist/index.html +++ b/internal/web/dist/index.html @@ -5,8 +5,8 @@ Codexometer // Experimental web - - + +
        diff --git a/internal/web/server.go b/internal/web/server.go index 6e1cb1b..0b6029b 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -103,7 +103,7 @@ func (s *server) handler() http.Handler { }))) mux.Handle("GET /api/events", s.authorize(http.HandlerFunc(s.events))) if s.control != nil { - for _, action := range []string{"offer", "prepare", "commit", "schedules"} { + for _, action := range []string{"offer", "prepare", "commit", "schedules", "commands"} { mux.Handle("POST /api/control/"+action, s.authorize(http.HandlerFunc(s.control.handle(action, "http://"+s.host)))) } } diff --git a/internal/web/state.go b/internal/web/state.go index d42d0cd..3dc48a1 100644 --- a/internal/web/state.go +++ b/internal/web/state.go @@ -9,6 +9,7 @@ import ( "time" "github.com/merefield/codexometer/internal/codex" + "github.com/merefield/codexometer/internal/statusline" "github.com/merefield/codexometer/internal/version" ) @@ -21,6 +22,7 @@ type Source interface { // Deliberately project source types: never serialize approval/input capabilities, // account fingerprints, arbitrary errors, or authentication objects to browsers. type session struct { + StatusLine map[string]string `json:"statusLine"` FileChanges []codex.FileDiffLine `json:"fileChanges,omitempty"` CurrentTask string `json:"currentTask,omitempty"` LatestGuidance string `json:"latestGuidance,omitempty"` @@ -118,23 +120,24 @@ type quotaThreshold struct { } type state struct { - Triggers []triggerSummary `json:"triggers,omitempty"` - Profiles []profileReview `json:"profiles,omitempty"` - Thresholds []quotaThreshold `json:"thresholds,omitempty"` - ProfileError bool `json:"profileError,omitempty"` - Control bool `json:"control"` - Version string `json:"version"` - Meters []meter `json:"meters"` - Credits []credit `json:"credits"` - CreditCount int `json:"creditCount"` - Sessions []session `json:"sessions"` - Usage *codex.AccountUsage `json:"usage"` - QuotaAt time.Time `json:"quotaAt"` - SessionsAt time.Time `json:"sessionsAt"` - UsageAt time.Time `json:"usageAt"` - QuotaError bool `json:"quotaError"` - SessionsError bool `json:"sessionsError"` - UsageError bool `json:"usageError"` + StatusLineFields []statusline.Field `json:"statusLineFields"` + Triggers []triggerSummary `json:"triggers,omitempty"` + Profiles []profileReview `json:"profiles,omitempty"` + Thresholds []quotaThreshold `json:"thresholds,omitempty"` + ProfileError bool `json:"profileError,omitempty"` + Control bool `json:"control"` + Version string `json:"version"` + Meters []meter `json:"meters"` + Credits []credit `json:"credits"` + CreditCount int `json:"creditCount"` + Sessions []session `json:"sessions"` + Usage *codex.AccountUsage `json:"usage"` + QuotaAt time.Time `json:"quotaAt"` + SessionsAt time.Time `json:"sessionsAt"` + UsageAt time.Time `json:"usageAt"` + QuotaError bool `json:"quotaError"` + SessionsError bool `json:"sessionsError"` + UsageError bool `json:"usageError"` } type store struct { @@ -157,7 +160,7 @@ type store struct { } func newStore() *store { - s := &store{state: state{Version: version.Current(), Meters: []meter{}, Credits: []credit{}, Sessions: []session{}}, previous: map[string]int64{}, samples: map[string][]sample{}, changed: make(chan struct{})} + s := &store{state: state{StatusLineFields: statusline.Fields(), Version: version.Current(), Meters: []meter{}, Credits: []credit{}, Sessions: []session{}}, previous: map[string]int64{}, samples: map[string][]sample{}, changed: make(chan struct{})} s.publish() return s } @@ -331,7 +334,8 @@ func (s *store) live(l codex.LiveUsageSnapshot, err error, now time.Time) { } s.state.Sessions = append(s.state.Sessions, session{ ID: row.ID, Name: codex.SanitizeSessionContext(row.Name), Directory: row.WorkingDirectory, Tokens: row.TotalTokens, Agents: row.AgentCount, - Status: sessionStatus(row), ContextKind: contextKind(row.Context.Kind), Text: text, + StatusLine: statusline.Values(statusline.Data{Model: row.ModelSettings.Model, Effort: row.ModelSettings.ReasoningEffort, Speed: row.ModelSettings.ServiceTier, Directory: row.WorkingDirectory, Name: row.Name, ID: row.ID, State: sessionStatus(row), Source: row.Context.Source, Tokens: row.TotalTokens, Agents: row.AgentCount}), + Status: sessionStatus(row), ContextKind: contextKind(row.Context.Kind), Text: text, Command: row.Context.CommandDetails.Command, Source: row.Context.Source, Activity: row.LastActivity, WorkingCommand: activity.Command, CommandStatus: activity.CommandStatus, ApprovalContext: approvalContext, diff --git a/intro-post.md b/intro-post.md index af12242..5d7d406 100644 --- a/intro-post.md +++ b/intro-post.md @@ -79,6 +79,8 @@ Take useful text with you: **[ (C)OPY ]** sits at the bottom-right of split, wid Long full-screen replies scroll with Up/Down, Page Up/Down or the mouse wheel; press Escape first if the composer is focused to restore keyboard scrolling. Copy includes offscreen text from the entire sanitized, bounded reply available to Codexometer—not an unlimited conversation transcript—and uses the same terminal clipboard support as Benchmark Copy. Copied text follows your terminal and operating system's normal clipboard retention. +Queued work moving too quickly to read? In terminal full detail, use **Alt+Left / Alt+Right** or **Previous / Next / Live** to browse up to ten recent completed prompt/reply excerpts per session. The answer you're reading stays pinned while newer turns run; scrolling and Copy follow that answer, while approvals and composing always target the live session. Shared app-server history can recover quick completions and reload recent replies after restarting; older setups retain only replies observed during the current launch. No conversation history is written to disk by Codexometer. + If an approval request is too long for inline controls, a highlighted **APPROVAL — OPEN DETAIL →** warning takes you to the complete request. Dismiss another session or enlarge the window and the buttons reappear automatically when there is enough room to review the request safely. File-change approvals now have a proper diff review too: file paths, old/new line @@ -89,10 +91,14 @@ Missing or unsafe patches and broader directory grants stay in Codex—no blind For **Approve Once**, press its number, release it, then press it again to confirm on terminals with detected **Kitty keyboard-protocol key-release support**. `C` remains an alternative; session-wide and persistent grants still require `C` or clicking confirmation. Confirmations expire after five seconds. Windows Terminal 1.24 inside Ubuntu/WSL and Apple's built-in Terminal.app use the `1` → `C` fallback. [Windows Terminal Preview 1.25](https://github.com/microsoft/terminal/releases/tag/v1.25.622.0) introduced the protocol, and the separate [kitty app](https://sw.kovidgoyal.net/kitty/) supports it on macOS. Follow the shortcut shown on the button—this terminal capability is optional, not a requirement for using Codexometer or approving commands. -Subtle animated dots in inline and full-screen detail indicate observed work, disappearing when the session is no longer observed working or needs attention. “Text sent ...” and “Decision sent ...” confirmations linger briefly so fast updates do not swallow the acknowledgement. Local activity signals remain best-effort, not proof that a model is still executing. +Subtle animated dots in inline and full-screen detail indicate observed work, disappearing when the session is no longer observed working or needs attention. When FOLLOW-UPS is visible, the dots sit above the queue to associate them with the active task rather than the waiting messages. “Text sent ...” and “Decision sent ...” confirmations linger briefly so fast updates do not swallow the acknowledgement. Local activity signals remain best-effort, not proof that a model is still executing. On a shared-server session ready for input, the selected full-width detail row also offers a follow-up composer when there is room for the complete context. Click or press Enter to write, Enter to send, and Escape to leave the editor. Smaller panels and structured questions keep full-screen detail as the fallback. +**Live command menus:** type `/` in the composer for a vertical popup with help that shrinks as you type: Up/Down selects, Tab completes, Enter opens options and Escape dismisses. Terminal and writable web mode discover model/reasoning options, advertised speed tiers, collaboration modes and allowed permission profiles. Session-setting changes require confirmation and an idle shared-server session. Skills, apps, MCP tools, hooks and experimental flags have browse-only catalogues; this is not every Codex CLI slash command. Unsupported catalogues stay hidden, and slash text is never silently sent as a prompt (`//` escapes a literal leading slash). + +**Your own status line:** full session detail has a configurable footer for model/reasoning, speed, tokens and directory. `/statusline` offers checkboxes, ordering and a live preview; choose multiple fields or hide it entirely. The settings belong to Codexometer and persist as display preferences. Web's footer configuration also works in read-only mode. + **Schedule the next move:** experimental full-detail scheduling (Ctrl+S, also in writable web mode) offers a delay, a calendar date/time, or confirmed quota recovery. A clickable **TRIGGER SET** pill and session label open the scheduler. While queued, the saved prompt and timing replace the ordinary composer: edit with Ctrl+S, delete with Ctrl+D, or request Send now with Ctrl+N and explicitly confirm with Ctrl+Y. The form is fully keyboard navigable; buttons work too. Requests follow session settings and wait for an eligible idle session with quota available. Keep Codexometer running: the queue is memory-only, closing the process cancels it, and uncertain sends are never automatically retried. It does not redeem reset credits. ## Quota estimates, resets, and session attention diff --git a/web/src/SessionActions.svelte b/web/src/SessionActions.svelte index ec1249a..a64c1d6 100644 --- a/web/src/SessionActions.svelte +++ b/web/src/SessionActions.svelte @@ -1,5 +1,6 @@ + +
        + + {#if notice && !open}

        {notice}

        {/if} + {#if open} +
        + {#if !suggesting} +

        {menu?.title || '/ COMMANDS'}

        +

        {menu?.help || 'Loading live options…'}

        + {/if} + {#if notice}

        {notice}

        {/if} + {#if busy}

        Loading / sending…

        {/if} + {#if selected} +

        {selected.label}

        +
        {selected.help || 'No additional help supplied by Codex.'}
        + {#if selected.action}

        + TARGET // {session}. Changes affect subsequent turns of this + session, not global defaults. Automatic quota thresholds may later + supersede model settings. +

        + + {#if confirmation && now >= expires}

        + Confirmation expired. Reopen the option. +

        {/if} + {/if} + + {:else} +
        + {#each choices as o, index (o.id)}{/each} +
        + {#if !busy && !choices.length}

        + No available matching commands. +

        {/if} + {#if slash && menu?.path === ''}

        + ↑/↓ select · Tab complete · Enter options · Escape dismiss +

        {/if} + {/if} + {#if !suggesting} + + + {/if} +
        + {/if} +
        + + diff --git a/web/src/Sessions.svelte b/web/src/Sessions.svelte index f7101bc..5dd973c 100644 --- a/web/src/Sessions.svelte +++ b/web/src/Sessions.svelte @@ -10,6 +10,7 @@ } from './preferences.svelte'; import Graph from './Graph.svelte'; import SessionActions from './SessionActions.svelte'; + import StatusLine from './StatusLine.svelte'; import SessionCopy from './SessionCopy.svelte'; import FileDiff from './FileDiff.svelte'; let { params = {} }: { params?: { id?: string } } = $props(); @@ -379,6 +380,7 @@ session={selected} active />{/key}{/if} + {#key selected.id}{/key} {:else}

        This session is no longer in the current observation. Return to sessions + import { live, type Session } from './state.svelte'; + import { + preferences, + savePreferences, + statusLineUI, + } from './preferences.svelte'; + let { session, stale = false }: { session: Session; stale?: boolean } = + $props(); + let draft = $state([]), + opened = $state(false); + let fields = $derived(live.data?.statusLineFields || []); + const normalize = (ids: string[] | null) => + [ + ...new Set(ids ?? fields.filter((f) => f.default).map((f) => f.id)), + ].filter((id) => fields.some((f) => f.id === id)); + const text = (ids: string[] | null) => + normalize(ids) + .map((id) => + id === 'run-state' && + (stale || !live.connected || live.data?.sessionsError) + ? 'STALE' + : session.statusLine?.[id], + ) + .filter(Boolean) + .join(' · '); + let entries = $derived([ + ...draft.map((id) => fields.find((f) => f.id === id)!).filter(Boolean), + ...fields.filter((f) => !draft.includes(f.id)), + ]); + $effect(() => { + if (statusLineUI.open && !opened) { + draft = normalize(preferences.statusLine); + opened = true; + } else if (!statusLineUI.open) opened = false; + }); + function toggle(id: string, checked: boolean) { + draft = checked ? [...draft, id] : draft.filter((value) => value !== id); + } + function move(id: string, delta: number) { + const at = draft.indexOf(id), + to = at + delta; + if (at < 0 || to < 0 || to >= draft.length) return; + const next = [...draft]; + [next[at], next[to]] = [next[to], next[at]]; + draft = next; + } + + +{#if statusLineUI.open} +

        +

        STATUS LINE // CODEXOMETER

        +

        + Choose multiple fields and their order. These display preferences do not + change Codex CLI configuration. Missing values are omitted. +

        +
        + {#each entries as field (field.id)} +
        + + {#if draft.includes(field.id)} + + + {/if} +
        + {/each} +
        +

        + Preview // {text(draft) || '—'} +

        + + +
        +{/if} +
        +

        + {text(preferences.statusLine)} +

        + +
        + + diff --git a/web/src/preferences.svelte.ts b/web/src/preferences.svelte.ts index f01944d..7790998 100644 --- a/web/src/preferences.svelte.ts +++ b/web/src/preferences.svelte.ts @@ -9,6 +9,7 @@ export const quotaViews = [ ]; const key = 'codexometer.web.preferences.v1'; interface Preferences { + statusLine: string[] | null; tab: 'quota' | 'sessions' | 'usage'; view: string; selected: string; @@ -16,6 +17,7 @@ interface Preferences { layouts: { id: string; level: number }[]; } const defaults: Preferences = { + statusLine: null, tab: 'quota', view: 'bars', selected: '', @@ -27,6 +29,16 @@ function read(): Preferences { const value = JSON.parse(localStorage.getItem(key) || 'null'); if (!value || typeof value !== 'object') return defaults; return { + statusLine: Array.isArray(value.statusLine) + ? ([ + ...new Set( + value.statusLine.filter( + (id: unknown): id is string => + typeof id === 'string' && id.length < 64, + ), + ), + ].slice(0, 32) as string[]) + : null, tab: ['quota', 'sessions', 'usage'].includes(value.tab) ? value.tab : 'quota', @@ -55,6 +67,7 @@ function read(): Preferences { } } export const preferences = $state(read()); +export const statusLineUI = $state({ open: false }); export function savePreferences() { const value = JSON.stringify(preferences); try { diff --git a/web/src/state.svelte.ts b/web/src/state.svelte.ts index 11100f4..51e97c8 100644 --- a/web/src/state.svelte.ts +++ b/web/src/state.svelte.ts @@ -4,6 +4,7 @@ export interface Sample { tokens: number; } export interface Session { + statusLine?: Record; fileChanges?: FileDiffLine[]; name?: string; id: string; @@ -104,6 +105,12 @@ export interface Threshold { remaining?: number; } export interface Snapshot { + statusLineFields?: { + id: string; + label: string; + help: string; + default: boolean; + }[]; triggers?: { session: string; status: string }[]; profiles?: { session: string; @@ -150,7 +157,7 @@ let sendControl: | ((action: string, body: unknown, signal?: AbortSignal) => Promise) | undefined; export async function controlRequest( - action: 'offer' | 'prepare' | 'commit' | 'schedules', + action: 'offer' | 'prepare' | 'commit' | 'schedules' | 'commands', body: unknown, signal?: AbortSignal, ): Promise { diff --git a/web/tests/browser.spec.ts b/web/tests/browser.spec.ts index 2aea945..1c682d2 100644 --- a/web/tests/browser.spec.ts +++ b/web/tests/browser.spec.ts @@ -1243,6 +1243,368 @@ test('changed requests, stale data and navigation invalidate browser confirmatio expect(calls.filter((c) => c.action === 'commit')).toHaveLength(0); }); +test('slash commands discover help and require a separate confirmation', async ({ + page, + pairingURL, +}) => { + const { calls } = await mockActions(page, 'prompt'); + const commands: string[] = []; + let revision = 'r0'; + let rejectCommit = false; + await page.route('**/api/control/commands', async (route) => { + const body = route.request().postDataJSON(); + commands.push(body.command.mode); + let result: object; + if (body.command.mode === 'prepare') { + // Applying a setting invalidates the old catalogue, as the real server does. + if (body.command.revision !== revision) { + await route.fulfill({ + status: 409, + json: { error: 'Command unavailable or unconfirmed' }, + }); + return; + } + result = { + confirmation: 'command-token', + expires: new Date(Date.now() + 30000).toISOString(), + }; + } else if (body.command.mode === 'commit') { + if (rejectCommit) { + await route.fulfill({ + status: 409, + json: { error: 'Command unavailable or unconfirmed' }, + }); + return; + } + revision = 'r1'; + result = { + message: 'Change requested. Codex will apply it to subsequent turns.', + }; + } else + result = + body.command.path === '' + ? { + title: '/ COMMANDS', + help: 'Live options', + path: '', + revision, + choices: [ + { + id: 'm', + label: '/model', + help: 'Choose a model. '.repeat(50), + next: 'model/test', + }, + ], + } + : { + title: '/model/test', + help: 'Server model help', + path: 'model/test', + revision, + choices: [ + { + id: 'effort', + label: 'Medium', + help: 'Server effort description', + action: true, + }, + { + id: 'high', + label: 'High', + help: 'Higher effort', + action: true, + }, + ], + }; + await route.fulfill({ json: result }); + }); + await page.goto(pairingURL); + await page.evaluate(() => { + location.hash = '/sessions/parent'; + }); + const text = page + .locator('.detail-workspace') + .getByRole('textbox', { name: 'Follow-up message' }); + await text.fill('/'); + const panel = page.getByRole('region', { name: 'Session slash commands' }); + await expect( + panel.getByRole('button', { name: '/model →', exact: true }), + ).toBeVisible(); + const popup = panel.locator('.command-popup'); + await expect(popup).toBeVisible(); + const initialPopup = await popup.boundingBox(); + const initialComposer = await text.boundingBox(); + const initialOptions = await popup.locator('.command-options button').all(); + expect(initialOptions).toHaveLength(2); + const firstOption = await initialOptions[0].boundingBox(); + const secondOption = await initialOptions[1].boundingBox(); + expect(secondOption!.y).toBeGreaterThanOrEqual( + firstOption!.y + firstOption!.height, + ); + const helpStyle = await initialOptions[0] + .locator('.command-help') + .evaluate((node) => { + const style = getComputedStyle(node); + const command = node.parentElement!.querySelector('.command-name')!; + return { + whiteSpace: style.whiteSpace, + overflow: style.overflow, + ellipsis: style.textOverflow, + clipped: node.scrollWidth > node.clientWidth, + helpBackground: style.backgroundColor, + commandBackground: getComputedStyle(command).backgroundColor, + sameLine: + Math.abs( + node.getBoundingClientRect().y - command.getBoundingClientRect().y, + ) < 5, + }; + }); + expect(helpStyle).toMatchObject({ + whiteSpace: 'nowrap', + overflow: 'hidden', + ellipsis: 'ellipsis', + clipped: true, + sameLine: true, + }); + expect(helpStyle.helpBackground).not.toBe(helpStyle.commandBackground); + await panel.getByRole('button', { name: '/ COMMANDS', exact: true }).click(); + await expect( + panel.getByRole('button', { name: '/model →', exact: true }), + ).toHaveCount(0); + await panel.getByRole('button', { name: '/ COMMANDS', exact: true }).click(); + await expect( + panel.getByRole('button', { name: '/model →', exact: true }), + ).toBeVisible(); + await text.fill('/mo'); + await expect(popup.locator('.command-options button')).toHaveCount(1); + const filteredPopup = await popup.boundingBox(); + const filteredComposer = await text.boundingBox(); + expect(filteredPopup!.height).toBeLessThan(initialPopup!.height); + expect(filteredComposer!.y).toBe(initialComposer!.y); + await text.press('Tab'); + await expect(text).toHaveValue('/model'); + await text.press('Escape'); + await expect( + panel.getByRole('button', { name: '/model →', exact: true }), + ).toHaveCount(0); + await expect(text).toBeFocused(); + await text.fill('/mod'); + await expect( + panel.getByRole('button', { name: '/model →', exact: true }), + ).toBeVisible(); + await text.press('Enter'); + await panel.getByRole('button', { name: 'Medium', exact: true }).click(); + await expect(panel).toContainText('Server effort description'); + await expect( + panel.getByRole('button', { name: 'CONFIRM CHANGE' }), + ).toBeEnabled(); + expect(commands.filter((c) => c === 'commit')).toHaveLength(0); + await panel.getByRole('button', { name: 'CONFIRM CHANGE' }).click(); + await expect(panel).toContainText('Change requested.'); + expect(commands.filter((c) => c === 'commit')).toHaveLength(1); + expect(calls.filter((c) => c.action === 'commit')).toHaveLength(0); + await expect(text).toHaveValue(''); + await expect(text).toBeFocused(); + await expect( + panel.getByRole('button', { name: 'BACK', exact: true }), + ).toHaveCount(0); + await expect( + panel.getByRole('button', { name: 'Medium', exact: true }), + ).toHaveCount(0); + await expect( + panel.getByRole('button', { name: 'REFRESH OPTIONS', exact: true }), + ).toHaveCount(0); + // Repeating the same command must reopen a fresh catalogue, not the old revision. + await text.fill('/mod'); + await expect( + panel.getByRole('button', { name: '/model →', exact: true }), + ).toBeEnabled(); + await text.press('Enter'); + await panel.getByRole('button', { name: 'High', exact: true }).click(); + await panel.getByRole('button', { name: 'CONFIRM CHANGE' }).click(); + await expect(text).toHaveValue(''); + await expect( + panel.getByRole('button', { name: 'High', exact: true }), + ).toHaveCount(0); + await expect(panel).not.toContainText('Change unconfirmed'); + expect(commands.filter((c) => c === 'commit')).toHaveLength(2); + // A failed confirmation retains the user's draft and the error context. + rejectCommit = true; + await text.fill('/mod'); + await expect( + panel.getByRole('button', { name: '/model →', exact: true }), + ).toBeEnabled(); + await text.press('Enter'); + await panel.getByRole('button', { name: 'Medium', exact: true }).click(); + await panel.getByRole('button', { name: 'CONFIRM CHANGE' }).click(); + await expect(panel).toContainText('Change unconfirmed'); + await expect(text).toHaveValue('/mod'); + await expect( + panel.getByRole('button', { name: 'BACK', exact: true }), + ).toBeVisible(); + await expect( + panel.getByRole('button', { name: 'CONFIRM CHANGE' }), + ).toBeDisabled(); +}); + +test('unavailable slash commands close without losing the draft and recover with a fresh catalogue', async ({ + page, + pairingURL, +}) => { + const { snapshot } = await mockActions(page, 'prompt'); + let lists = 0; + const modes: string[] = []; + await page.route('**/api/control/commands', async (route) => { + const body = route.request().postDataJSON(); + modes.push(body.command.mode); + if (body.command.mode === 'list') lists++; + await route.fulfill({ + json: { + title: '/ COMMANDS', + help: 'Live options', + path: '', + revision: 'r' + lists, + choices: [ + { id: 'model', label: '/model', help: 'Model help', next: 'model' }, + ], + }, + }); + }); + await page.goto(pairingURL); + await page.evaluate(() => { + location.hash = '/sessions/parent'; + }); + const text = page + .locator('.detail-workspace') + .getByRole('textbox', { name: 'Follow-up message' }); + await text.fill('/mod'); + const panel = page.getByRole('region', { name: 'Session slash commands' }); + await expect( + panel.getByRole('button', { name: '/model →', exact: true }), + ).toBeVisible(); + const before = lists; + snapshot.sessionsError = true; + await page.evaluate( + (detail) => + window.dispatchEvent(new CustomEvent('test-snapshot', { detail })), + snapshot, + ); + await expect(page.locator('.command-popup')).toHaveCount(0); + await expect( + page.getByText( + 'Session controls temporarily unavailable. Check Codex for current state.', + ), + ).toBeVisible(); + expect(lists).toBe(before); + snapshot.sessionsError = false; + await page.evaluate( + (detail) => + window.dispatchEvent(new CustomEvent('test-snapshot', { detail })), + snapshot, + ); + await expect(text).toHaveValue('/mod'); + // Dismiss any newly mounted suggestions, then explicitly reopen commands. + await text.press('Escape'); + await expect(page.locator('.command-popup')).toHaveCount(0); + const recovered = lists; + await panel.getByRole('button', { name: '/ COMMANDS', exact: true }).click(); + await expect( + panel.getByRole('button', { name: '/model →', exact: true }), + ).toBeVisible(); + expect(lists).toBeGreaterThan(recovered); + await text.press('Escape'); + await expect(page.locator('.command-popup')).toHaveCount(0); + await expect(text).toHaveValue('/mod'); + expect(modes.every((mode) => mode === 'list')).toBe(true); +}); + +test('read-only status line supports multiple fields, ordering and persistence without control writes', async ({ + page, + pairingURL, +}) => { + await mockStream(page, { + version: 'test', + control: false, + meters: [], + credits: [], + creditCount: 0, + usage: null, + sessionsError: false, + sessionsAt: new Date().toISOString(), + statusLineFields: [ + { + id: 'model-with-reasoning', + label: 'Model and reasoning', + help: 'Observed model and effort', + default: true, + }, + { + id: 'used-tokens', + label: 'Tokens', + help: 'Observed tokens', + default: true, + }, + { + id: 'thread-id', + label: 'Session ID', + help: 'Full identifier', + default: false, + }, + ], + sessions: [ + { + id: 'one', + name: 'Test session', + directory: '/work', + tokens: 1200, + agents: 0, + status: 'TURN COMPLETE', + contextKind: 'LAST REPLY', + text: 'Done.', + command: '', + source: 'LOCAL', + activity: new Date().toISOString(), + samples: [], + statusLine: { + 'model-with-reasoning': 'model high', + 'used-tokens': '1.2K tokens', + 'thread-id': 'one', + }, + }, + ], + }); + const writes: string[] = []; + await page.route('**/api/control/**', (route) => { + writes.push(route.request().url()); + return route.abort(); + }); + await page.goto(pairingURL); + await page.evaluate(() => { + location.hash = '/sessions/one'; + }); + const line = page.getByLabel('Session status line', { exact: true }); + await expect(line).toHaveText('model high · 1.2K tokens'); + await page.getByRole('button', { name: 'Configure status line' }).click(); + const picker = page.getByRole('region', { name: 'Status line fields' }); + await picker.getByRole('checkbox', { name: /Model and reasoning/ }).uncheck(); + await picker.getByRole('checkbox', { name: /Session ID/ }).check(); + await picker.getByRole('button', { name: 'Move Session ID earlier' }).click(); + await expect(page.getByLabel('Status line preview')).toContainText( + 'one · 1.2K tokens', + ); + await expect(line).toHaveText('model high · 1.2K tokens'); + await picker.getByRole('button', { name: 'APPLY', exact: true }).click(); + await expect(line).toHaveText('one · 1.2K tokens'); + await page.reload(); + await expect(line).toHaveText('one · 1.2K tokens'); + await page.getByRole('button', { name: 'Configure status line' }).click(); + await picker.getByRole('checkbox', { name: /Tokens/ }).uncheck(); + await picker.getByRole('button', { name: 'CANCEL', exact: true }).click(); + await expect(line).toHaveText('one · 1.2K tokens'); + expect(writes).toHaveLength(0); +}); + test('quota review hides only follow-ups, cancels confirmation and preserves the draft', async ({ page, pairingURL,