pi-sessions 0.7.2 → 0.9.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 +71 -37
- package/extensions/pi-sessions.ts +125 -0
- package/extensions/session-ask/agent.ts +34 -14
- package/extensions/{session-ask.ts → session-ask/install.ts} +73 -90
- package/extensions/session-ask/navigate.ts +3 -1
- package/extensions/session-ask/presenter.ts +23 -0
- package/extensions/session-ask/renderer.ts +63 -0
- package/extensions/session-ask/tool-contract.ts +27 -0
- package/extensions/session-ask/view-model.ts +18 -0
- package/extensions/session-auto-title/generate.ts +18 -18
- package/extensions/{session-auto-title.ts → session-auto-title/install.ts} +67 -46
- package/extensions/session-auto-title/model.ts +25 -17
- package/extensions/session-auto-title/retitle.ts +26 -12
- package/extensions/session-auto-title/wizard.ts +12 -6
- package/extensions/session-handoff/board-view-model.ts +181 -0
- package/extensions/session-handoff/board.ts +663 -0
- package/extensions/session-handoff/bootstrap.ts +174 -0
- package/extensions/session-handoff/extract.ts +101 -122
- package/extensions/session-handoff/install.ts +225 -0
- package/extensions/session-handoff/kickoff.ts +92 -0
- package/extensions/session-handoff/launch/backend.ts +16 -0
- package/extensions/session-handoff/launch/deferred.ts +20 -0
- package/extensions/session-handoff/launch/ghostty.ts +99 -0
- package/extensions/session-handoff/launch/resolution.ts +57 -0
- package/extensions/session-handoff/launch/shell.ts +7 -0
- package/extensions/session-handoff/launch/tmux.ts +31 -0
- package/extensions/session-handoff/launch-options.ts +61 -0
- package/extensions/session-handoff/launch-target.ts +91 -0
- package/extensions/session-handoff/metadata.ts +152 -63
- package/extensions/session-handoff/model.ts +58 -0
- package/extensions/session-handoff/query.ts +0 -1
- package/extensions/session-handoff/receipt.ts +96 -0
- package/extensions/session-handoff/review.ts +2 -28
- package/extensions/session-handoff/spawn.ts +76 -181
- package/extensions/session-handoff/strong-modal.ts +13 -0
- package/extensions/session-handoff/tool-contract.ts +12 -0
- package/extensions/session-handoff/tool-presenter.ts +35 -0
- package/extensions/session-handoff/tool-renderer.ts +66 -0
- package/extensions/session-handoff/tool-schema.ts +54 -0
- package/extensions/session-handoff/tool-view-model.ts +49 -0
- package/extensions/session-handoff/tool.ts +206 -0
- package/extensions/session-handoff/ui.ts +66 -0
- package/extensions/session-hooks/install.ts +75 -0
- package/extensions/{session-index.ts → session-index/install.ts} +9 -7
- package/extensions/session-messaging/broker/process.ts +21 -28
- package/extensions/session-messaging/install.ts +78 -0
- package/extensions/session-messaging/pi/cancel-session-presenter.ts +64 -0
- package/extensions/session-messaging/pi/incoming-message-presenter.ts +32 -0
- package/extensions/session-messaging/pi/incoming-message-view-model.ts +20 -0
- package/extensions/session-messaging/pi/incoming-runtime.ts +27 -23
- package/extensions/session-messaging/pi/message-contracts.ts +32 -0
- package/extensions/session-messaging/pi/renderer.ts +19 -12
- package/extensions/session-messaging/pi/send-message-presenter.ts +39 -0
- package/extensions/session-messaging/pi/send-message-view-model.ts +54 -0
- package/extensions/session-messaging/pi/service.ts +262 -81
- package/extensions/session-messaging/pi/tools.ts +184 -40
- package/extensions/session-search/extract.ts +60 -14
- package/extensions/session-search/hooks.ts +10 -13
- package/extensions/session-search/install.ts +373 -0
- package/extensions/session-search/renderer.ts +195 -0
- package/extensions/session-search/tool-contract.ts +51 -0
- package/extensions/shared/composition.ts +20 -0
- package/extensions/shared/errors.ts +3 -0
- package/extensions/shared/model-resolution.ts +73 -0
- package/extensions/shared/model-runtime.ts +33 -0
- package/extensions/shared/model.ts +7 -10
- package/extensions/shared/rendering/collapsible-text.ts +89 -0
- package/extensions/shared/rendering/expandable-content-layout.ts +84 -0
- package/extensions/shared/rendering/theme.ts +5 -0
- package/extensions/shared/session-broker/client.ts +30 -29
- package/extensions/shared/session-broker/protocol.ts +128 -10
- package/extensions/shared/session-index/common.ts +5 -5
- package/extensions/shared/session-index/lineage.ts +0 -3
- package/extensions/shared/session-index/schema.ts +0 -1
- package/extensions/shared/session-index/scoring.ts +1 -2
- package/extensions/shared/session-index/search.ts +11 -4
- package/extensions/shared/session-index/store.ts +4 -9
- package/extensions/shared/settings.ts +80 -38
- package/extensions/shared/text.ts +5 -0
- package/extensions/shared/thinking-levels.ts +22 -0
- package/extensions/shared/tmux.ts +205 -0
- package/extensions/subagents/cancel.ts +111 -0
- package/extensions/subagents/classify.ts +52 -0
- package/extensions/subagents/install.ts +344 -0
- package/extensions/subagents/launch-target.ts +75 -0
- package/extensions/subagents/ledger.ts +217 -0
- package/extensions/subagents/reconcile.ts +386 -0
- package/extensions/subagents/report-message-presenter.ts +36 -0
- package/extensions/subagents/report-message-renderer.ts +21 -0
- package/extensions/subagents/report-message-view-model.ts +29 -0
- package/extensions/subagents/report.ts +159 -0
- package/extensions/subagents/roster.ts +262 -0
- package/extensions/subagents/wake.ts +173 -0
- package/images/handoff-board-subagents.png +0 -0
- package/images/handoff-board-user-sessions.png +0 -0
- package/images/handoff-subagent-report.png +0 -0
- package/images/session-handoff-tool.png +0 -0
- package/package.json +10 -16
- package/extensions/session-handoff.ts +0 -741
- package/extensions/session-hooks.ts +0 -75
- package/extensions/session-messaging/pi/message-view.ts +0 -131
- package/extensions/session-messaging.ts +0 -30
- package/extensions/session-search.ts +0 -419
- package/extensions/shared/session-broker/active.ts +0 -26
|
@@ -30,7 +30,6 @@ const SESSION_LINEAGE_QUERY_ROW_SCHEMA = Type.Object({
|
|
|
30
30
|
parentSessionId: NULLABLE_STRING_SCHEMA,
|
|
31
31
|
sessionOrigin: Type.Union([SESSION_ORIGIN_SCHEMA, Type.Null()]),
|
|
32
32
|
handoffGoal: NULLABLE_STRING_SCHEMA,
|
|
33
|
-
handoffNextTask: NULLABLE_STRING_SCHEMA,
|
|
34
33
|
});
|
|
35
34
|
|
|
36
35
|
const SESSION_RELATED_QUERY_ROW_SCHEMA = Type.Intersect([
|
|
@@ -66,7 +65,6 @@ function sessionLineageColumns(alias?: string): string {
|
|
|
66
65
|
`${prefix}parent_session_id as parentSessionId`,
|
|
67
66
|
`${prefix}session_origin as sessionOrigin`,
|
|
68
67
|
`${prefix}handoff_goal as handoffGoal`,
|
|
69
|
-
`${prefix}handoff_next_task as handoffNextTask`,
|
|
70
68
|
].join(",\n ");
|
|
71
69
|
}
|
|
72
70
|
|
|
@@ -85,7 +83,6 @@ function buildSessionLineageRow(
|
|
|
85
83
|
parentSessionId: row.parentSessionId ?? undefined,
|
|
86
84
|
sessionOrigin: row.sessionOrigin ?? undefined,
|
|
87
85
|
handoffGoal: row.handoffGoal ?? undefined,
|
|
88
|
-
handoffNextTask: row.handoffNextTask ?? undefined,
|
|
89
86
|
};
|
|
90
87
|
}
|
|
91
88
|
|
|
@@ -6,8 +6,7 @@ const RECENCY_HALF_LIFE_DAYS = 21;
|
|
|
6
6
|
|
|
7
7
|
const TEXT_SOURCE_WEIGHTS = new Map<string, number>([
|
|
8
8
|
["session_name", 360],
|
|
9
|
-
["
|
|
10
|
-
["handoff_goal", 260],
|
|
9
|
+
["handoff_goal", 300],
|
|
11
10
|
["user_text", 180],
|
|
12
11
|
["assistant_text", 150],
|
|
13
12
|
["bash_command", 140],
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
type SearchSessionsParams,
|
|
20
20
|
type SearchSort,
|
|
21
21
|
type SessionIndexDatabase,
|
|
22
|
+
type SessionKind,
|
|
22
23
|
type SessionLineageRelation,
|
|
23
24
|
sanitizeFilterValues,
|
|
24
25
|
tokenizeSearchText,
|
|
@@ -51,7 +52,6 @@ const SESSION_LIST_ROW_SCHEMA = Type.Object({
|
|
|
51
52
|
firstUserPrompt: NULLABLE_STRING_SCHEMA,
|
|
52
53
|
sessionOrigin: Type.Union([SESSION_ORIGIN_SCHEMA, Type.Null()]),
|
|
53
54
|
handoffGoal: NULLABLE_STRING_SCHEMA,
|
|
54
|
-
handoffNextTask: NULLABLE_STRING_SCHEMA,
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
type SessionListRow = Static<typeof SESSION_LIST_ROW_SCHEMA>;
|
|
@@ -88,6 +88,7 @@ interface SearchWhereFilters {
|
|
|
88
88
|
repo: string | undefined;
|
|
89
89
|
touched: string[];
|
|
90
90
|
changed: string[];
|
|
91
|
+
kind: SessionKind | undefined;
|
|
91
92
|
excludeSessionIds: string[];
|
|
92
93
|
includeSessionIds: string[] | undefined;
|
|
93
94
|
}
|
|
@@ -202,6 +203,7 @@ function buildSearchFilters(params: SearchSessionsParams): SearchFilters {
|
|
|
202
203
|
repo: params.repo?.trim(),
|
|
203
204
|
touched: sanitizeFilterValues(params.touched),
|
|
204
205
|
changed: sanitizeFilterValues(params.changed),
|
|
206
|
+
kind: params.kind,
|
|
205
207
|
limit: normalizeResultLimit(params.limit),
|
|
206
208
|
sort: normalizeSort(params.sort),
|
|
207
209
|
query: params.query?.trim() || undefined,
|
|
@@ -258,8 +260,7 @@ function getFilteredSessionCandidates(
|
|
|
258
260
|
s.parent_session_id as parentSessionId,
|
|
259
261
|
s.first_user_prompt as firstUserPrompt,
|
|
260
262
|
s.session_origin as sessionOrigin,
|
|
261
|
-
s.handoff_goal as handoffGoal
|
|
262
|
-
s.handoff_next_task as handoffNextTask
|
|
263
|
+
s.handoff_goal as handoffGoal
|
|
263
264
|
FROM sessions s
|
|
264
265
|
${where.sql}
|
|
265
266
|
ORDER BY s.modified_ts ${orderDirection}
|
|
@@ -445,6 +446,7 @@ function createSessionIdWhereFilters(sessionIds: string[] | undefined): SearchWh
|
|
|
445
446
|
repo: undefined,
|
|
446
447
|
touched: [],
|
|
447
448
|
changed: [],
|
|
449
|
+
kind: undefined,
|
|
448
450
|
excludeSessionIds: [],
|
|
449
451
|
includeSessionIds: sessionIds === undefined ? undefined : sanitizeFilterValues(sessionIds),
|
|
450
452
|
};
|
|
@@ -657,7 +659,6 @@ function buildSearchResult(
|
|
|
657
659
|
firstUserPrompt: row.firstUserPrompt ?? undefined,
|
|
658
660
|
sessionOrigin: row.sessionOrigin ?? undefined,
|
|
659
661
|
handoffGoal: row.handoffGoal ?? undefined,
|
|
660
|
-
handoffNextTask: row.handoffNextTask ?? undefined,
|
|
661
662
|
relation: relationBySessionId.get(row.sessionId),
|
|
662
663
|
snippet: "",
|
|
663
664
|
evidence: [...fileEvidence],
|
|
@@ -759,6 +760,12 @@ function buildSessionWhereClause(alias: string, filters: SearchWhereFilters): Sq
|
|
|
759
760
|
args.push(filters.cwd, filters.cwdLike ?? `${escapeLikePrefix(filters.cwd)}%`);
|
|
760
761
|
}
|
|
761
762
|
|
|
763
|
+
if (filters.kind === "subagent") {
|
|
764
|
+
conditions.push(`${alias}.session_origin = 'subagent'`);
|
|
765
|
+
} else if (filters.kind === "user") {
|
|
766
|
+
conditions.push(`(${alias}.session_origin IS NULL OR ${alias}.session_origin <> 'subagent')`);
|
|
767
|
+
}
|
|
768
|
+
|
|
762
769
|
if (filters.excludeSessionIds.length > 0) {
|
|
763
770
|
conditions.push(
|
|
764
771
|
`${alias}.session_id NOT IN (${filters.excludeSessionIds.map(() => "?").join(", ")})`,
|
|
@@ -27,7 +27,6 @@ const SESSION_ROW_QUERY_SCHEMA = Type.Object({
|
|
|
27
27
|
parentSessionId: NULLABLE_STRING_SCHEMA,
|
|
28
28
|
sessionOrigin: Type.Union([SESSION_ORIGIN_SCHEMA, Type.Null()]),
|
|
29
29
|
handoffGoal: NULLABLE_STRING_SCHEMA,
|
|
30
|
-
handoffNextTask: NULLABLE_STRING_SCHEMA,
|
|
31
30
|
indexedFileSize: Type.Union([Type.Number(), Type.Null()]),
|
|
32
31
|
indexedFileMtimeMs: Type.Union([Type.Number(), Type.Null()]),
|
|
33
32
|
indexedFileAnchor: NULLABLE_STRING_SCHEMA,
|
|
@@ -49,7 +48,6 @@ function sessionRowBindings(row: SessionRow, indexSource: string) {
|
|
|
49
48
|
row.parentSessionId ?? null,
|
|
50
49
|
row.sessionOrigin ?? null,
|
|
51
50
|
row.handoffGoal ?? null,
|
|
52
|
-
row.handoffNextTask ?? null,
|
|
53
51
|
row.indexedFileSize ?? null,
|
|
54
52
|
row.indexedFileMtimeMs ?? null,
|
|
55
53
|
row.indexedFileAnchor ?? null,
|
|
@@ -70,10 +68,10 @@ export function insertSession(
|
|
|
70
68
|
session_id, session_path, session_name, first_user_prompt, cwd, repo_roots_json,
|
|
71
69
|
created_ts, modified_ts, message_count, entry_count,
|
|
72
70
|
parent_session_path, parent_session_id, session_origin,
|
|
73
|
-
handoff_goal,
|
|
71
|
+
handoff_goal,
|
|
74
72
|
indexed_file_size, indexed_file_mtime_ms, indexed_file_anchor,
|
|
75
73
|
index_version, indexed_at_ts, index_source
|
|
76
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
74
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
77
75
|
`,
|
|
78
76
|
).run(...sessionRowBindings(row, indexSource));
|
|
79
77
|
syncSessionRepoRoots(db, row);
|
|
@@ -90,10 +88,10 @@ export function upsertSession(
|
|
|
90
88
|
session_id, session_path, session_name, first_user_prompt, cwd, repo_roots_json,
|
|
91
89
|
created_ts, modified_ts, message_count, entry_count,
|
|
92
90
|
parent_session_path, parent_session_id, session_origin,
|
|
93
|
-
handoff_goal,
|
|
91
|
+
handoff_goal,
|
|
94
92
|
indexed_file_size, indexed_file_mtime_ms, indexed_file_anchor,
|
|
95
93
|
index_version, indexed_at_ts, index_source
|
|
96
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
94
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
97
95
|
ON CONFLICT(session_id) DO UPDATE SET
|
|
98
96
|
session_path = excluded.session_path,
|
|
99
97
|
session_name = excluded.session_name,
|
|
@@ -108,7 +106,6 @@ export function upsertSession(
|
|
|
108
106
|
parent_session_id = excluded.parent_session_id,
|
|
109
107
|
session_origin = excluded.session_origin,
|
|
110
108
|
handoff_goal = excluded.handoff_goal,
|
|
111
|
-
handoff_next_task = excluded.handoff_next_task,
|
|
112
109
|
indexed_file_size = excluded.indexed_file_size,
|
|
113
110
|
indexed_file_mtime_ms = excluded.indexed_file_mtime_ms,
|
|
114
111
|
indexed_file_anchor = excluded.indexed_file_anchor,
|
|
@@ -142,7 +139,6 @@ export function getSessionRowByPath(
|
|
|
142
139
|
parent_session_id as parentSessionId,
|
|
143
140
|
session_origin as sessionOrigin,
|
|
144
141
|
handoff_goal as handoffGoal,
|
|
145
|
-
handoff_next_task as handoffNextTask,
|
|
146
142
|
indexed_file_size as indexedFileSize,
|
|
147
143
|
indexed_file_mtime_ms as indexedFileMtimeMs,
|
|
148
144
|
indexed_file_anchor as indexedFileAnchor
|
|
@@ -179,7 +175,6 @@ function buildSessionRow(row: Static<typeof SESSION_ROW_QUERY_SCHEMA>): SessionR
|
|
|
179
175
|
parentSessionId: row.parentSessionId ?? undefined,
|
|
180
176
|
sessionOrigin: row.sessionOrigin ?? undefined,
|
|
181
177
|
handoffGoal: row.handoffGoal ?? undefined,
|
|
182
|
-
handoffNextTask: row.handoffNextTask ?? undefined,
|
|
183
178
|
indexedFileSize: row.indexedFileSize ?? undefined,
|
|
184
179
|
indexedFileMtimeMs: row.indexedFileMtimeMs ?? undefined,
|
|
185
180
|
indexedFileAnchor: row.indexedFileAnchor ?? undefined,
|
|
@@ -4,14 +4,39 @@ import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
|
4
4
|
import { SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import type { KeyId } from "@earendil-works/pi-tui";
|
|
6
6
|
import { type Static, Type } from "typebox";
|
|
7
|
+
import { isThinkingLevel } from "./thinking-levels.ts";
|
|
7
8
|
import { parseTypeBoxValue } from "./typebox.ts";
|
|
8
9
|
|
|
9
10
|
export const DEFAULT_AUTO_TITLE_REFRESH_TURNS = 4;
|
|
11
|
+
export const DEFAULT_AUTO_TITLE_TIMEOUT_SECONDS = 15;
|
|
10
12
|
export const DEFAULT_AUTO_TITLE_PROMPT = `Name this coding session (under 80 chars). Be specific to what is being discussed. Your exact output will be displayed to the user, so make sure that it contains ONLY the title itself and nothing else.`;
|
|
11
13
|
const SESSION_FILE_SETTINGS_SCHEMA = Type.Object({
|
|
14
|
+
messaging: Type.Optional(
|
|
15
|
+
Type.Object({
|
|
16
|
+
enable: Type.Optional(Type.Boolean()),
|
|
17
|
+
}),
|
|
18
|
+
),
|
|
19
|
+
subagents: Type.Optional(
|
|
20
|
+
Type.Object({
|
|
21
|
+
enable: Type.Optional(Type.Boolean()),
|
|
22
|
+
maxDepth: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
23
|
+
}),
|
|
24
|
+
),
|
|
12
25
|
handoff: Type.Optional(
|
|
13
26
|
Type.Object({
|
|
27
|
+
enable: Type.Optional(Type.Boolean()),
|
|
14
28
|
pickerShortcut: Type.Optional(Type.String()),
|
|
29
|
+
persistRuns: Type.Optional(Type.Boolean()),
|
|
30
|
+
deferred: Type.Optional(
|
|
31
|
+
Type.Object({
|
|
32
|
+
copyToClipboard: Type.Optional(Type.Boolean()),
|
|
33
|
+
}),
|
|
34
|
+
),
|
|
35
|
+
}),
|
|
36
|
+
),
|
|
37
|
+
search: Type.Optional(
|
|
38
|
+
Type.Object({
|
|
39
|
+
enable: Type.Optional(Type.Boolean()),
|
|
15
40
|
}),
|
|
16
41
|
),
|
|
17
42
|
index: Type.Optional(
|
|
@@ -21,7 +46,9 @@ const SESSION_FILE_SETTINGS_SCHEMA = Type.Object({
|
|
|
21
46
|
),
|
|
22
47
|
autoTitle: Type.Optional(
|
|
23
48
|
Type.Object({
|
|
49
|
+
enable: Type.Optional(Type.Boolean()),
|
|
24
50
|
refreshTurns: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
51
|
+
timeoutSecs: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
25
52
|
model: Type.Optional(Type.String()),
|
|
26
53
|
thinkingLevel: Type.Optional(Type.String()),
|
|
27
54
|
prompt: Type.Optional(Type.String()),
|
|
@@ -29,34 +56,30 @@ const SESSION_FILE_SETTINGS_SCHEMA = Type.Object({
|
|
|
29
56
|
),
|
|
30
57
|
ask: Type.Optional(
|
|
31
58
|
Type.Object({
|
|
59
|
+
enable: Type.Optional(Type.Boolean()),
|
|
32
60
|
model: Type.Optional(Type.String()),
|
|
33
61
|
thinkingLevel: Type.Optional(Type.String()),
|
|
34
62
|
persistRuns: Type.Optional(Type.Boolean()),
|
|
35
63
|
}),
|
|
36
64
|
),
|
|
65
|
+
hooks: Type.Optional(
|
|
66
|
+
Type.Object({
|
|
67
|
+
enable: Type.Optional(Type.Boolean()),
|
|
68
|
+
}),
|
|
69
|
+
),
|
|
37
70
|
});
|
|
38
71
|
const ROOT_SETTINGS_SCHEMA = Type.Object({
|
|
39
72
|
sessions: Type.Optional(SESSION_FILE_SETTINGS_SCHEMA),
|
|
40
73
|
});
|
|
41
74
|
|
|
42
|
-
export class ModelReference {
|
|
43
|
-
constructor(
|
|
44
|
-
readonly provider: string,
|
|
45
|
-
readonly modelId: string,
|
|
46
|
-
) {}
|
|
47
|
-
|
|
48
|
-
toString(): string {
|
|
49
|
-
return `${this.provider}/${this.modelId}`;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
75
|
export interface AgentModelSettings {
|
|
54
|
-
model?:
|
|
76
|
+
model?: string | undefined;
|
|
55
77
|
thinkingLevel?: ThinkingLevel | undefined;
|
|
56
78
|
}
|
|
57
79
|
|
|
58
80
|
export interface AutoTitleSettings extends AgentModelSettings {
|
|
59
81
|
refreshTurns: number;
|
|
82
|
+
timeoutMs: number;
|
|
60
83
|
prompt: string;
|
|
61
84
|
}
|
|
62
85
|
|
|
@@ -64,9 +87,27 @@ export interface AskSettings extends AgentModelSettings {
|
|
|
64
87
|
persistRuns: boolean;
|
|
65
88
|
}
|
|
66
89
|
|
|
90
|
+
export interface FeatureToggles {
|
|
91
|
+
messaging: boolean;
|
|
92
|
+
subagents: boolean;
|
|
93
|
+
handoff: boolean;
|
|
94
|
+
search: boolean;
|
|
95
|
+
ask: boolean;
|
|
96
|
+
autoTitle: boolean;
|
|
97
|
+
hooks: boolean;
|
|
98
|
+
}
|
|
99
|
+
|
|
67
100
|
export interface SessionSettings {
|
|
101
|
+
features: FeatureToggles;
|
|
102
|
+
subagents: {
|
|
103
|
+
maxDepth: number;
|
|
104
|
+
};
|
|
68
105
|
handoff: {
|
|
69
106
|
pickerShortcut: KeyId;
|
|
107
|
+
persistRuns: boolean;
|
|
108
|
+
deferred: {
|
|
109
|
+
copyToClipboard: boolean;
|
|
110
|
+
};
|
|
70
111
|
};
|
|
71
112
|
index: {
|
|
72
113
|
path: string;
|
|
@@ -89,6 +130,10 @@ export function getDefaultSessionAskRunsDir(): string {
|
|
|
89
130
|
return path.join(getDefaultIndexDir(), "session-ask");
|
|
90
131
|
}
|
|
91
132
|
|
|
133
|
+
export function getDefaultHandoffRunsDir(): string {
|
|
134
|
+
return path.join(getDefaultIndexDir(), "session-handoff");
|
|
135
|
+
}
|
|
136
|
+
|
|
92
137
|
function expandHome(rawPath: string): string {
|
|
93
138
|
if (rawPath === "~") {
|
|
94
139
|
return os.homedir();
|
|
@@ -124,33 +169,9 @@ function normalizePickerShortcut(value: string | undefined): KeyId {
|
|
|
124
169
|
return (trimmed ? trimmed : "alt+o") as KeyId;
|
|
125
170
|
}
|
|
126
171
|
|
|
127
|
-
export function parseModelReference(value: string | undefined): ModelReference | undefined {
|
|
128
|
-
const trimmed = value?.trim();
|
|
129
|
-
if (!trimmed) {
|
|
130
|
-
return undefined;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const slashIndex = trimmed.indexOf("/");
|
|
134
|
-
if (slashIndex <= 0 || slashIndex === trimmed.length - 1) {
|
|
135
|
-
return undefined;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return new ModelReference(trimmed.slice(0, slashIndex), trimmed.slice(slashIndex + 1));
|
|
139
|
-
}
|
|
140
|
-
|
|
141
172
|
function parseThinkingLevel(value: string | undefined): ThinkingLevel | undefined {
|
|
142
173
|
const trimmed = value?.trim();
|
|
143
|
-
|
|
144
|
-
case "off":
|
|
145
|
-
case "minimal":
|
|
146
|
-
case "low":
|
|
147
|
-
case "medium":
|
|
148
|
-
case "high":
|
|
149
|
-
case "xhigh":
|
|
150
|
-
return trimmed;
|
|
151
|
-
default:
|
|
152
|
-
return undefined;
|
|
153
|
-
}
|
|
174
|
+
return isThinkingLevel(trimmed) ? trimmed : undefined;
|
|
154
175
|
}
|
|
155
176
|
|
|
156
177
|
function resolveAgentModelSettings(
|
|
@@ -161,7 +182,7 @@ function resolveAgentModelSettings(
|
|
|
161
182
|
}
|
|
162
183
|
| undefined,
|
|
163
184
|
): AgentModelSettings {
|
|
164
|
-
const model =
|
|
185
|
+
const model = value?.model?.trim() || undefined;
|
|
165
186
|
const thinkingLevel = parseThinkingLevel(value?.thinkingLevel);
|
|
166
187
|
return {
|
|
167
188
|
...(model ? { model } : {}),
|
|
@@ -180,12 +201,32 @@ function loadSessionFileSettings(): SessionFileSettings {
|
|
|
180
201
|
return parsed.sessions ?? {};
|
|
181
202
|
}
|
|
182
203
|
|
|
204
|
+
function resolveFeatureToggles(value: SessionFileSettings): FeatureToggles {
|
|
205
|
+
return {
|
|
206
|
+
messaging: value.messaging?.enable ?? true,
|
|
207
|
+
subagents: value.subagents?.enable ?? true,
|
|
208
|
+
handoff: value.handoff?.enable ?? true,
|
|
209
|
+
search: value.search?.enable ?? true,
|
|
210
|
+
ask: value.ask?.enable ?? true,
|
|
211
|
+
autoTitle: value.autoTitle?.enable ?? true,
|
|
212
|
+
hooks: value.hooks?.enable ?? true,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
183
216
|
function resolveSessionSettings(fileSettings: SessionFileSettings): SessionSettings {
|
|
184
217
|
const indexDir = normalizeIndexDir(fileSettings.index?.dir);
|
|
185
218
|
|
|
186
219
|
return {
|
|
220
|
+
features: resolveFeatureToggles(fileSettings),
|
|
221
|
+
subagents: {
|
|
222
|
+
maxDepth: fileSettings.subagents?.maxDepth ?? 2,
|
|
223
|
+
},
|
|
187
224
|
handoff: {
|
|
188
225
|
pickerShortcut: normalizePickerShortcut(fileSettings.handoff?.pickerShortcut),
|
|
226
|
+
persistRuns: fileSettings.handoff?.persistRuns ?? false,
|
|
227
|
+
deferred: {
|
|
228
|
+
copyToClipboard: fileSettings.handoff?.deferred?.copyToClipboard ?? true,
|
|
229
|
+
},
|
|
189
230
|
},
|
|
190
231
|
index: {
|
|
191
232
|
path: path.join(indexDir, "index.sqlite"),
|
|
@@ -193,6 +234,7 @@ function resolveSessionSettings(fileSettings: SessionFileSettings): SessionSetti
|
|
|
193
234
|
autoTitle: {
|
|
194
235
|
...resolveAgentModelSettings(fileSettings.autoTitle),
|
|
195
236
|
refreshTurns: fileSettings.autoTitle?.refreshTurns ?? DEFAULT_AUTO_TITLE_REFRESH_TURNS,
|
|
237
|
+
timeoutMs: (fileSettings.autoTitle?.timeoutSecs ?? DEFAULT_AUTO_TITLE_TIMEOUT_SECONDS) * 1000,
|
|
196
238
|
prompt: normalizeAutoTitlePrompt(fileSettings.autoTitle?.prompt),
|
|
197
239
|
},
|
|
198
240
|
ask: {
|
|
@@ -18,6 +18,11 @@ export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
18
18
|
return typeof value === "object" && value !== null;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export function normalizeOptionalText(value: string | undefined): string | undefined {
|
|
22
|
+
const normalized = value?.trim();
|
|
23
|
+
return normalized || undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
21
26
|
export function truncateInline(value: string, maxChars: number): string {
|
|
22
27
|
const cleaned = value.replace(/\s+/g, " ").trim();
|
|
23
28
|
if (cleaned.length <= maxChars) {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
|
+
|
|
3
|
+
// Pi exports the ThinkingLevel type but no runtime list of its members
|
|
4
|
+
// (getSupportedThinkingLevels requires a concrete model). This object is the
|
|
5
|
+
// one local runtime definition; Record makes upstream additions a type error.
|
|
6
|
+
const THINKING_LEVELS_BY_NAME = {
|
|
7
|
+
off: true,
|
|
8
|
+
minimal: true,
|
|
9
|
+
low: true,
|
|
10
|
+
medium: true,
|
|
11
|
+
high: true,
|
|
12
|
+
xhigh: true,
|
|
13
|
+
max: true,
|
|
14
|
+
} satisfies Record<ThinkingLevel, true>;
|
|
15
|
+
|
|
16
|
+
export const THINKING_LEVELS = Object.freeze(
|
|
17
|
+
Object.keys(THINKING_LEVELS_BY_NAME) as ThinkingLevel[],
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
export function isThinkingLevel(value: string | undefined): value is ThinkingLevel {
|
|
21
|
+
return value !== undefined && Object.hasOwn(THINKING_LEVELS_BY_NAME, value);
|
|
22
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type { ExecOptions, ExecResult } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
const TMUX_COMMAND = "tmux";
|
|
4
|
+
const TMUX_TIMEOUT_MS = 15_000;
|
|
5
|
+
const SESSION_ID_OPTION = "@pi_session_id";
|
|
6
|
+
const WINDOW_FORMAT = "#{window_id}\t#{window_name}\t#{@pi_session_id}";
|
|
7
|
+
|
|
8
|
+
export interface TmuxWindow {
|
|
9
|
+
windowId: string;
|
|
10
|
+
name: string;
|
|
11
|
+
piSessionId: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CreateTmuxWindowOptions {
|
|
15
|
+
tmuxSession: string;
|
|
16
|
+
name: string;
|
|
17
|
+
cwd: string;
|
|
18
|
+
command: string;
|
|
19
|
+
piSessionId: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface TmuxExecutor {
|
|
23
|
+
exec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isInsideTmux(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
27
|
+
return Boolean(env.TMUX);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function tmuxSessionName(parentSessionId: string): string {
|
|
31
|
+
const hex = parentSessionId.replaceAll("-", "").match(/^[0-9a-fA-F]+/)?.[0] ?? "";
|
|
32
|
+
if (hex.length < 8) {
|
|
33
|
+
throw new Error(`Cannot derive tmux session name from session id: ${parentSessionId}`);
|
|
34
|
+
}
|
|
35
|
+
return `pi-${hex.slice(0, 12).toLowerCase()}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function isTmuxInstalled(executor: TmuxExecutor, cwd: string): Promise<boolean> {
|
|
39
|
+
const result = await executor.exec(TMUX_COMMAND, ["-V"], { cwd, timeout: TMUX_TIMEOUT_MS });
|
|
40
|
+
return result.code === 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function listTmuxWindows(
|
|
44
|
+
executor: TmuxExecutor,
|
|
45
|
+
tmuxSession: string,
|
|
46
|
+
): Promise<TmuxWindow[]> {
|
|
47
|
+
const result = await executor.exec(
|
|
48
|
+
TMUX_COMMAND,
|
|
49
|
+
["list-windows", "-t", tmuxSession, "-F", WINDOW_FORMAT],
|
|
50
|
+
{ timeout: TMUX_TIMEOUT_MS },
|
|
51
|
+
);
|
|
52
|
+
if (result.code !== 0) {
|
|
53
|
+
if (isMissingSession(result)) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
throw tmuxError("list windows", result);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return result.stdout
|
|
60
|
+
.split("\n")
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
.map(parseWindow)
|
|
63
|
+
.filter((window): window is TmuxWindow => window !== undefined);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function hasAttachedTmuxClients(
|
|
67
|
+
executor: TmuxExecutor,
|
|
68
|
+
tmuxSession: string,
|
|
69
|
+
): Promise<boolean> {
|
|
70
|
+
const result = await executor.exec(
|
|
71
|
+
TMUX_COMMAND,
|
|
72
|
+
["list-clients", "-t", tmuxSession, "-F", "#{client_name}"],
|
|
73
|
+
{ timeout: TMUX_TIMEOUT_MS },
|
|
74
|
+
);
|
|
75
|
+
if (result.code !== 0) {
|
|
76
|
+
if (isMissingSession(result)) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
throw tmuxError("list attached clients", result);
|
|
80
|
+
}
|
|
81
|
+
return result.stdout.split("\n").some((line) => line.trim().length > 0);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function createTmuxWindow(
|
|
85
|
+
executor: TmuxExecutor,
|
|
86
|
+
options: CreateTmuxWindowOptions,
|
|
87
|
+
): Promise<TmuxWindow> {
|
|
88
|
+
const existing = (await listTmuxWindows(executor, options.tmuxSession)).find(
|
|
89
|
+
(window) => window.piSessionId === options.piSessionId,
|
|
90
|
+
);
|
|
91
|
+
if (existing) {
|
|
92
|
+
return existing;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const sessionExists = await hasTmuxSession(executor, options.tmuxSession);
|
|
96
|
+
const args = sessionExists
|
|
97
|
+
? [
|
|
98
|
+
"new-window",
|
|
99
|
+
"-d",
|
|
100
|
+
"-P",
|
|
101
|
+
"-F",
|
|
102
|
+
"#{window_id}",
|
|
103
|
+
"-t",
|
|
104
|
+
options.tmuxSession,
|
|
105
|
+
"-n",
|
|
106
|
+
options.name,
|
|
107
|
+
"-c",
|
|
108
|
+
options.cwd,
|
|
109
|
+
options.command,
|
|
110
|
+
]
|
|
111
|
+
: [
|
|
112
|
+
"new-session",
|
|
113
|
+
"-d",
|
|
114
|
+
"-P",
|
|
115
|
+
"-F",
|
|
116
|
+
"#{window_id}",
|
|
117
|
+
"-s",
|
|
118
|
+
options.tmuxSession,
|
|
119
|
+
"-n",
|
|
120
|
+
options.name,
|
|
121
|
+
"-c",
|
|
122
|
+
options.cwd,
|
|
123
|
+
options.command,
|
|
124
|
+
];
|
|
125
|
+
const created = await executor.exec(TMUX_COMMAND, args, {
|
|
126
|
+
cwd: options.cwd,
|
|
127
|
+
timeout: TMUX_TIMEOUT_MS,
|
|
128
|
+
});
|
|
129
|
+
if (created.code !== 0) {
|
|
130
|
+
throw tmuxError("create window", created);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const windowId = created.stdout.trim();
|
|
134
|
+
if (!windowId) {
|
|
135
|
+
throw new Error("tmux created a window without returning its id.");
|
|
136
|
+
}
|
|
137
|
+
const stamped = await executor.exec(
|
|
138
|
+
TMUX_COMMAND,
|
|
139
|
+
["set-option", "-w", "-t", windowId, SESSION_ID_OPTION, options.piSessionId],
|
|
140
|
+
{ timeout: TMUX_TIMEOUT_MS },
|
|
141
|
+
);
|
|
142
|
+
if (stamped.code !== 0) {
|
|
143
|
+
await executor.exec(TMUX_COMMAND, ["kill-window", "-t", windowId], {
|
|
144
|
+
timeout: TMUX_TIMEOUT_MS,
|
|
145
|
+
});
|
|
146
|
+
throw tmuxError("stamp window", stamped);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { windowId, name: options.name, piSessionId: options.piSessionId };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function killTmuxWindow(
|
|
153
|
+
executor: TmuxExecutor,
|
|
154
|
+
tmuxSession: string,
|
|
155
|
+
piSessionId: string,
|
|
156
|
+
): Promise<boolean> {
|
|
157
|
+
const windows = await listTmuxWindows(executor, tmuxSession);
|
|
158
|
+
for (const window of windows.filter((candidate) => candidate.piSessionId === piSessionId)) {
|
|
159
|
+
await executor.exec(TMUX_COMMAND, ["kill-window", "-t", window.windowId], {
|
|
160
|
+
timeout: TMUX_TIMEOUT_MS,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return !(await listTmuxWindows(executor, tmuxSession)).some(
|
|
164
|
+
(window) => window.piSessionId === piSessionId,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function killTmuxSession(
|
|
169
|
+
executor: TmuxExecutor,
|
|
170
|
+
tmuxSession: string,
|
|
171
|
+
): Promise<boolean> {
|
|
172
|
+
const result = await executor.exec(TMUX_COMMAND, ["kill-session", "-t", tmuxSession], {
|
|
173
|
+
timeout: TMUX_TIMEOUT_MS,
|
|
174
|
+
});
|
|
175
|
+
if (result.code !== 0 && !isMissingSession(result)) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
return !(await hasTmuxSession(executor, tmuxSession));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function hasTmuxSession(executor: TmuxExecutor, tmuxSession: string): Promise<boolean> {
|
|
182
|
+
const result = await executor.exec(TMUX_COMMAND, ["has-session", "-t", tmuxSession], {
|
|
183
|
+
timeout: TMUX_TIMEOUT_MS,
|
|
184
|
+
});
|
|
185
|
+
return result.code === 0;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function parseWindow(line: string): TmuxWindow | undefined {
|
|
189
|
+
const [windowId, name, piSessionId] = line.split("\t");
|
|
190
|
+
if (!windowId || name === undefined || !piSessionId) {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
return { windowId, name, piSessionId };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function isMissingSession(result: ExecResult): boolean {
|
|
197
|
+
return /can't find session|no server running|failed to connect to server|error connecting to .*no such file/i.test(
|
|
198
|
+
`${result.stderr}\n${result.stdout}`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function tmuxError(action: string, result: ExecResult): Error {
|
|
203
|
+
const details = `${result.stderr || result.stdout}`.trim() || `exit code ${result.code}`;
|
|
204
|
+
return new Error(`Failed to ${action} with tmux: ${details}`);
|
|
205
|
+
}
|