paseo-smart-session 3.0.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +34 -23
  2. package/client/chat.tsx +2 -2
  3. package/client/commands.ts +62 -0
  4. package/client/display.tsx +9 -62
  5. package/client/guard/index.tsx +11 -42
  6. package/client/guard/paused.tsx +91 -120
  7. package/client/guard/status.ts +113 -0
  8. package/client/guard/timeline.tsx +61 -56
  9. package/client/overview.tsx +59 -52
  10. package/client/pill-store.ts +8 -2
  11. package/client/pill-view.ts +61 -0
  12. package/client/pill.tsx +50 -47
  13. package/client/popover.tsx +147 -31
  14. package/client/screens.ts +6 -0
  15. package/client/settings.tsx +223 -83
  16. package/docs/media/before-after.png +0 -0
  17. package/docs/media/smart-session-guard.png +0 -0
  18. package/docs/media/smart-session-mobile.png +0 -0
  19. package/docs/media/smart-session-pill.png +0 -0
  20. package/docs/media/smart-session-settings.png +0 -0
  21. package/docs/navigation-verification.md +4 -4
  22. package/docs/session-guard.md +27 -16
  23. package/index.client.tsx +19 -34
  24. package/index.server.ts +6 -2
  25. package/mcp.mjs +32 -3
  26. package/package.json +1 -1
  27. package/paseo-plugin.json +1 -0
  28. package/server/guard/engine.ts +37 -5
  29. package/server/guard/index.ts +27 -14
  30. package/server/guard/zerosub.ts +82 -0
  31. package/shared/guard/contracts.ts +2 -0
  32. package/shared/guard/presentation.ts +71 -11
  33. package/shared/guard/timeline.ts +3 -3
  34. package/shared/guard/timing.ts +37 -0
  35. package/shared/guard/zerosub.ts +276 -0
  36. package/shared/smart-session.ts +16 -1
  37. package/client/guard/pill-icons.tsx +0 -26
  38. package/client/guard/pills.ts +0 -132
  39. package/client/guard/settings.tsx +0 -114
  40. package/client/guard/surfaces.ts +0 -3
  41. package/docs/screenshots/display-settings-desktop.png +0 -0
  42. package/docs/screenshots/display-settings-mobile.png +0 -0
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![Paseo](https://img.shields.io/badge/Paseo-%E2%89%A5%200.14.0-8A63D2?style=for-the-badge)](https://paseo.sh)
6
6
  [![License](https://img.shields.io/github/license/tomgrin10/paseo-smart-session?style=for-the-badge&color=2563eb)](LICENSE)
7
7
 
8
- Know when your plan runs out, and let agents manage their own long-running work. Smart Session records your Claude plan usage and turns it into a forecast ("Session runs out around 5:40 PM"), shows how full each session's context is, lets an agent save its task state and compact itself at a safe point, and with Session Guard pauses work at the session limit and continues it after the reset.
8
+ Know when your plan runs out, and let agents manage their own long-running work. Smart Session records your Claude plan usage and turns it into a forecast ("Session runs out around 5:40 PM"), shows how full each session's context is, lets an agent save its task state and compact itself at a safe point, and pauses work at the session limit to continue it after the reset.
9
9
 
10
10
  <picture>
11
11
  <source media="(prefers-color-scheme: light)" srcset="docs/media/smart-session-hero-light.png">
@@ -24,18 +24,18 @@ On first load it registers its Claude Code hooks, touching only entries that poi
24
24
 
25
25
  ## Use
26
26
 
27
- **Smart Session** in the sidebar opens the overview. Its headline says when the session window runs out at the current pace, or that it lasts until the reset.
27
+ **Smart Session** in the sidebar opens the overview. Its headline says when the session window runs out at the current pace, or that it lasts until the reset. While sessions are paused at the limit it leads with them instead ("2 sessions resume at 5:58 PM"), and lists them first with their actions.
28
28
 
29
29
  - **Plan limits.** Each window (the 5-hour session, the week, per-model weekly caps) shows how much is used, when it resets, and where it lands at the current pace: a faint extension of the meter, and a tick where an even pace would be. The session window adds a chart of its recorded usage and the projection to the reset, plus every session window of the last 14 days, so you can see how often you hit the limit.
30
30
  - **Session context.** Every open session with its share of the context window, the line where Smart Compact asks it to compact, how fast it is filling, and whether it is enrolled. Press a row to open the session.
31
- - **Smart Compact and Session Guard.** One switch each, what they will do, and links to their settings.
31
+ - **Smart Compact and Pause at the limit.** One switch each, what they will do next, and the sessions you changed from your default.
32
32
  - **Tokens, last 30 days.** Billable tokens, the cache hit rate and the subagent share, per day, by hour of the week, by workspace and by model, reconstructed from Claude Code's transcripts.
33
33
 
34
- The pill above each message box is a ring of that session's context, with a tick at its compact line. Press it to see the detail, when it last saved its task state and compacted, and to enrol the session or take it out.
34
+ Each session has one pill above its message box: a ring of its context, with a tick at its compact line. Press it to see the detail, when it last saved its task state and compacted, and to switch Smart Compact and Pause at the limit for that session. When the session is paused at the limit the same pill turns amber and says when it resumes, and it stays up even if you hide the pill.
35
35
 
36
36
  ![Pressing the context ring above the message box, then following it to the Smart Session screen and sweeping the window chart: recorded usage up to now, the projection at this pace, and the stretch after it runs out](docs/media/smart-session-flow.gif)
37
37
 
38
- ![A session's context popover: 34% of a 1M context, 336k tokens, compacts at 30%, past its compact line; task state saved 10m ago; last compaction 3h ago, 318k to 31k; the Smart Compact switch for this session](docs/media/smart-session-pill.png)
38
+ ![Two popovers of the one pill. Left: 34% of a 1M context, past its compact line, task state and last compaction, then the Smart Compact and Pause at the limit switches for this session. Right: the same pill while paused, reading Resumes 6:27 AM, with Cancel continuation and a link to all paused sessions](docs/media/smart-session-pill.png)
39
39
 
40
40
  Agents get these tools:
41
41
 
@@ -45,7 +45,7 @@ Agents get these tools:
45
45
  | `checkpoint` | Save durable task state for this session |
46
46
  | `request_compaction` | Queue `/compact`, and choose whether and how to continue afterwards |
47
47
  | `defer_compaction` | Put off a compaction and record why |
48
- | `budget_status` | Plan usage, burn rate and reset times, and whether Session Guard will pause this session |
48
+ | `budget_status` | Plan usage, burn rate and reset times, and whether this session pauses at the limit |
49
49
 
50
50
  The usual flow is `context_status → checkpoint → request_compaction`, then stop or re-read the state and continue. Saved state holds the goal, current step, decisions made and approaches that failed, so a resumed agent neither redoes work nor asks how to continue. In the chat each call is one line ("Saved task state · Cap retries at five", "Asked to compact · …"), and the messages Smart Session sends show as small cards with the full text one press away.
51
51
 
@@ -59,32 +59,43 @@ After compaction, the agent chooses to stop (`continue_after_compaction: false`)
59
59
 
60
60
  The forecast uses the last half hour for the session window and the window's own average for a week, so a burst of work doesn't read as "the week runs out tonight".
61
61
 
62
- **Settings → Plugins → Smart Session → Smart Compact:**
62
+ ## Pause at the limit
63
63
 
64
- | Setting | Default | |
65
- | --- | --- | --- |
66
- | Smart Compact | On | Context notices and agent-requested compaction |
67
- | Enrol sessions automatically | On | Enrol a session once it writes task state |
68
- | Context pill on every session | On | The ring above each message box |
69
- | Windows of 400k or more | 30% | Where large windows are asked to compact |
70
- | Smaller windows | 85% | Where smaller windows are asked to compact |
71
- | Register the hooks | On | Keep the context and compaction hooks installed in Claude Code |
64
+ When the provider's session window is spent, Smart Session pauses running sessions instead of letting them fail, and continues the same conversations once the window resets, even with the app closed. It is **off by default**: turn it on from the overview or in settings. It was the standalone [Session Guard](https://github.com/tomgrin10/paseo-session-guard) plugin, and its per-session choices and waiting pauses carry over.
72
65
 
73
- ## Session Guard
66
+ On Linux, Claude and Codex sessions pause in place together with their native subagents; elsewhere the turn is interrupted and one continuation is sent after the reset. A paused session's pill, its chat and its workspace say so: the chat gets a card with the time it resumes, **Resume now** and **Dismiss pause**, which folds into one line once the pause ends. For Claude, a reset is confirmed from whichever is fresher, Paseo's cached usage or Smart Session's own recorder.
74
67
 
75
- Session Guard pauses agents at the provider's session limit and continues the same conversations after the reset, even with the app closed. It is the [Session Guard](https://github.com/tomgrin10/paseo-session-guard) plugin folded into Smart Session, and it is **off by default**: turn it on from the overview or **Settings → Plugins → Smart Session → Session Guard**.
68
+ ![A pause card in the chat with the time it resumes, Resume now and Dismiss pause, and the Smart Session screen leading with 2 sessions resume at 6:27 AM above the list of paused sessions](docs/media/smart-session-guard.png)
76
69
 
77
- On Linux, Claude and Codex sessions pause in place together with their native subagents; elsewhere the turn is interrupted and one continuation is sent after the reset. Each paused session gets a card in its chat with the reset time and a countdown, **Resume now** and **Dismiss pause**; once the pause ends the card folds into one line. **Paused sessions** lists every pause by its reset time, and the session's pill and its workspace read **Paused**. For Claude, a reset is confirmed from whichever is fresher, Paseo's cached usage or Smart Session's own recorder.
70
+ If the standalone Session Guard plugin is still enabled, it keeps doing the pausing and Smart Session says so in one line, so a session is never paused twice. Disable or uninstall it to hand over. See the [guide](docs/session-guard.md) for timing and limits.
78
71
 
79
- ![Session Guard: a pause card in the chat with the reset time, Resume now and Dismiss pause, and the Paused sessions screen listing sessions waiting for the reset](docs/media/smart-session-guard.png)
72
+ On a phone, the pill opens as a sheet and every screen fits one column.
80
73
 
81
- While the standalone plugin is enabled, Smart Session leaves session protection to it, so a session is never paused twice. Disable or uninstall Session Guard to hand over; per-session choices and waiting pauses in `$PASEO_HOME/plugin-data/session-guard/` carry over. See the [Session Guard guide](docs/session-guard.md) for settings and limits.
74
+ ![Smart Session at phone width: the overview leading with paused sessions, the pill's sheet while paused, and the settings screen](docs/media/smart-session-mobile.png)
82
75
 
83
- On a phone, the pill popovers open as sheets and every screen fits one column.
76
+ ## Settings
84
77
 
85
- ![Smart Session at phone width: the overview, the context popover as a sheet, and Paused sessions](docs/media/smart-session-mobile.png)
78
+ Everything is on one screen, **Settings → Plugins → Smart Session**, in the order a session meets it. ⌘K **Smart Session: settings** opens it.
86
79
 
87
- **Settings → Plugins → Smart Session → Display** hides the sidebar row or the composer pills; neither turns a feature off.
80
+ | Section | Setting | Default | |
81
+ | --- | --- | --- | --- |
82
+ | Smart Compact | Smart Compact | On | Context notices and agent-requested compaction |
83
+ | | Enrol sessions automatically | On | Enrol a session once it writes task state |
84
+ | When to ask | Windows of 400k or more | 30% | Where large windows are asked to compact |
85
+ | | Smaller windows | 85% | Where smaller windows are asked to compact |
86
+ | Pause at the limit | Pause at the limit | Off | Pause sessions at the session limit |
87
+ | | Sessions | Every session | Or only the ones you turn on from their pill |
88
+ | | Continue after the reset | On | Off, you continue each one by hand |
89
+ | | Include native subagents | On | Freeze nested workers too (Linux) |
90
+ | | Label paused workspaces | On | An amber Paused label in the sidebar |
91
+ | Display | Sidebar row | On | |
92
+ | | Pill on every session | On | A paused session keeps its pill either way |
93
+ | Claude Code | Register the hooks | On | Keep the context and compaction hooks installed |
94
+ | Limit timing | Pause at, check interval, wait after reset, spacing, continuation message, providers, windows | 100%, 15s, 30s, 5s | Behind **Edit**; each field is checked on its own |
95
+
96
+ Hiding the row or the pill never turns a feature off.
97
+
98
+ ![The Smart Session settings screen: Smart Compact, When to ask, Pause at the limit, Display, Claude Code and Limit timing](docs/media/smart-session-settings.png)
88
99
 
89
100
  ## Data and access
90
101
 
package/client/chat.tsx CHANGED
@@ -52,7 +52,7 @@ function ToolRowView({ item, theme }: PluginTimelineItemProps<ToolRow>) {
52
52
  const CARD: Record<PromptCard["kind"], { icon: string; title: string }> = {
53
53
  compact: { icon: "FoldVertical", title: "Compaction the agent asked for" },
54
54
  continue: { icon: "CornerDownRight", title: "Continue after compaction" },
55
- "guard-continue": { icon: "ShieldCheck", title: "Continue after the reset" },
55
+ "guard-continue": { icon: "Play", title: "Continue after the reset" },
56
56
  "guard-resume": { icon: "Play", title: "Resume now" },
57
57
  };
58
58
 
@@ -71,7 +71,7 @@ function PromptCardView({ item, theme, layout }: PluginTimelineItemProps<PromptC
71
71
  const card = item.data;
72
72
  const [expanded, setExpanded] = useState(false);
73
73
  const look = CARD[card.kind];
74
- const sender = card.kind.startsWith("guard") ? "Session Guard" : "Smart Compact";
74
+ const sender = card.kind.startsWith("guard") ? "Smart Session" : "Smart Compact";
75
75
  return <View style={{ alignSelf: layout.compact ? "stretch" : "flex-end", width: "100%", maxWidth: 560, marginVertical: 4 }}>
76
76
  <View style={[{ borderRadius: 12, backgroundColor: c.surface1, overflow: "hidden" }, raised(c)]}>
77
77
  <View style={{ flexDirection: "row", alignItems: "center", gap: 8, paddingHorizontal: 14, paddingTop: 11, paddingBottom: 4 }}>
@@ -0,0 +1,62 @@
1
+ import type { PluginClientContext } from "@getpaseo/plugin/client";
2
+
3
+ type CommandItem = Parameters<PluginClientContext["addCommandCenterItem"]>[0];
4
+
5
+ import { displayPreferences, displayRpc } from "../shared/display.ts";
6
+ import { enrolmentState } from "../shared/governor.ts";
7
+ import { getSettings, setSettings } from "../shared/smart-session.ts";
8
+ import { refreshPills } from "./pill-store.ts";
9
+ import { OVERVIEW_SCREEN, SETTINGS_SCREEN } from "./screens.ts";
10
+
11
+ /**
12
+ * Smart Session in the Command Center: one name, four places. The old Session Guard
13
+ * and display items folded in here; their keywords stay so muscle memory still finds them.
14
+ */
15
+ export function commandItems(): CommandItem[] {
16
+ return [
17
+ {
18
+ id: "smart-session-open",
19
+ title: "Smart Session",
20
+ icon: "Gauge",
21
+ keywords: ["usage", "budget", "limit", "forecast", "context", "tokens", "compact", "smart session", "overview", "plan"],
22
+ context: "global",
23
+ onSelect({ openScreen }) { openScreen({ screenId: OVERVIEW_SCREEN }); },
24
+ },
25
+ {
26
+ id: "smart-session-settings",
27
+ title: "Smart Session: settings",
28
+ icon: "Settings",
29
+ keywords: ["settings", "smart compact", "threshold", "hooks", "pause at the limit", "protection", "quota", "limit", "guard", "session guard", "display", "sidebar", "pill"],
30
+ context: "global",
31
+ onSelect({ openSettings }) { openSettings(SETTINGS_SCREEN); },
32
+ },
33
+ {
34
+ id: "smart-session-paused",
35
+ title: "Smart Session: paused sessions",
36
+ icon: "Pause",
37
+ keywords: ["paused", "pause", "resume", "waiting", "reset", "quota", "limit", "subagents", "guard", "session guard"],
38
+ context: "global",
39
+ onSelect({ openScreen }) { openScreen({ screenId: OVERVIEW_SCREEN, params: { focus: "paused" } }); },
40
+ },
41
+ {
42
+ id: "smart-session-pill",
43
+ title: "Smart Session: show or hide the pill",
44
+ icon: "ToggleLeft",
45
+ keywords: ["pill", "compact", "smart", "context", "ring", "enrol", "composer", "display", "show", "hide"],
46
+ context: "global",
47
+ async onSelect({ rpc }) {
48
+ // The same one switch as settings: the pill flag, plus the display gate when showing.
49
+ const { settings } = await rpc(getSettings, {});
50
+ const display = await rpc(displayRpc.read, {}).catch(() => null);
51
+ const gate = display?.status === "ready" ? displayPreferences.schema.safeParse(display.values) : null;
52
+ const gateOn = gate?.success ? gate.data.enable_composer_button : true;
53
+ const next = !(settings.showPill && gateOn);
54
+ if (next && gate?.success && !gateOn && display?.status === "ready") {
55
+ await rpc(displayRpc.write, { values: { ...gate.data, enable_composer_button: true }, revision: display.revision });
56
+ }
57
+ await rpc(setSettings, { showPill: next });
58
+ await refreshPills(() => rpc(enrolmentState, {}));
59
+ },
60
+ },
61
+ ];
62
+ }
@@ -1,8 +1,6 @@
1
- import type { PluginClientContext, PluginSidebarItemProps, PluginSurfaceProps } from "@getpaseo/plugin/client";
2
- import { useSettings } from "@getpaseo/plugin/client";
3
- import { SettingsAction, SettingsCard, SettingsSection, SettingsSwitch, SidebarRow } from "@getpaseo/plugin/client/ui";
4
- import { useEffect } from "react";
5
- import { ActivityIndicator, ScrollView, Text, View } from "react-native";
1
+ import type { PluginClientContext, PluginSidebarItemProps } from "@getpaseo/plugin/client";
2
+ import { SidebarRow } from "@getpaseo/plugin/client/ui";
3
+ import { Text } from "react-native";
6
4
  import { displayPreferences, displayRpc } from "../shared/display";
7
5
  import { Chip, useNow } from "./kit";
8
6
  import { useLive, type Live } from "./live";
@@ -14,52 +12,12 @@ type Display = import("zod").infer<typeof displayPreferences.schema>;
14
12
  const listeners = new Set<(values: Display) => void>();
15
13
  export function subscribeDisplay(listener: (values: Display) => void) { listeners.add(listener); return () => { listeners.delete(listener); }; }
16
14
 
17
- /** Where Smart Session shows up. Hiding something never turns its feature off. */
18
- function DisplaySettings({ theme, layout }: PluginSurfaceProps) {
19
- const settings = useSettings(displayPreferences);
20
- const values = settings.status === "ready" ? settings.values : undefined;
21
- useEffect(() => {
22
- if (values !== undefined) for (const listener of listeners) listener(values);
23
- }, [values]);
24
-
25
- if (settings.status === "loading") return <ActivityIndicator accessibilityLabel="Loading display settings" color={theme.colors.foregroundMuted} style={{ padding: 24 }} />;
26
- if (settings.status !== "ready") return (
27
- <View style={{ gap: 12, padding: 16 }}>
28
- <Text style={{ color: theme.colors.statusDanger }}>{settings.error}</Text>
29
- <SettingsCard>
30
- <SettingsAction label="Try loading the saved settings again" actionLabel="Retry" onPress={() => void settings.reload()} />
31
- {settings.status === "invalid" && <SettingsAction label="Replace invalid display settings with defaults" actionLabel="Reset" disabled={settings.saving} onPress={() => void settings.reset()} />}
32
- </SettingsCard>
33
- {settings.saveError && <Text style={{ color: theme.colors.statusDanger }}>{settings.saveError}</Text>}
34
- </View>
35
- );
36
-
37
- return (
38
- <ScrollView style={{ flex: 1 }} contentContainerStyle={{ paddingVertical: 8, gap: 16 }}>
39
- <SettingsSection title="Where Smart Session appears" info="Hiding a row or a pill never turns its feature off. ⌘K Smart Session still opens the overview.">
40
- <SettingsCard>
41
- <SettingsSwitch
42
- label="Sidebar row"
43
- hint="Smart Session in the sidebar, with the session window's usage, or how many sessions are paused."
44
- value={settings.values.showSidebar}
45
- disabled={settings.saving}
46
- onValueChange={(showSidebar) => void settings.save({ ...settings.values, showSidebar }, settings.revision)}
47
- />
48
- <SettingsSwitch
49
- label="Composer pills"
50
- hint="The context ring and, while Session Guard runs, its shield, above each message box."
51
- value={settings.values.enable_composer_button}
52
- disabled={settings.saving}
53
- onValueChange={(enable_composer_button) => void settings.save({ ...settings.values, enable_composer_button }, settings.revision)}
54
- />
55
- </SettingsCard>
56
- {settings.saveError && <Text accessibilityRole="alert" style={{ color: theme.colors.statusDanger }}>{settings.saveError}</Text>}
57
- </SettingsSection>
58
- </ScrollView>
59
- );
15
+ /** Tells the pill and the sidebar about a display document read anywhere in this bundle. */
16
+ export function broadcastDisplay(values: Display) {
17
+ for (const listener of [...listeners]) listener(values);
60
18
  }
61
19
 
62
- /** The sidebar row, the Display settings screen, and the preference poll that drives both. */
20
+ /** The sidebar row, and the display preference poll that drives it and the pill. */
63
21
  export function contributeDisplay(client: PluginClientContext, live: Live) {
64
22
  function SmartSessionSidebarItem({ currentScreen, openScreen, theme }: PluginSidebarItemProps) {
65
23
  useLive(live);
@@ -73,7 +31,7 @@ export function contributeDisplay(client: PluginClientContext, live: Live) {
73
31
  onPress={() => openScreen({ screenId: OVERVIEW_SCREEN })}
74
32
  trailing={trailing === null ? undefined : trailing.tone === "muted"
75
33
  ? <Text accessibilityLabel={`Session window ${trailing.label} used`} numberOfLines={1} style={{ color: c.foregroundMuted, fontSize: 12, lineHeight: 16, ...tabular }}>{trailing.label}</Text>
76
- : <Chip theme={theme} label={trailing.label} color={toneColor(c, trailing.tone)} />}
34
+ : <Chip theme={theme} icon={trailing.label.endsWith("paused") ? "Pause" : undefined} label={trailing.label} color={toneColor(c, trailing.tone)} />}
77
35
  />
78
36
  );
79
37
  }
@@ -83,15 +41,6 @@ export function contributeDisplay(client: PluginClientContext, live: Live) {
83
41
  // Register synchronously, then reconcile the saved preference, so that an
84
42
  // already-open sidebar route can resolve its screen during that first render.
85
43
  sidebar.set(true);
86
- const removeSettings = client.addSettingsScreen({ id: "display", title: "Display", icon: "PanelLeft", Component: DisplaySettings });
87
- const removeCommand = client.addCommandCenterItem({
88
- id: "display-settings",
89
- title: "Smart Session: display settings",
90
- icon: "PanelLeft",
91
- keywords: ["smart session", "sidebar", "pill", "show", "hide", "display", "settings"],
92
- context: "global",
93
- onSelect({ openSettings }) { openSettings("display"); },
94
- });
95
44
  let disposed = false;
96
45
  let pending = false;
97
46
  let generation = 0;
@@ -105,7 +54,7 @@ export function contributeDisplay(client: PluginClientContext, live: Live) {
105
54
  const result = await client.rpc(displayRpc.read, {});
106
55
  if (!disposed && generation === currentGeneration && result.status === "ready") {
107
56
  const parsed = displayPreferences.schema.safeParse(result.values);
108
- if (parsed.success) { sidebar.set(parsed.data.showSidebar); for (const listener of listeners) listener(parsed.data); }
57
+ if (parsed.success) { sidebar.set(parsed.data.showSidebar); broadcastDisplay(parsed.data); }
109
58
  }
110
59
  } catch {
111
60
  // Retain the last valid preference through temporary disconnects.
@@ -118,7 +67,5 @@ export function contributeDisplay(client: PluginClientContext, live: Live) {
118
67
  clearInterval(timer);
119
68
  listeners.delete(update);
120
69
  sidebar.dispose();
121
- removeSettings();
122
- removeCommand();
123
70
  };
124
71
  }
@@ -1,48 +1,17 @@
1
1
  import type { PluginClientContext } from "@getpaseo/plugin/client";
2
- import { ScrollView } from "react-native";
3
2
 
4
3
  import { TimelineDataSchema } from "../../shared/guard/timeline.ts";
5
- import type { AgentDirectory } from "../agents.ts";
6
- import type { Live } from "../live.ts";
7
- import { PausedSessions } from "./paused.tsx";
8
- import { guardPillIcons } from "./pill-icons.tsx";
9
- import { contributePills } from "./pills.ts";
10
- import { GuardSettings } from "./settings.tsx";
11
- import { GUARD_SCREENS, GUARD_SETTINGS } from "./surfaces.ts";
12
4
  import { GuardTimelineItem } from "./timeline.tsx";
13
5
 
14
- /** Session Guard's client half: its settings, paused-sessions screen, timeline cards and pill. */
15
- export function contributeGuardClient(client: PluginClientContext, agents: AgentDirectory, live?: Live) {
16
- const openPaused = () => client.openScreen({ screenId: GUARD_SCREENS.paused });
17
- const openSettings = () => client.openSettings(GUARD_SETTINGS);
18
- const Settings = (props: Parameters<typeof GuardSettings>[0] extends infer P ? Omit<P, "openOverview"> : never) => (
19
- <ScrollView style={{ flex: 1 }} contentContainerStyle={{ paddingVertical: 8 }}>
20
- <GuardSettings {...props} openOverview={openPaused} />
21
- </ScrollView>
22
- );
23
-
24
- const removers = [
25
- client.addScreen({ id: GUARD_SCREENS.paused, title: "Paused sessions", Component: (props) => <PausedSessions {...props} openSettings={openSettings} /> }),
26
- client.addSettingsScreen({ id: GUARD_SETTINGS, title: "Session Guard", icon: "ShieldCheck", Component: Settings }),
27
- client.addCommandCenterItem({
28
- id: "session-guard-settings", title: "Session Guard: settings", icon: "ShieldCheck",
29
- keywords: ["quota", "limit", "reset", "pause", "resume", "protection", "session guard"], context: "global",
30
- onSelect({ openSettings: open }) { open(GUARD_SETTINGS); },
31
- }),
32
- client.addCommandCenterItem({
33
- id: "session-guard-paused", title: "Session Guard: paused sessions", icon: "Pause",
34
- keywords: ["quota", "waiting", "reset", "resume", "subagents", "session guard"], context: "global",
35
- onSelect({ openScreen }) { openScreen({ screenId: GUARD_SCREENS.paused }); },
36
- }),
37
- // The kind and version the standalone plugin wrote, so a pause it recorded and this
38
- // plugin later delivers renders the same card.
39
- client.addTimelineRenderer({
40
- kind: "quota-pause", version: 1, schema: TimelineDataSchema,
41
- Component: (props) => <GuardTimelineItem {...props} openOverview={openPaused} />,
42
- }),
43
- contributePills(client, agents, guardPillIcons, live),
44
- ];
45
- return () => {
46
- for (const remove of removers.splice(0)) remove();
47
- };
6
+ /**
7
+ * The pause card in the chat. The kind and version are the ones the standalone
8
+ * Session Guard plugin wrote, so a pause it recorded and this plugin later delivers
9
+ * renders the same card, and old chats keep rendering.
10
+ */
11
+ export function contributeGuardTimeline(client: PluginClientContext) {
12
+ const openPaused = () => client.openScreen({ screenId: "overview", params: { focus: "paused" } });
13
+ return client.addTimelineRenderer({
14
+ kind: "quota-pause", version: 1, schema: TimelineDataSchema,
15
+ Component: (props) => <GuardTimelineItem {...props} openOverview={openPaused} />,
16
+ });
48
17
  }