paseo-smart-session 2.1.0 → 2.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.
- package/README.md +40 -11
- package/client/agents.ts +26 -4
- package/client/charts.tsx +277 -152
- package/client/chat.tsx +122 -0
- package/client/display.tsx +55 -33
- package/client/guard/index.tsx +14 -16
- package/client/guard/paused.tsx +125 -38
- package/client/guard/pill-icons.tsx +1 -1
- package/client/guard/pills.ts +16 -6
- package/client/guard/surfaces.ts +3 -2
- package/client/guard/timeline.tsx +116 -56
- package/client/kit.tsx +144 -0
- package/client/live.ts +49 -0
- package/client/motion.tsx +94 -0
- package/client/overview.tsx +373 -0
- package/client/pill-store.ts +77 -0
- package/client/pill.tsx +83 -260
- package/client/popover.tsx +113 -0
- package/client/settings.tsx +105 -0
- package/client/theme.ts +56 -0
- package/docs/media/smart-session-chat.png +0 -0
- package/docs/media/smart-session-flow.gif +0 -0
- package/docs/media/smart-session-guard.png +0 -0
- package/docs/media/smart-session-hero-light.png +0 -0
- package/docs/media/smart-session-hero.png +0 -0
- package/docs/media/smart-session-mobile.png +0 -0
- package/docs/media/smart-session-pill.png +0 -0
- package/docs/navigation-verification.md +6 -6
- package/docs/screenshots/display-settings-desktop.png +0 -0
- package/docs/screenshots/display-settings-mobile.png +0 -0
- package/docs/session-guard.md +3 -3
- package/index.client.tsx +47 -35
- package/index.server.ts +14 -5
- package/mcp.mjs +1 -1
- package/package.json +1 -1
- package/paseo-plugin.json +10 -0
- package/server/settings.ts +10 -0
- package/shared/blocks.ts +15 -0
- package/shared/chat.ts +174 -0
- package/shared/forecast.ts +233 -0
- package/shared/format.ts +47 -1
- package/shared/governor.ts +3 -0
- package/shared/smart-session.ts +16 -0
- package/client/navigation.tsx +0 -14
- package/client/surface.tsx +0 -659
- package/docs/screenshots/smart-compact-handoff.png +0 -0
package/README.md
CHANGED
|
@@ -5,9 +5,14 @@
|
|
|
5
5
|
[](https://paseo.sh)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
|
-
|
|
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.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
<picture>
|
|
11
|
+
<source media="(prefers-color-scheme: light)" srcset="docs/media/smart-session-hero-light.png">
|
|
12
|
+
<img alt="The Smart Session screen: the headline Session runs out around 8:55 PM, 44m before the 9:39 PM reset. Below it the session window at 77% with a chart of this window's usage and the projection crossing 100% before the reset, the earlier session windows of the last 14 days, the week and the Opus week, then each session's context with its compact line" src="docs/media/smart-session-hero.png">
|
|
13
|
+
</picture>
|
|
14
|
+
|
|
15
|
+
Screenshots use synthetic usage history and sessions.
|
|
11
16
|
|
|
12
17
|
## Install
|
|
13
18
|
|
|
@@ -19,6 +24,19 @@ On first load it registers its Claude Code hooks, touching only entries that poi
|
|
|
19
24
|
|
|
20
25
|
## Use
|
|
21
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.
|
|
28
|
+
|
|
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
|
+
- **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.
|
|
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
|
+
|
|
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.
|
|
35
|
+
|
|
36
|
+

|
|
37
|
+
|
|
38
|
+

|
|
39
|
+
|
|
22
40
|
Agents get these tools:
|
|
23
41
|
|
|
24
42
|
| Tool | What it does |
|
|
@@ -29,33 +47,45 @@ Agents get these tools:
|
|
|
29
47
|
| `defer_compaction` | Put off a compaction and record why |
|
|
30
48
|
| `budget_status` | Plan usage, burn rate and reset times, and whether Session Guard will pause this session |
|
|
31
49
|
|
|
32
|
-
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.
|
|
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.
|
|
33
51
|
|
|
34
|
-
|
|
52
|
+

|
|
35
53
|
|
|
36
54
|
## How Smart Compact works
|
|
37
55
|
|
|
38
|
-
As a session fills, hooks send context notices. At the compact
|
|
56
|
+
As a session fills, hooks send context notices. At the compact line, the agent is asked at the end of its turn whether it wants to compact. Nothing is forced: the agent can compact, defer with a reason, or keep working. A request is delivered only when the agent is idle and its state file is current; `request_compaction` refuses a missing or stale one.
|
|
39
57
|
|
|
40
58
|
After compaction, the agent chooses to stop (`continue_after_compaction: false`), continue with the default prompt to re-read its state, or continue with its own `continue_message`. A `/compact` typed by a person never gets a plugin continuation.
|
|
41
59
|
|
|
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
|
+
|
|
62
|
+
**Settings → Plugins → Smart Session → Smart Compact:**
|
|
63
|
+
|
|
42
64
|
| Setting | Default | |
|
|
43
65
|
| --- | --- | --- |
|
|
44
66
|
| Smart Compact | On | Context notices and agent-requested compaction |
|
|
45
|
-
| Large-window compact threshold | 30% | Context windows of 400k tokens or more |
|
|
46
|
-
| Small-window compact threshold | 85% | Smaller context windows |
|
|
47
67
|
| Enrol sessions automatically | On | Enrol a session once it writes task state |
|
|
48
|
-
|
|
|
49
|
-
|
|
|
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 |
|
|
50
72
|
|
|
51
73
|
## Session Guard
|
|
52
74
|
|
|
53
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**.
|
|
54
76
|
|
|
55
|
-
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 session gets a
|
|
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.
|
|
78
|
+
|
|
79
|
+

|
|
56
80
|
|
|
57
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.
|
|
58
82
|
|
|
83
|
+
On a phone, the pill popovers open as sheets and every screen fits one column.
|
|
84
|
+
|
|
85
|
+

|
|
86
|
+
|
|
87
|
+
**Settings → Plugins → Smart Session → Display** hides the sidebar row or the composer pills; neither turns a feature off.
|
|
88
|
+
|
|
59
89
|
## Data and access
|
|
60
90
|
|
|
61
91
|
Everything lives in `$PASEO_HOME/plugin-data/smart-session/`: monthly usage and context logs, compaction records, an hourly spend index by model, workspace and agent type, and one `state/<agentId>.md` per agent. Logs are append-only, and a damaged line is skipped rather than risking the rest.
|
|
@@ -79,7 +109,6 @@ paseo plugin reload smart-session # after changes
|
|
|
79
109
|
|
|
80
110
|
- [Defer](https://github.com/tomgrin10/paseo-defer) — Schedule messages for later delivery.
|
|
81
111
|
- [Follow-up](https://github.com/tomgrin10/paseo-follow-up) — Track workspace PRs, remaining tasks and completion.
|
|
82
|
-
- [Graphite](https://github.com/tomgrin10/paseo-graphite) — Graphite stacks and PR status.
|
|
83
112
|
- [Send to Paseo](https://github.com/tomgrin10/send-to-paseo) — Send GitHub and Graphite PRs to Paseo from Chrome.
|
|
84
113
|
- [Session Guard](https://github.com/tomgrin10/paseo-session-guard) — Pause at session limits and continue after reset.
|
|
85
114
|
- [Transcript Search](https://github.com/tomgrin10/paseo-transcript-search) — Search conversations across connected hosts.
|
package/client/agents.ts
CHANGED
|
@@ -19,12 +19,17 @@ export interface DirectoryAgent {
|
|
|
19
19
|
readonly workspaceId: string;
|
|
20
20
|
/** Closed or archived: still listed, but no longer a session anyone is working in. */
|
|
21
21
|
readonly closed: boolean;
|
|
22
|
+
/** Context occupancy as of the agent's last turn, from its snapshot; null until it has run one. */
|
|
23
|
+
readonly usedTokens: number | null;
|
|
24
|
+
readonly maxTokens: number | null;
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
export interface AgentDirectory {
|
|
25
28
|
current(): ReadonlyMap<string, DirectoryAgent>;
|
|
26
29
|
/** Called after every change; read `current()` for the new state. */
|
|
27
30
|
subscribe(listener: () => void): () => void;
|
|
31
|
+
/** Bumped on every change, for `useSyncExternalStore`. */
|
|
32
|
+
version(): number;
|
|
28
33
|
/** Releases the observation. Safe to call more than once. */
|
|
29
34
|
dispose(): Promise<void>;
|
|
30
35
|
}
|
|
@@ -35,6 +40,7 @@ interface ListedAgent {
|
|
|
35
40
|
readonly workspaceId?: string | null;
|
|
36
41
|
readonly status?: string;
|
|
37
42
|
readonly archivedAt?: string | null;
|
|
43
|
+
readonly lastUsage?: { readonly contextWindowUsedTokens?: number; readonly contextWindowMaxTokens?: number } | null;
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
interface AgentUpdate {
|
|
@@ -56,11 +62,18 @@ interface AgentListing {
|
|
|
56
62
|
|
|
57
63
|
function entryOf(agent: ListedAgent | undefined): [string, DirectoryAgent] | null {
|
|
58
64
|
if (typeof agent?.id !== "string" || typeof agent.workspaceId !== "string" || agent.workspaceId === "") return null;
|
|
59
|
-
|
|
65
|
+
const used = agent.lastUsage?.contextWindowUsedTokens;
|
|
66
|
+
const max = agent.lastUsage?.contextWindowMaxTokens;
|
|
67
|
+
return [agent.id, {
|
|
68
|
+
workspaceId: agent.workspaceId,
|
|
69
|
+
closed: agent.status === "closed" || !!agent.archivedAt,
|
|
70
|
+
usedTokens: typeof used === "number" && Number.isFinite(used) ? used : null,
|
|
71
|
+
maxTokens: typeof max === "number" && Number.isFinite(max) && max > 0 ? max : null,
|
|
72
|
+
}];
|
|
60
73
|
}
|
|
61
74
|
|
|
62
75
|
function same(a: DirectoryAgent | undefined, b: DirectoryAgent): boolean {
|
|
63
|
-
return a !== undefined && a.workspaceId === b.workspaceId && a.closed === b.closed;
|
|
76
|
+
return a !== undefined && a.workspaceId === b.workspaceId && a.closed === b.closed && a.usedTokens === b.usedTokens && a.maxTokens === b.maxTokens;
|
|
64
77
|
}
|
|
65
78
|
|
|
66
79
|
export function observeAgents(client: PluginClientContext): AgentDirectory {
|
|
@@ -71,13 +84,21 @@ export function observeAgents(client: PluginClientContext): AgentDirectory {
|
|
|
71
84
|
let unsubscribeOwnedObservation: (() => void) | null = null;
|
|
72
85
|
let releaseObservation: (() => Promise<void>) | null = null;
|
|
73
86
|
|
|
87
|
+
let version = 0;
|
|
74
88
|
const notify = () => {
|
|
89
|
+
version += 1;
|
|
75
90
|
for (const listener of [...listeners]) listener();
|
|
76
91
|
};
|
|
77
92
|
|
|
78
93
|
function upsert(agent: ListedAgent | undefined): boolean {
|
|
79
|
-
|
|
80
|
-
if (entry === null
|
|
94
|
+
let entry = entryOf(agent);
|
|
95
|
+
if (entry === null) return false;
|
|
96
|
+
// An update that carries no usage is not a reading of zero: keep the last one.
|
|
97
|
+
const known = agents.get(entry[0]);
|
|
98
|
+
if (entry[1].usedTokens === null && agent?.lastUsage == null && known !== undefined) {
|
|
99
|
+
entry = [entry[0], { ...entry[1], usedTokens: known.usedTokens, maxTokens: known.maxTokens }];
|
|
100
|
+
}
|
|
101
|
+
if (same(known, entry[1])) return false;
|
|
81
102
|
agents.set(entry[0], entry[1]);
|
|
82
103
|
return true;
|
|
83
104
|
}
|
|
@@ -155,6 +176,7 @@ export function observeAgents(client: PluginClientContext): AgentDirectory {
|
|
|
155
176
|
|
|
156
177
|
return {
|
|
157
178
|
current: () => agents,
|
|
179
|
+
version: () => version,
|
|
158
180
|
subscribe(listener) {
|
|
159
181
|
listeners.add(listener);
|
|
160
182
|
return () => {
|