@duetso/agent 0.1.53 → 0.1.54
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 +13 -2
- package/dist/package.json +1 -1
- package/dist/src/cli/help.js +1 -1
- package/dist/src/cli/memory-db.d.ts.map +1 -1
- package/dist/src/cli/memory-db.js +14 -5
- package/dist/src/cli/memory-db.js.map +1 -1
- package/dist/src/cli/memory-tui.js +1 -1
- package/dist/src/cli/memory-tui.js.map +1 -1
- package/dist/src/cli/resume-hint.d.ts +1 -1
- package/dist/src/cli/resume-hint.d.ts.map +1 -1
- package/dist/src/cli/resume-hint.js +2 -2
- package/dist/src/cli/resume-hint.js.map +1 -1
- package/dist/src/cli/run.d.ts +1 -1
- package/dist/src/cli/run.d.ts.map +1 -1
- package/dist/src/cli/run.js +7 -6
- package/dist/src/cli/run.js.map +1 -1
- package/dist/src/memory/context-pack.d.ts +29 -0
- package/dist/src/memory/context-pack.d.ts.map +1 -0
- package/dist/src/memory/context-pack.js +40 -0
- package/dist/src/memory/context-pack.js.map +1 -0
- package/dist/src/memory/embedding-worker.d.ts +26 -0
- package/dist/src/memory/embedding-worker.d.ts.map +1 -0
- package/dist/src/memory/embedding-worker.js +145 -0
- package/dist/src/memory/embedding-worker.js.map +1 -0
- package/dist/src/memory/embedding.d.ts +59 -0
- package/dist/src/memory/embedding.d.ts.map +1 -0
- package/dist/src/memory/embedding.js +126 -0
- package/dist/src/memory/embedding.js.map +1 -0
- package/dist/src/memory/loader.d.ts +81 -0
- package/dist/src/memory/loader.d.ts.map +1 -0
- package/dist/src/memory/loader.js +162 -0
- package/dist/src/memory/loader.js.map +1 -0
- package/dist/src/memory/migrations.d.ts +54 -0
- package/dist/src/memory/migrations.d.ts.map +1 -0
- package/dist/src/memory/migrations.js +199 -0
- package/dist/src/memory/migrations.js.map +1 -0
- package/dist/src/memory/observational.d.ts +18 -0
- package/dist/src/memory/observational.d.ts.map +1 -1
- package/dist/src/memory/observational.js +63 -14
- package/dist/src/memory/observational.js.map +1 -1
- package/dist/src/memory/pglite.d.ts +5 -5
- package/dist/src/memory/pglite.d.ts.map +1 -1
- package/dist/src/memory/pglite.js +20 -6
- package/dist/src/memory/pglite.js.map +1 -1
- package/dist/src/memory/recall.d.ts +49 -0
- package/dist/src/memory/recall.d.ts.map +1 -0
- package/dist/src/memory/recall.js +185 -0
- package/dist/src/memory/recall.js.map +1 -0
- package/dist/src/memory/storage.d.ts +36 -1
- package/dist/src/memory/storage.d.ts.map +1 -1
- package/dist/src/memory/storage.js +68 -14
- package/dist/src/memory/storage.js.map +1 -1
- package/dist/src/memory/store.d.ts +33 -1
- package/dist/src/memory/store.d.ts.map +1 -1
- package/dist/src/memory/store.js +22 -16
- package/dist/src/memory/store.js.map +1 -1
- package/dist/src/session/session.d.ts.map +1 -1
- package/dist/src/session/session.js +5 -1
- package/dist/src/session/session.js.map +1 -1
- package/dist/src/tui/app.d.ts.map +1 -1
- package/dist/src/tui/app.js +327 -30
- package/dist/src/tui/app.js.map +1 -1
- package/dist/src/tui/autocomplete.d.ts.map +1 -1
- package/dist/src/tui/autocomplete.js +5 -0
- package/dist/src/tui/autocomplete.js.map +1 -1
- package/dist/src/tui/clipboard.d.ts +28 -0
- package/dist/src/tui/clipboard.d.ts.map +1 -0
- package/dist/src/tui/clipboard.js +95 -0
- package/dist/src/tui/clipboard.js.map +1 -0
- package/dist/src/tui/history.d.ts +14 -0
- package/dist/src/tui/history.d.ts.map +1 -1
- package/dist/src/tui/history.js +21 -0
- package/dist/src/tui/history.js.map +1 -1
- package/dist/src/tui/recent-sessions.d.ts +51 -0
- package/dist/src/tui/recent-sessions.d.ts.map +1 -0
- package/dist/src/tui/recent-sessions.js +150 -0
- package/dist/src/tui/recent-sessions.js.map +1 -0
- package/dist/src/tui/starters.d.ts +92 -0
- package/dist/src/tui/starters.d.ts.map +1 -0
- package/dist/src/tui/starters.js +196 -0
- package/dist/src/tui/starters.js.map +1 -0
- package/dist/src/tui/theme.d.ts +2 -2
- package/dist/src/tui/theme.d.ts.map +1 -1
- package/dist/src/tui/theme.js +2 -2
- package/dist/src/tui/theme.js.map +1 -1
- package/dist/src/tui/transcript-log.d.ts +45 -0
- package/dist/src/tui/transcript-log.d.ts.map +1 -0
- package/dist/src/tui/transcript-log.js +76 -0
- package/dist/src/tui/transcript-log.js.map +1 -0
- package/dist/src/turn-runner/tools.d.ts +28 -1
- package/dist/src/turn-runner/tools.d.ts.map +1 -1
- package/dist/src/turn-runner/tools.js +135 -3
- package/dist/src/turn-runner/tools.js.map +1 -1
- package/dist/src/turn-runner/turn-runner.d.ts +1 -0
- package/dist/src/turn-runner/turn-runner.d.ts.map +1 -1
- package/dist/src/turn-runner/turn-runner.js +70 -3
- package/dist/src/turn-runner/turn-runner.js.map +1 -1
- package/dist/src/types/config.d.ts +12 -0
- package/dist/src/types/config.d.ts.map +1 -1
- package/dist/src/types/memory.d.ts +59 -21
- package/dist/src/types/memory.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/src/memory/schema.d.ts +0 -7
- package/dist/src/memory/schema.d.ts.map +0 -1
- package/dist/src/memory/schema.js +0 -21
- package/dist/src/memory/schema.js.map +0 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* Filesystem layout the helper expects:
|
|
6
|
+
* <root>/<session-dir>/state.json
|
|
7
|
+
*
|
|
8
|
+
* `state.json` is the same file the runtime writes via Session persistence;
|
|
9
|
+
* we only read the fields needed for the boot list (id, updatedAt, the most
|
|
10
|
+
* recent user prompt). Sessions with no user messages or unparseable JSON
|
|
11
|
+
* are silently skipped so a single bad file never breaks the boot screen.
|
|
12
|
+
*/
|
|
13
|
+
/** Default path mirrors {@link DEFAULT_SESSION_STORAGE_DIR} from session-manager. */
|
|
14
|
+
export const DEFAULT_SESSIONS_ROOT = join(homedir(), ".duet", "sessions");
|
|
15
|
+
/** Hard cap on the displayed prompt before ellipsis truncation. */
|
|
16
|
+
export const RECENT_PROMPT_MAX_LENGTH = 60;
|
|
17
|
+
/**
|
|
18
|
+
* Scan the on-disk sessions directory and return up to `limit` sessions
|
|
19
|
+
* sorted newest-first. Any session that has no readable last user prompt
|
|
20
|
+
* (empty messages, malformed JSON, missing state.json) is skipped so the
|
|
21
|
+
* caller can decide rendering on a clean list.
|
|
22
|
+
*/
|
|
23
|
+
export function listRecentSessions(input = {}) {
|
|
24
|
+
const root = input.sessionsRoot ?? DEFAULT_SESSIONS_ROOT;
|
|
25
|
+
const limit = input.limit ?? 4;
|
|
26
|
+
const excludeId = input.excludeId;
|
|
27
|
+
let entries;
|
|
28
|
+
try {
|
|
29
|
+
entries = readdirSync(root);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
const candidates = [];
|
|
35
|
+
for (const name of entries) {
|
|
36
|
+
if (excludeId && name === excludeId)
|
|
37
|
+
continue;
|
|
38
|
+
const statePath = join(root, name, "state.json");
|
|
39
|
+
let raw;
|
|
40
|
+
let modifiedAt;
|
|
41
|
+
try {
|
|
42
|
+
const stats = statSync(statePath);
|
|
43
|
+
if (!stats.isFile())
|
|
44
|
+
continue;
|
|
45
|
+
modifiedAt = stats.mtimeMs;
|
|
46
|
+
raw = readFileSync(statePath, "utf8");
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
let parsed;
|
|
52
|
+
try {
|
|
53
|
+
parsed = JSON.parse(raw);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const prompt = extractLastUserPrompt(parsed);
|
|
59
|
+
if (!prompt)
|
|
60
|
+
continue;
|
|
61
|
+
candidates.push({ sessionId: name, lastUserPrompt: prompt, modifiedAt });
|
|
62
|
+
}
|
|
63
|
+
candidates.sort((a, b) => b.modifiedAt - a.modifiedAt);
|
|
64
|
+
return candidates.slice(0, limit);
|
|
65
|
+
}
|
|
66
|
+
/** Truncate a prompt for boot-row display; appends `…` when over the cap. */
|
|
67
|
+
export function truncateRecentPrompt(prompt, max = RECENT_PROMPT_MAX_LENGTH) {
|
|
68
|
+
const trimmed = prompt.trim();
|
|
69
|
+
if (trimmed.length <= max)
|
|
70
|
+
return trimmed;
|
|
71
|
+
return `${trimmed.slice(0, max - 1).trimEnd()}…`;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Render a relative-time label suitable for a single boot row.
|
|
75
|
+
* `now` is injectable for tests; production passes `Date.now()`.
|
|
76
|
+
*/
|
|
77
|
+
export function relativeTimeLabel(modifiedAt, now = Date.now()) {
|
|
78
|
+
const diffMs = Math.max(0, now - modifiedAt);
|
|
79
|
+
const minutes = Math.floor(diffMs / 60_000);
|
|
80
|
+
if (minutes < 1)
|
|
81
|
+
return "just now";
|
|
82
|
+
if (minutes < 60)
|
|
83
|
+
return `${minutes}m ago`;
|
|
84
|
+
const hours = Math.floor(minutes / 60);
|
|
85
|
+
if (hours < 24)
|
|
86
|
+
return `${hours}h ago`;
|
|
87
|
+
const days = Math.floor(hours / 24);
|
|
88
|
+
if (days === 1)
|
|
89
|
+
return "yesterday";
|
|
90
|
+
if (days < 7)
|
|
91
|
+
return `${days}d ago`;
|
|
92
|
+
const weeks = Math.floor(days / 7);
|
|
93
|
+
if (weeks < 5)
|
|
94
|
+
return `${weeks}w ago`;
|
|
95
|
+
const months = Math.floor(days / 30);
|
|
96
|
+
if (months < 12)
|
|
97
|
+
return `${months}mo ago`;
|
|
98
|
+
const years = Math.floor(days / 365);
|
|
99
|
+
return `${years}y ago`;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Reach into a parsed state.json and return the last user-authored prompt.
|
|
103
|
+
* The runtime persists messages under `state.agent.messages`; each message
|
|
104
|
+
* has either a string `content` or a list of content blocks where text
|
|
105
|
+
* blocks expose `{ type: "text", text }`.
|
|
106
|
+
*/
|
|
107
|
+
function extractLastUserPrompt(parsed) {
|
|
108
|
+
const messages = pickMessages(parsed);
|
|
109
|
+
if (!messages)
|
|
110
|
+
return undefined;
|
|
111
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
112
|
+
const msg = messages[i];
|
|
113
|
+
if (!msg || typeof msg !== "object")
|
|
114
|
+
continue;
|
|
115
|
+
const role = msg.role;
|
|
116
|
+
if (role !== "user")
|
|
117
|
+
continue;
|
|
118
|
+
const text = userMessageText(msg.content);
|
|
119
|
+
const trimmed = text.trim();
|
|
120
|
+
if (trimmed.length > 0)
|
|
121
|
+
return trimmed;
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
function pickMessages(parsed) {
|
|
126
|
+
if (!parsed || typeof parsed !== "object")
|
|
127
|
+
return undefined;
|
|
128
|
+
const state = parsed.state;
|
|
129
|
+
if (!state || typeof state !== "object")
|
|
130
|
+
return undefined;
|
|
131
|
+
const agent = state.agent;
|
|
132
|
+
if (!agent || typeof agent !== "object")
|
|
133
|
+
return undefined;
|
|
134
|
+
const messages = agent.messages;
|
|
135
|
+
return Array.isArray(messages) ? messages : undefined;
|
|
136
|
+
}
|
|
137
|
+
function userMessageText(content) {
|
|
138
|
+
if (typeof content === "string")
|
|
139
|
+
return content;
|
|
140
|
+
if (!Array.isArray(content))
|
|
141
|
+
return "";
|
|
142
|
+
return content
|
|
143
|
+
.filter((block) => !!block &&
|
|
144
|
+
typeof block === "object" &&
|
|
145
|
+
block.type === "text" &&
|
|
146
|
+
typeof block.text === "string")
|
|
147
|
+
.map((block) => block.text)
|
|
148
|
+
.join("");
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=recent-sessions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recent-sessions.js","sourceRoot":"","sources":["../../../src/tui/recent-sessions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { type RecentSession } from "./recent-sessions.js";
|
|
3
|
+
/**
|
|
4
|
+
* Inputs for {@link selectStarters}. The helper is a pure function over the
|
|
5
|
+
* filesystem and the persisted session history; it never reads from a session
|
|
6
|
+
* runtime, so it can be exercised cheaply in tests with tmp-dir fixtures.
|
|
7
|
+
*/
|
|
8
|
+
export interface StartersInput {
|
|
9
|
+
/** Absolute working directory. Used to detect git/package/scratch context. */
|
|
10
|
+
cwd: string;
|
|
11
|
+
/**
|
|
12
|
+
* Replayed session history (most recent last). When present, the most
|
|
13
|
+
* recent user prompt becomes the optional 5th "resume" starter.
|
|
14
|
+
*/
|
|
15
|
+
sessionHistory?: readonly AgentMessage[];
|
|
16
|
+
/**
|
|
17
|
+
* Pre-loaded list of recent sessions other than the current one. Each
|
|
18
|
+
* entry surfaces as a numbered "continue: <prompt> — <relative time>"
|
|
19
|
+
* row below the cwd-based starters. Caller is responsible for excluding
|
|
20
|
+
* the active session id and ordering newest-first.
|
|
21
|
+
*/
|
|
22
|
+
recentSessions?: readonly RecentSession[];
|
|
23
|
+
/** Override `now` for deterministic relative-time labels in tests. */
|
|
24
|
+
now?: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Output of {@link selectStarters}: cwd-based starters, plus optional
|
|
28
|
+
* recent-session continuations and an optional resume preview for the
|
|
29
|
+
* current session's own history.
|
|
30
|
+
*/
|
|
31
|
+
export interface StartersResult {
|
|
32
|
+
/** Always exactly 4 prompts, ordered for the numbered list rendering. */
|
|
33
|
+
starters: string[];
|
|
34
|
+
/**
|
|
35
|
+
* Last user message from {@link StartersInput.sessionHistory}, trimmed and
|
|
36
|
+
* truncated to {@link RESUME_MAX_LENGTH}. Undefined when no resumable
|
|
37
|
+
* prompt exists.
|
|
38
|
+
*/
|
|
39
|
+
resumePrompt?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Continuation rows derived from {@link StartersInput.recentSessions}.
|
|
42
|
+
* `prompt` is the raw last user prompt from that session (caller decides
|
|
43
|
+
* what to do when a row is picked); `label` is the rendered row body
|
|
44
|
+
* (truncated prompt + relative time) ready to drop into the boot list.
|
|
45
|
+
* The label intentionally omits any "continue:" prefix — the boot screen
|
|
46
|
+
* carries that meaning in the "pick up the thread" section header.
|
|
47
|
+
*/
|
|
48
|
+
recentSessions: {
|
|
49
|
+
sessionId: string;
|
|
50
|
+
prompt: string;
|
|
51
|
+
label: string;
|
|
52
|
+
}[];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* One numbered, selectable boot row in render order.
|
|
56
|
+
*
|
|
57
|
+
* `kind: "recent"` rows always render under the "pick up the thread" header
|
|
58
|
+
* and come first when any recent session exists; `kind: "prompt"` rows render
|
|
59
|
+
* under "or start something new" (returning user) or "what should we work on
|
|
60
|
+
* today?" (new user). The numbering 1..N matches the position in this list,
|
|
61
|
+
* so callers can drive arrow-key + digit navigation off a single index space.
|
|
62
|
+
*/
|
|
63
|
+
export interface SelectableStarter {
|
|
64
|
+
kind: "prompt" | "recent";
|
|
65
|
+
label: string;
|
|
66
|
+
submit: string;
|
|
67
|
+
sessionId?: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Flatten a {@link StartersResult} into the ordered, numbered list the boot
|
|
71
|
+
* screen renders. Recent-session rows lead when present so returning users
|
|
72
|
+
* see continuity first; otherwise the cwd-based starters lead.
|
|
73
|
+
*/
|
|
74
|
+
export declare function orderedSelectableStarters(result: StartersResult): SelectableStarter[];
|
|
75
|
+
/** Hard cap on the resume preview before it gets ellipsis-truncated. */
|
|
76
|
+
export declare const RESUME_MAX_LENGTH = 80;
|
|
77
|
+
/**
|
|
78
|
+
* Pick four starter prompts (and an optional resume preview) based on the
|
|
79
|
+
* shape of the working directory and any prior session history.
|
|
80
|
+
*
|
|
81
|
+
* Detection rules run in priority order; the first match wins. Content
|
|
82
|
+
* signals beat location signals so a working tree that happens to live
|
|
83
|
+
* under /tmp still classifies by what is in it.
|
|
84
|
+
* 1. git repo with at least one commit
|
|
85
|
+
* 2. package.json present (no commits / empty git is fine)
|
|
86
|
+
* 3. .duet/skills/ present
|
|
87
|
+
* 4. text-heavy: more than 5 .md or .txt files and no package.json
|
|
88
|
+
* 5. scratch dir (/tmp, ~/Desktop, ~/Downloads) or empty
|
|
89
|
+
* 6. default
|
|
90
|
+
*/
|
|
91
|
+
export declare function selectStarters(input: StartersInput): StartersResult;
|
|
92
|
+
//# sourceMappingURL=starters.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"starters.d.ts","sourceRoot":"","sources":["../../../src/tui/starters.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAGlE,OAAO,EAAE,KAAK,aAAa,EAA2C,MAAM,sBAAsB,CAAC;AAEnG;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,8EAA8E;IAC9E,GAAG,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,cAAc,CAAC,EAAE,SAAS,YAAY,EAAE,CAAC;IACzC;;;;;OAKG;IACH,cAAc,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;IAC1C,sEAAsE;IACtE,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,yEAAyE;IACzE,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;OAOG;IACH,cAAc,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACxE;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,cAAc,GAAG,iBAAiB,EAAE,CAcrF;AAED,wEAAwE;AACxE,eAAO,MAAM,iBAAiB,KAAK,CAAC;AA4CpC;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,aAAa,GAAG,cAAc,CAYnE"}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { relativeTimeLabel, truncateRecentPrompt } from "./recent-sessions.js";
|
|
6
|
+
/**
|
|
7
|
+
* Flatten a {@link StartersResult} into the ordered, numbered list the boot
|
|
8
|
+
* screen renders. Recent-session rows lead when present so returning users
|
|
9
|
+
* see continuity first; otherwise the cwd-based starters lead.
|
|
10
|
+
*/
|
|
11
|
+
export function orderedSelectableStarters(result) {
|
|
12
|
+
const ordered = [];
|
|
13
|
+
for (const row of result.recentSessions) {
|
|
14
|
+
ordered.push({
|
|
15
|
+
kind: "recent",
|
|
16
|
+
label: row.label,
|
|
17
|
+
submit: row.prompt,
|
|
18
|
+
sessionId: row.sessionId,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
for (const text of result.starters) {
|
|
22
|
+
ordered.push({ kind: "prompt", label: text, submit: text });
|
|
23
|
+
}
|
|
24
|
+
return ordered;
|
|
25
|
+
}
|
|
26
|
+
/** Hard cap on the resume preview before it gets ellipsis-truncated. */
|
|
27
|
+
export const RESUME_MAX_LENGTH = 80;
|
|
28
|
+
const GIT_STARTERS = [
|
|
29
|
+
"review my latest commit and suggest fixes",
|
|
30
|
+
"write release notes for the last 5 commits",
|
|
31
|
+
"find unused exports",
|
|
32
|
+
"summarize what changed in the last week",
|
|
33
|
+
];
|
|
34
|
+
const PACKAGE_STARTERS = [
|
|
35
|
+
"scaffold this idea into a working app",
|
|
36
|
+
"pick a tech stack for me",
|
|
37
|
+
"audit my dependencies",
|
|
38
|
+
"what should I build first",
|
|
39
|
+
];
|
|
40
|
+
const SCRATCH_STARTERS = [
|
|
41
|
+
"build me a landing page",
|
|
42
|
+
"research my top 3 competitors",
|
|
43
|
+
"write a cold email",
|
|
44
|
+
"plan my next launch",
|
|
45
|
+
];
|
|
46
|
+
const SKILL_STARTERS = [
|
|
47
|
+
"create a new skill",
|
|
48
|
+
"improve an existing skill",
|
|
49
|
+
"review my skill catalog",
|
|
50
|
+
"find skills I'm missing",
|
|
51
|
+
];
|
|
52
|
+
const TEXT_STARTERS = [
|
|
53
|
+
"summarize these notes",
|
|
54
|
+
"find duplicates across files",
|
|
55
|
+
"write a blog post from these notes",
|
|
56
|
+
"organize this folder",
|
|
57
|
+
];
|
|
58
|
+
const DEFAULT_STARTERS = [
|
|
59
|
+
"build me something",
|
|
60
|
+
"research a topic",
|
|
61
|
+
"write something",
|
|
62
|
+
"help me think through a decision",
|
|
63
|
+
];
|
|
64
|
+
/**
|
|
65
|
+
* Pick four starter prompts (and an optional resume preview) based on the
|
|
66
|
+
* shape of the working directory and any prior session history.
|
|
67
|
+
*
|
|
68
|
+
* Detection rules run in priority order; the first match wins. Content
|
|
69
|
+
* signals beat location signals so a working tree that happens to live
|
|
70
|
+
* under /tmp still classifies by what is in it.
|
|
71
|
+
* 1. git repo with at least one commit
|
|
72
|
+
* 2. package.json present (no commits / empty git is fine)
|
|
73
|
+
* 3. .duet/skills/ present
|
|
74
|
+
* 4. text-heavy: more than 5 .md or .txt files and no package.json
|
|
75
|
+
* 5. scratch dir (/tmp, ~/Desktop, ~/Downloads) or empty
|
|
76
|
+
* 6. default
|
|
77
|
+
*/
|
|
78
|
+
export function selectStarters(input) {
|
|
79
|
+
const starters = [...pickStarters(input.cwd)];
|
|
80
|
+
const resumePrompt = pickResumePrompt(input.sessionHistory);
|
|
81
|
+
const recentSessions = (input.recentSessions ?? []).map((session) => ({
|
|
82
|
+
sessionId: session.sessionId,
|
|
83
|
+
prompt: session.lastUserPrompt,
|
|
84
|
+
label: `${truncateRecentPrompt(session.lastUserPrompt)} — ${relativeTimeLabel(session.modifiedAt, input.now)}`,
|
|
85
|
+
}));
|
|
86
|
+
return resumePrompt ? { starters, resumePrompt, recentSessions } : { starters, recentSessions };
|
|
87
|
+
}
|
|
88
|
+
function pickStarters(cwd) {
|
|
89
|
+
if (isGitRepoWithCommits(cwd))
|
|
90
|
+
return GIT_STARTERS;
|
|
91
|
+
if (hasPackageJson(cwd))
|
|
92
|
+
return PACKAGE_STARTERS;
|
|
93
|
+
if (hasDuetSkills(cwd))
|
|
94
|
+
return SKILL_STARTERS;
|
|
95
|
+
if (isTextHeavy(cwd))
|
|
96
|
+
return TEXT_STARTERS;
|
|
97
|
+
if (isScratchDir(cwd) || isEmptyDir(cwd))
|
|
98
|
+
return SCRATCH_STARTERS;
|
|
99
|
+
return DEFAULT_STARTERS;
|
|
100
|
+
}
|
|
101
|
+
function isGitRepoWithCommits(cwd) {
|
|
102
|
+
if (!safeIsDir(cwd))
|
|
103
|
+
return false;
|
|
104
|
+
try {
|
|
105
|
+
const out = execFileSync("git", ["-C", cwd, "rev-list", "--count", "HEAD"], {
|
|
106
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
107
|
+
timeout: 1000,
|
|
108
|
+
encoding: "utf8",
|
|
109
|
+
});
|
|
110
|
+
return Number.parseInt(out.trim(), 10) > 0;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function hasPackageJson(cwd) {
|
|
117
|
+
return existsSync(join(cwd, "package.json"));
|
|
118
|
+
}
|
|
119
|
+
// Only the literal scratch roots count. Descendants like `/tmp/foo`
|
|
120
|
+
// fall through to content detection or isEmptyDir — a non-empty
|
|
121
|
+
// directory inside /tmp is a working tree, not a scratch space.
|
|
122
|
+
function isScratchDir(cwd) {
|
|
123
|
+
const resolved = resolve(cwd);
|
|
124
|
+
const home = homedir();
|
|
125
|
+
return (resolved === "/tmp" ||
|
|
126
|
+
resolved === join(home, "Desktop") ||
|
|
127
|
+
resolved === join(home, "Downloads"));
|
|
128
|
+
}
|
|
129
|
+
function isEmptyDir(cwd) {
|
|
130
|
+
if (!safeIsDir(cwd))
|
|
131
|
+
return false;
|
|
132
|
+
try {
|
|
133
|
+
return readdirSync(cwd).length === 0;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function hasDuetSkills(cwd) {
|
|
140
|
+
const dir = join(cwd, ".duet", "skills");
|
|
141
|
+
return existsSync(dir) && safeIsDir(dir);
|
|
142
|
+
}
|
|
143
|
+
function isTextHeavy(cwd) {
|
|
144
|
+
if (!safeIsDir(cwd))
|
|
145
|
+
return false;
|
|
146
|
+
if (hasPackageJson(cwd))
|
|
147
|
+
return false;
|
|
148
|
+
let textFiles = 0;
|
|
149
|
+
try {
|
|
150
|
+
for (const name of readdirSync(cwd)) {
|
|
151
|
+
const lower = name.toLowerCase();
|
|
152
|
+
if (lower.endsWith(".md") || lower.endsWith(".txt")) {
|
|
153
|
+
textFiles += 1;
|
|
154
|
+
if (textFiles > 5)
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
function safeIsDir(path) {
|
|
165
|
+
try {
|
|
166
|
+
return statSync(path).isDirectory();
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function pickResumePrompt(history) {
|
|
173
|
+
if (!history || history.length === 0)
|
|
174
|
+
return undefined;
|
|
175
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
176
|
+
const msg = history[i];
|
|
177
|
+
if (!msg || !("role" in msg) || msg.role !== "user")
|
|
178
|
+
continue;
|
|
179
|
+
const text = userMessageText(msg.content).trim();
|
|
180
|
+
if (!text)
|
|
181
|
+
continue;
|
|
182
|
+
if (text.length <= RESUME_MAX_LENGTH)
|
|
183
|
+
return text;
|
|
184
|
+
return `${text.slice(0, RESUME_MAX_LENGTH - 1).trimEnd()}…`;
|
|
185
|
+
}
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
function userMessageText(content) {
|
|
189
|
+
if (typeof content === "string")
|
|
190
|
+
return content;
|
|
191
|
+
return content
|
|
192
|
+
.filter((block) => block.type === "text")
|
|
193
|
+
.map((block) => block.text)
|
|
194
|
+
.join("");
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=starters.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"starters.js","sourceRoot":"","sources":["../../../src/tui/starters.ts"],"names":[],"mappings":"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"}
|
package/dist/src/tui/theme.d.ts
CHANGED
|
@@ -10,6 +10,6 @@ export declare const COLORS: {
|
|
|
10
10
|
readonly status: "#34D399";
|
|
11
11
|
readonly border: "#374151";
|
|
12
12
|
};
|
|
13
|
-
export declare const HINT_IDLE = "Enter: send \u00B7
|
|
14
|
-
export declare const HINT_RUNNING = "Enter:
|
|
13
|
+
export declare const HINT_IDLE = "Enter: send \u00B7 Shift+Enter: newline \u00B7 drag-select + Cmd+C to copy \u00B7 Esc: quit";
|
|
14
|
+
export declare const HINT_RUNNING = "Enter: queue follow-up \u00B7 drag-select + Cmd+C to copy \u00B7 Esc: interrupt";
|
|
15
15
|
//# sourceMappingURL=theme.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../../../src/tui/theme.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,MAAM;;;;;;;;;;;CAWT,CAAC;AAEX,eAAO,MAAM,SAAS,
|
|
1
|
+
{"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../../../src/tui/theme.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,MAAM;;;;;;;;;;;CAWT,CAAC;AAEX,eAAO,MAAM,SAAS,gGAC0D,CAAC;AACjF,eAAO,MAAM,YAAY,oFAA0E,CAAC"}
|
package/dist/src/tui/theme.js
CHANGED
|
@@ -13,6 +13,6 @@ export const COLORS = {
|
|
|
13
13
|
status: "#34D399",
|
|
14
14
|
border: "#374151",
|
|
15
15
|
};
|
|
16
|
-
export const HINT_IDLE = "Enter: send ·
|
|
17
|
-
export const HINT_RUNNING = "Enter:
|
|
16
|
+
export const HINT_IDLE = "Enter: send · Shift+Enter: newline · drag-select + Cmd+C to copy · Esc: quit";
|
|
17
|
+
export const HINT_RUNNING = "Enter: queue follow-up · drag-select + Cmd+C to copy · Esc: interrupt";
|
|
18
18
|
//# sourceMappingURL=theme.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"theme.js","sourceRoot":"","sources":["../../../src/tui/theme.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,wEAAwE;AACxE,2DAA2D;AAC3D,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,IAAI,EAAE,SAAS;IACf,KAAK,EAAE,SAAS;IAChB,SAAS,EAAE,SAAS;IACpB,IAAI,EAAE,SAAS;IACf,MAAM,EAAE,SAAS;IACjB,KAAK,EAAE,SAAS;IAChB,IAAI,EAAE,SAAS;IACf,MAAM,EAAE,SAAS;IACjB,MAAM,EAAE,SAAS;IACjB,MAAM,EAAE,SAAS;CACT,CAAC;AAEX,MAAM,CAAC,MAAM,SAAS,
|
|
1
|
+
{"version":3,"file":"theme.js","sourceRoot":"","sources":["../../../src/tui/theme.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,wEAAwE;AACxE,2DAA2D;AAC3D,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,IAAI,EAAE,SAAS;IACf,KAAK,EAAE,SAAS;IAChB,SAAS,EAAE,SAAS;IACpB,IAAI,EAAE,SAAS;IACf,MAAM,EAAE,SAAS;IACjB,KAAK,EAAE,SAAS;IAChB,IAAI,EAAE,SAAS;IACf,MAAM,EAAE,SAAS;IACjB,MAAM,EAAE,SAAS;IACjB,MAAM,EAAE,SAAS;CACT,CAAC;AAEX,MAAM,CAAC,MAAM,SAAS,GACpB,8EAA8E,CAAC;AACjF,MAAM,CAAC,MAAM,YAAY,GAAG,uEAAuE,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny in-memory log of user / agent messages used by the `/copy` slash
|
|
3
|
+
* command and the Ctrl+Y keystroke. The TUI's transcript itself is rendered
|
|
4
|
+
* line-by-line into a ScrollBoxRenderable, so there is no straightforward
|
|
5
|
+
* way to retrieve "the last assistant message" as a clean string. This log
|
|
6
|
+
* is the parallel structure that makes copy-out possible.
|
|
7
|
+
*
|
|
8
|
+
* Only logical message bodies belong here — labels (`you:`, `[reasoning]`,
|
|
9
|
+
* `[tool]`, ...) live in the transcript renderer and are stripped before
|
|
10
|
+
* appending so what lands on the clipboard is exactly the text the user
|
|
11
|
+
* (or the agent) wrote.
|
|
12
|
+
*/
|
|
13
|
+
export type TranscriptEntryKind = "user" | "agent";
|
|
14
|
+
export interface TranscriptEntry {
|
|
15
|
+
kind: TranscriptEntryKind;
|
|
16
|
+
/** Final body text. For streamed agent replies, append the resolved text once. */
|
|
17
|
+
text: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Pick the text to copy based on the user's `/copy` argument.
|
|
21
|
+
*
|
|
22
|
+
* - `"last"` (the default): the most recent agent reply. Falls back to the
|
|
23
|
+
* most recent user message only if no agent reply exists yet — useful when
|
|
24
|
+
* a user wants to grab their own prompt back before the agent responds.
|
|
25
|
+
* - `"all"`: the entire conversation, in order, formatted with `you:` /
|
|
26
|
+
* `agent:` labels so the export reads cleanly outside the terminal.
|
|
27
|
+
* - a positive integer N: the last N entries (any kind), formatted the same
|
|
28
|
+
* way as `"all"`.
|
|
29
|
+
*
|
|
30
|
+
* Returns `undefined` when the requested slice is empty so the caller can
|
|
31
|
+
* surface "nothing to copy yet" instead of writing an empty clipboard.
|
|
32
|
+
*/
|
|
33
|
+
export declare function selectCopyText(log: readonly TranscriptEntry[], argument: "last" | "all" | number): string | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Parse the raw argument portion of a `/copy ...` command. Accepted shapes:
|
|
36
|
+
*
|
|
37
|
+
* /copy → "last"
|
|
38
|
+
* /copy last → "last"
|
|
39
|
+
* /copy all → "all"
|
|
40
|
+
* /copy 5 → 5
|
|
41
|
+
*
|
|
42
|
+
* Returns `undefined` for malformed input so the caller can show usage help.
|
|
43
|
+
*/
|
|
44
|
+
export declare function parseCopyArgument(raw: string): "last" | "all" | number | undefined;
|
|
45
|
+
//# sourceMappingURL=transcript-log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transcript-log.d.ts","sourceRoot":"","sources":["../../../src/tui/transcript-log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,OAAO,CAAC;AAEnD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,mBAAmB,CAAC;IAC1B,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,SAAS,eAAe,EAAE,EAC/B,QAAQ,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAChC,MAAM,GAAG,SAAS,CAgBpB;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,SAAS,CAOlF"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny in-memory log of user / agent messages used by the `/copy` slash
|
|
3
|
+
* command and the Ctrl+Y keystroke. The TUI's transcript itself is rendered
|
|
4
|
+
* line-by-line into a ScrollBoxRenderable, so there is no straightforward
|
|
5
|
+
* way to retrieve "the last assistant message" as a clean string. This log
|
|
6
|
+
* is the parallel structure that makes copy-out possible.
|
|
7
|
+
*
|
|
8
|
+
* Only logical message bodies belong here — labels (`you:`, `[reasoning]`,
|
|
9
|
+
* `[tool]`, ...) live in the transcript renderer and are stripped before
|
|
10
|
+
* appending so what lands on the clipboard is exactly the text the user
|
|
11
|
+
* (or the agent) wrote.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Pick the text to copy based on the user's `/copy` argument.
|
|
15
|
+
*
|
|
16
|
+
* - `"last"` (the default): the most recent agent reply. Falls back to the
|
|
17
|
+
* most recent user message only if no agent reply exists yet — useful when
|
|
18
|
+
* a user wants to grab their own prompt back before the agent responds.
|
|
19
|
+
* - `"all"`: the entire conversation, in order, formatted with `you:` /
|
|
20
|
+
* `agent:` labels so the export reads cleanly outside the terminal.
|
|
21
|
+
* - a positive integer N: the last N entries (any kind), formatted the same
|
|
22
|
+
* way as `"all"`.
|
|
23
|
+
*
|
|
24
|
+
* Returns `undefined` when the requested slice is empty so the caller can
|
|
25
|
+
* surface "nothing to copy yet" instead of writing an empty clipboard.
|
|
26
|
+
*/
|
|
27
|
+
export function selectCopyText(log, argument) {
|
|
28
|
+
if (argument === "last") {
|
|
29
|
+
const lastAgent = findLast(log, (entry) => entry.kind === "agent");
|
|
30
|
+
if (lastAgent)
|
|
31
|
+
return lastAgent.text;
|
|
32
|
+
const lastUser = findLast(log, (entry) => entry.kind === "user");
|
|
33
|
+
return lastUser?.text;
|
|
34
|
+
}
|
|
35
|
+
if (argument === "all") {
|
|
36
|
+
return log.length > 0 ? formatEntries(log) : undefined;
|
|
37
|
+
}
|
|
38
|
+
// Numeric N — clamp to log length and take the tail.
|
|
39
|
+
const n = Math.max(1, Math.floor(argument));
|
|
40
|
+
const slice = log.slice(-n);
|
|
41
|
+
return slice.length > 0 ? formatEntries(slice) : undefined;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Parse the raw argument portion of a `/copy ...` command. Accepted shapes:
|
|
45
|
+
*
|
|
46
|
+
* /copy → "last"
|
|
47
|
+
* /copy last → "last"
|
|
48
|
+
* /copy all → "all"
|
|
49
|
+
* /copy 5 → 5
|
|
50
|
+
*
|
|
51
|
+
* Returns `undefined` for malformed input so the caller can show usage help.
|
|
52
|
+
*/
|
|
53
|
+
export function parseCopyArgument(raw) {
|
|
54
|
+
const trimmed = raw.trim();
|
|
55
|
+
if (trimmed === "" || trimmed === "last")
|
|
56
|
+
return "last";
|
|
57
|
+
if (trimmed === "all")
|
|
58
|
+
return "all";
|
|
59
|
+
const n = Number(trimmed);
|
|
60
|
+
if (Number.isFinite(n) && n >= 1 && Number.isInteger(n))
|
|
61
|
+
return n;
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
function findLast(items, predicate) {
|
|
65
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
66
|
+
if (predicate(items[i]))
|
|
67
|
+
return items[i];
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
function formatEntries(entries) {
|
|
72
|
+
return entries
|
|
73
|
+
.map((entry) => `${entry.kind === "user" ? "you" : "agent"}: ${entry.text}`)
|
|
74
|
+
.join("\n\n");
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=transcript-log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transcript-log.js","sourceRoot":"","sources":["../../../src/tui/transcript-log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAUH;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,cAAc,CAC5B,GAA+B,EAC/B,QAAiC;IAEjC,IAAI,QAAQ,KAAK,MAAM,EAAE,CAAC;QACxB,MAAM,SAAS,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC;QACnE,IAAI,SAAS;YAAE,OAAO,SAAS,CAAC,IAAI,CAAC;QACrC,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC;QACjE,OAAO,QAAQ,EAAE,IAAI,CAAC;IACxB,CAAC;IAED,IAAI,QAAQ,KAAK,KAAK,EAAE,CAAC;QACvB,OAAO,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACzD,CAAC;IAED,qDAAqD;IACrD,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC5C,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5B,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAC7D,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3B,IAAI,OAAO,KAAK,EAAE,IAAI,OAAO,KAAK,MAAM;QAAE,OAAO,MAAM,CAAC;IACxD,IAAI,OAAO,KAAK,KAAK;QAAE,OAAO,KAAK,CAAC;IACpC,MAAM,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;IAC1B,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;QAAE,OAAO,CAAC,CAAC;IAClE,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,QAAQ,CAAI,KAAmB,EAAE,SAA+B;IACvE,KAAK,IAAI,CAAC,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3C,IAAI,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAAE,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC;IAC3C,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,aAAa,CAAC,OAAmC;IACxD,OAAO,OAAO;SACX,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,KAAK,KAAK,CAAC,IAAI,EAAE,CAAC;SAC3E,IAAI,CAAC,MAAM,CAAC,CAAC;AAClB,CAAC"}
|