@bacnh85/pi-subagent 0.14.0 → 0.15.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/CHANGELOG.md CHANGED
@@ -1,5 +1,76 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.15.0 (2026-08-09)
4
+
5
+ ### Packaging
6
+
7
+ - Added `extensions/background.ts`, `history.ts`, `result.ts`, `widget.ts`
8
+ to `files[]` — they were imported by `index.ts` but missing from the
9
+ manifest, so the published package would have failed to load.
10
+
11
+ ### Compact collapsed result (no tool-call trace in conversation)
12
+
13
+ Completed single subagent results no longer dump the tool-call trace
14
+ (`→ ls`, `→ grep`, `→ read …`) into the conversation block. The collapsed
15
+ view now shows the answer preview + usage, matching Claude Code's
16
+ `⎿ Done (N tool uses · tokens)` and pi-task's `⎿ <summary> (Ctrl+O to expand)`
17
+ UX. The full trace remains available via Ctrl+O (expanded) and `/agent`
18
+ (thread viewer); the hint text now points to both.
19
+
20
+ - `renderSingleResult` collapsed branch: `✓ agent` + `⎿ <first ~200 chars of
21
+ final output>` + usage + `(Ctrl+O to expand · /agent for full thread)`.
22
+ - Removed the dead `renderDisplayItems` / `COLLAPSED_ITEM_COUNT` — the
23
+ collapsed path no longer lists tool calls (was the noise source).
24
+ - Parallel and chain collapsed views were already compact (per-task/step
25
+ one-liners) — unchanged.
26
+ - New tests: `render.test.ts` (5 cases) asserting collapsed shows the answer
27
+ preview and NOT the tool-call trace, plus error/no-output/hint paths.
28
+
29
+ ### Live progress widget (Phase 1)
30
+
31
+ A persistent above-editor widget now shows what each running subagent is doing
32
+ right now — spinner, agent name, elapsed time, tool-call count, and the latest
33
+ tool call with ✓/✗/⟳ status. Fed by live `threadStore` subscriptions (per SDK
34
+ session event), not JSONL polling. Replaces the old 30s plain-text
35
+ "still running…" heartbeat.
36
+
37
+ - One block per running thread (single, parallel, chain), capped at 8 +
38
+ `+N more running`.
39
+ - Latest tool-call line with `done`/`error`/`in_progress` status derived by
40
+ pairing assistant `toolCall` parts against later `toolResult` messages.
41
+ - Clears automatically when no threads are running.
42
+ - Inspired by [pi-task](https://github.com/heyhuynhgiabuu/pi-task)'s widget UX,
43
+ but cheaper: in-process SDK gives per-event live data without polling.
44
+
45
+ ### Background mode + task control (Phase 2)
46
+
47
+ - `background: true` (single mode only) runs the subagent detached: `execute`
48
+ returns immediately with a receipt, and completion arrives as a follow-up
49
+ turn via `sendMessage({ triggerTurn: true, deliverAs: "followUp" })`.
50
+ - `operation: "status"` / `operation: "cancel"` with `taskId` inspects or
51
+ cancels a running background task without relaunching.
52
+ - A `pi-subagent-complete` message renderer renders the follow-up turn
53
+ compactly (status icon, agent, output, usage).
54
+ - Background tasks are aborted and the widget is disposed on `session_shutdown`.
55
+
56
+ ### Structured result + history registry (Phase 3)
57
+
58
+ - New `result.ts`: parent-side structured extraction of the child's final
59
+ message (summary, findings, files, caveats, next steps) by detecting
60
+ markdown headers. No child XML contract — we structure the output ourselves.
61
+ - New `history.ts`: durable metadata under `.pi/subagent-history.json`. Every
62
+ completed task (foreground and background) is recorded.
63
+ - `/subagent history` lists recent delegations with status and timestamp.
64
+ - On restart, prior-session `running` entries are marked `interrupted` (honest
65
+ about the in-process ceiling: we cannot resume a live SDK session).
66
+
67
+ ## 0.14.1 (2026-08-07)
68
+
69
+ ### Improvements
70
+
71
+ - Widen peer dependency range to support Pi 0.84.0 (`>=0.80.0 <0.85.0`).
72
+ No code changes — verified compatible against the 0.84.0 SDK types.
73
+
3
74
  ## 0.14.0 (2026-08-05)
4
75
 
5
76
  ### Git worktree isolation (`sandbox: worktree`)
package/README.md CHANGED
@@ -1,6 +1,39 @@
1
1
  # @bacnh85/pi-subagent
2
2
 
3
- Isolated in-process subagents for Pi. The `subagent` tool supports single, parallel (8 tasks, 4 concurrent), and chained execution; `/agent` opens inspectable child threads.
3
+ Isolated in-process subagents for Pi. The `subagent` tool supports single, parallel (8 tasks, 4 concurrent), and chained execution; `/agent` opens inspectable child threads. A live progress widget shows running tasks above the editor; `background: true` runs detached with follow-up-turn completion.
4
+
5
+ ## Live progress widget
6
+
7
+ When subagents run, a persistent widget appears above the editor showing each
8
+ running task: spinner, agent name, elapsed time, tool count, and the latest tool
9
+ call with ✓/✗/⟳ status. The widget clears when no tasks are running. Fed by live
10
+ SDK session events — no polling.
11
+
12
+ ## Background mode
13
+
14
+ Single-mode tasks can run detached with `background: true`:
15
+
16
+ ```ts
17
+ subagent({ agent: "planner", task: "Design the cache layer", background: true })
18
+ ```
19
+
20
+ The tool returns immediately with a receipt (including a `taskId`), and when
21
+ the subagent finishes, the result arrives as a follow-up turn that the parent
22
+ agent reads and acts on. Use task control to inspect or cancel:
23
+
24
+ ```ts
25
+ subagent({ operation: "status", taskId: "bg-..." }) // read-only snapshot
26
+ subagent({ operation: "cancel", taskId: "bg-..." }) // abort a running task
27
+ ```
28
+
29
+ You will be notified on completion — do not poll or sleep.
30
+
31
+ ## History
32
+
33
+ Every completed task (foreground and background) is recorded to
34
+ `.pi/subagent-history.json`. Use `/subagent history` to list recent
35
+ delegations with status and timestamp. Prior-session running tasks are marked
36
+ `interrupted` on restart (in-process SDK sessions cannot be resumed).
4
37
 
5
38
  ## Install
6
39
 
@@ -0,0 +1,351 @@
1
+ /**
2
+ * Background task execution for pi-subagent.
3
+ *
4
+ * A background task runs detached from the parent tool call: `execute` returns
5
+ * immediately with a receipt, and when the child finishes, a follow-up turn is
6
+ * delivered via sendMessage({ triggerTurn: true, deliverAs: "followUp" }).
7
+ *
8
+ * The in-memory registry supports task control (status/cancel) by taskId.
9
+ */
10
+
11
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
12
+ import { CONFIG_DIR_NAME } from "@earendil-works/pi-coding-agent";
13
+ import { join } from "node:path";
14
+ import type { SubAgentResult, SubAgentProgress } from "./runner.ts";
15
+ import { isFailedResult, getResultOutput, getFinalOutput } from "./runner.ts";
16
+ import type { threadStore as ThreadStoreType } from "./threads.ts";
17
+ import type { AgentScope } from "./agents.ts";
18
+ import { parseStructuredResult } from "./result.ts";
19
+ import { appendHistory } from "./history.ts";
20
+
21
+ /** Mirrors SubagentDetails from index.ts without a circular import. */
22
+ interface BackgroundDetails {
23
+ mode: "single" | "parallel" | "chain";
24
+ agentScope: AgentScope;
25
+ projectAgentsDir: string | null;
26
+ results: SubAgentResult[];
27
+ }
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Types
31
+ // ---------------------------------------------------------------------------
32
+
33
+ export type BackgroundStatus = "running" | "completed" | "failed" | "aborted" | "timeout";
34
+
35
+ export interface BackgroundTask {
36
+ id: string;
37
+ threadId: string;
38
+ agent: string;
39
+ task: string;
40
+ startedAt: number;
41
+ status: BackgroundStatus;
42
+ controller: AbortController;
43
+ result?: SubAgentResult;
44
+ completedAt?: number;
45
+ }
46
+
47
+ export interface BackgroundDeps {
48
+ pi: ExtensionAPI;
49
+ ctx: ExtensionContext;
50
+ /** Run a single agent (the existing runOne closure). */
51
+ runOne: (
52
+ agent: string,
53
+ task: string,
54
+ cwd: string | undefined,
55
+ signal: AbortSignal,
56
+ timeoutMs: number | undefined,
57
+ onProgress: (partial: SubAgentResult) => void,
58
+ onActivity: (progress: SubAgentProgress) => void,
59
+ onHeartbeatDetails: () => BackgroundDetails,
60
+ onHeartbeat: () => void,
61
+ isReadOnly?: boolean,
62
+ ) => Promise<SubAgentResult>;
63
+ threadStore: typeof ThreadStoreType;
64
+ }
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Registry (in-memory, per-session)
68
+ // ---------------------------------------------------------------------------
69
+
70
+ const backgroundTasks = new Map<string, BackgroundTask>();
71
+
72
+ export function getBackgroundTask(id: string): BackgroundTask | undefined {
73
+ return backgroundTasks.get(id);
74
+ }
75
+
76
+ export function getAllBackgroundTasks(): BackgroundTask[] {
77
+ return Array.from(backgroundTasks.values());
78
+ }
79
+
80
+ export function clearBackgroundTasks(): void {
81
+ for (const task of backgroundTasks.values()) {
82
+ task.controller.abort();
83
+ }
84
+ backgroundTasks.clear();
85
+ }
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // Status snapshot (for operation: "status")
89
+ // ---------------------------------------------------------------------------
90
+
91
+ export interface TaskSnapshot {
92
+ id: string;
93
+ agent: string;
94
+ task: string;
95
+ status: BackgroundStatus;
96
+ startedAt: number;
97
+ completedAt?: number;
98
+ elapsedMs: number;
99
+ threadId: string;
100
+ result?: { output: string; model?: string; usage?: unknown };
101
+ }
102
+
103
+ export function snapshotTask(task: BackgroundTask, now = Date.now()): TaskSnapshot {
104
+ const result = task.result;
105
+ return {
106
+ id: task.id,
107
+ agent: task.agent,
108
+ task: task.task,
109
+ status: task.status,
110
+ startedAt: task.startedAt,
111
+ completedAt: task.completedAt,
112
+ elapsedMs: now - task.startedAt,
113
+ threadId: task.threadId,
114
+ result: result
115
+ ? { output: getResultOutput(result), model: result.model, usage: result.usage }
116
+ : undefined,
117
+ };
118
+ }
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // Start a background task
122
+ // ---------------------------------------------------------------------------
123
+
124
+ export interface StartBackgroundInput {
125
+ agent: string;
126
+ task: string;
127
+ cwd?: string;
128
+ timeout?: number;
129
+ agentColor?: string;
130
+ toolCallId?: string;
131
+ deps: BackgroundDeps;
132
+ }
133
+
134
+ export interface StartBackgroundResult {
135
+ taskId: string;
136
+ receipt: string;
137
+ }
138
+
139
+ /**
140
+ * Start a detached background task. Returns immediately with a receipt.
141
+ * On completion, delivers a follow-up turn to the parent session.
142
+ */
143
+ export function startBackgroundTask(input: StartBackgroundInput): StartBackgroundResult {
144
+ const { agent, task, cwd, timeout, agentColor, toolCallId, deps } = input;
145
+ const taskId = `bg-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`;
146
+ const controller = new AbortController();
147
+ const startedAt = Date.now();
148
+
149
+ // Create a thread so the task shows in the widget + /agent viewer.
150
+ const thread = deps.threadStore.createThread({
151
+ agentName: agent,
152
+ task,
153
+ mode: "single",
154
+ toolCallId,
155
+ color: agentColor,
156
+ });
157
+
158
+ const bgTask: BackgroundTask = {
159
+ id: taskId,
160
+ threadId: thread.id,
161
+ agent,
162
+ task,
163
+ startedAt,
164
+ status: "running",
165
+ controller,
166
+ };
167
+ backgroundTasks.set(taskId, bgTask);
168
+
169
+ // Run detached — the parent does NOT await this.
170
+ void deps
171
+ .runOne(
172
+ agent,
173
+ task,
174
+ cwd,
175
+ controller.signal,
176
+ timeout,
177
+ (partial) => deps.threadStore.updateThread(thread.id, { result: partial }),
178
+ (progress) => deps.threadStore.updateProgress(thread.id, progress),
179
+ () => ({ mode: "single" as const, agentScope: "user" as const, projectAgentsDir: null, results: [] }),
180
+ () => deps.threadStore.refreshHeartbeat(thread.id),
181
+ )
182
+ .then((result) => {
183
+ bgTask.result = result;
184
+ bgTask.completedAt = Date.now();
185
+ const failed = isFailedResult(result);
186
+ bgTask.status = failed
187
+ ? result.stopReason === "timeout"
188
+ ? "timeout"
189
+ : result.stopReason === "aborted"
190
+ ? "aborted"
191
+ : "failed"
192
+ : "completed";
193
+ try {
194
+ deps.threadStore.updateThread(thread.id, {
195
+ status: failed ? (result.stopReason === "aborted" ? "aborted" : "failed") : "completed",
196
+ result,
197
+ });
198
+ } catch {
199
+ /* thread store unavailable — non-fatal; status/history already set */
200
+ }
201
+ recordHistory(bgTask, result, deps);
202
+ // Delivery failure must NOT cascade into .catch: the task genuinely
203
+ // completed, so its status/history must reflect that. Swallow sendMessage
204
+ // errors (e.g. session already shut down) as non-fatal.
205
+ try {
206
+ deliverCompletion(bgTask, result, deps);
207
+ } catch {
208
+ /* delivery failed — non-fatal; status/history already recorded */
209
+ }
210
+ scheduleEviction(taskId);
211
+ })
212
+ .catch((err: unknown) => {
213
+ // Reached when runOne itself rejects (delivery is isolated above; any
214
+ // other side-effect throw in .then is also caught here defensively).
215
+ bgTask.completedAt = Date.now();
216
+ bgTask.status = "failed";
217
+ const errMsg = err instanceof Error ? err.message : String(err);
218
+ try {
219
+ deps.threadStore.updateThread(thread.id, { status: "failed" });
220
+ } catch {
221
+ /* thread store unavailable — non-fatal */
222
+ }
223
+ recordHistory(bgTask, undefined, deps, errMsg);
224
+ try {
225
+ deliverError(bgTask, errMsg, deps);
226
+ } catch {
227
+ /* delivery failed — non-fatal */
228
+ }
229
+ scheduleEviction(taskId);
230
+ });
231
+
232
+ const receipt = [
233
+ `Started background task ${taskId} (${agent}).`,
234
+ `Use subagent operation:"status" taskId:"${taskId}" to inspect progress.`,
235
+ "You will be notified automatically when it completes — DO NOT poll or sleep.",
236
+ ].join("\n");
237
+
238
+ return { taskId, receipt };
239
+ }
240
+
241
+ // ---------------------------------------------------------------------------
242
+ // Completion delivery (follow-up turn)
243
+ // ---------------------------------------------------------------------------
244
+
245
+ function deliverCompletion(task: BackgroundTask, result: SubAgentResult, deps: BackgroundDeps): void {
246
+ const output = getFinalOutput(result.messages) || getResultOutput(result) || "(no output)";
247
+ const failed = isFailedResult(result);
248
+ const phase = task.status;
249
+ const summary = output.split("\n").slice(0, 1)[0]!.slice(0, 200);
250
+ const elapsedMs = (task.completedAt ?? Date.now()) - task.startedAt;
251
+
252
+ deps.pi.sendMessage(
253
+ {
254
+ customType: "pi-subagent-complete",
255
+ content: failed
256
+ ? `Background task ${task.id} (${task.agent}) ${phase}.\n\n${getResultOutput(result)}`
257
+ : `Background task ${task.id} (${task.agent}) completed.\n\n${output}`,
258
+ display: true,
259
+ details: {
260
+ task_id: task.id,
261
+ agent: task.agent,
262
+ status: task.status,
263
+ summary,
264
+ full_output: output,
265
+ elapsed_ms: elapsedMs,
266
+ model: result.model,
267
+ usage: result.usage,
268
+ background: true,
269
+ },
270
+ },
271
+ { triggerTurn: true, deliverAs: "followUp" },
272
+ );
273
+ }
274
+
275
+ function deliverError(task: BackgroundTask, errMsg: string, deps: BackgroundDeps): void {
276
+ deps.pi.sendMessage(
277
+ {
278
+ customType: "pi-subagent-complete",
279
+ content: `Background task ${task.id} (${task.agent}) failed: ${errMsg}`,
280
+ display: true,
281
+ details: {
282
+ task_id: task.id,
283
+ agent: task.agent,
284
+ status: "failed" as const,
285
+ summary: errMsg,
286
+ full_output: errMsg,
287
+ background: true,
288
+ },
289
+ },
290
+ { triggerTurn: true, deliverAs: "followUp" },
291
+ );
292
+ }
293
+
294
+ // ---------------------------------------------------------------------------
295
+ // History recording (durable metadata for /subagent history)
296
+ // ---------------------------------------------------------------------------
297
+
298
+ function recordHistory(
299
+ task: BackgroundTask,
300
+ result: SubAgentResult | undefined,
301
+ deps: BackgroundDeps,
302
+ errMsg?: string,
303
+ ): void {
304
+ try {
305
+ const output = errMsg ?? (result ? getFinalOutput(result.messages) || getResultOutput(result) : "");
306
+ const structured = parseStructuredResult(output);
307
+ const piDir = join(deps.ctx.cwd, CONFIG_DIR_NAME);
308
+ appendHistory(piDir, {
309
+ id: task.id,
310
+ agent: task.agent,
311
+ task: task.task,
312
+ status: task.status === "running" ? "interrupted" : (task.status as "completed" | "failed" | "aborted" | "timeout"),
313
+ startedAt: task.startedAt,
314
+ completedAt: task.completedAt,
315
+ summary: structured.summary,
316
+ background: true,
317
+ model: result?.model,
318
+ });
319
+ } catch {
320
+ // History file not writable — non-fatal.
321
+ }
322
+ }
323
+
324
+ // ---------------------------------------------------------------------------
325
+ // Cancellation
326
+ // ---------------------------------------------------------------------------
327
+
328
+ export type CancelOutcome = "cancelled" | "not_found" | "already_done";
329
+
330
+ export function cancelBackgroundTask(id: string): { outcome: CancelOutcome; task?: BackgroundTask } {
331
+ const task = backgroundTasks.get(id);
332
+ if (!task) return { outcome: "not_found" };
333
+ if (task.status !== "running") return { outcome: "already_done", task };
334
+ task.controller.abort();
335
+ // Status will be finalized by the runOne promise resolving with aborted.
336
+ return { outcome: "cancelled", task };
337
+ }
338
+
339
+ // ---------------------------------------------------------------------------
340
+ // Eviction — completed tasks are retained briefly for status queries, then
341
+ // dropped so the in-memory registry doesn't grow unbounded across a session.
342
+ // ---------------------------------------------------------------------------
343
+
344
+ const TASK_RETENTION_MS = 60_000;
345
+
346
+ function scheduleEviction(taskId: string): void {
347
+ const timer = setTimeout(() => {
348
+ backgroundTasks.delete(taskId);
349
+ }, TASK_RETENTION_MS);
350
+ timer.unref?.();
351
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Task history registry — durable metadata for listing and re-run.
3
+ *
4
+ * Persists completed task metadata under .pi/subagent-history.json so the user
5
+ * can review past delegations via `/subagent history`. This is metadata ONLY:
6
+ * in-process SDK cannot resume a live session, so we never advertise "resume".
7
+ *
8
+ * On session start, any `running` entries from a prior session (crash/restart)
9
+ * are marked `interrupted` — honest about the architectural ceiling.
10
+ */
11
+
12
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
13
+ import { dirname, join } from "node:path";
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Types
17
+ // ---------------------------------------------------------------------------
18
+
19
+ export type HistoryStatus = "completed" | "failed" | "aborted" | "timeout" | "interrupted" | "running";
20
+
21
+ export interface HistoryEntry {
22
+ id: string;
23
+ agent: string;
24
+ task: string;
25
+ status: HistoryStatus;
26
+ startedAt: number;
27
+ completedAt?: number;
28
+ summary?: string;
29
+ cwd?: string;
30
+ background?: boolean;
31
+ model?: string;
32
+ }
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Path resolution
36
+ // ---------------------------------------------------------------------------
37
+
38
+ const HISTORY_FILE = "subagent-history.json";
39
+
40
+ /** Maximum history entries kept on disk. */
41
+ export const MAX_HISTORY_ENTRIES = 200;
42
+
43
+ export function getHistoryPath(piDir: string): string {
44
+ return join(piDir, HISTORY_FILE);
45
+ }
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Read / write
49
+ // ---------------------------------------------------------------------------
50
+
51
+ function readJsonArray(file: string): HistoryEntry[] {
52
+ try {
53
+ if (!existsSync(file)) return [];
54
+ const parsed = JSON.parse(readFileSync(file, "utf-8")) as unknown;
55
+ return Array.isArray(parsed) ? (parsed as HistoryEntry[]) : [];
56
+ } catch {
57
+ return [];
58
+ }
59
+ }
60
+
61
+ export function readHistory(piDir: string): HistoryEntry[] {
62
+ return readJsonArray(getHistoryPath(piDir));
63
+ }
64
+
65
+ export function appendHistory(piDir: string, entry: HistoryEntry): void {
66
+ const file = getHistoryPath(piDir);
67
+ const entries = readJsonArray(file);
68
+ // Upsert by id — if the id exists, replace it.
69
+ const idx = entries.findIndex((e) => e.id === entry.id);
70
+ if (idx >= 0) entries[idx] = entry;
71
+ else entries.push(entry);
72
+ writeHistory(piDir, entries);
73
+ // ponytail: bound the file inline — every append trims, so the file can
74
+ // never grow unbounded no matter which caller forgets to trim.
75
+ trimHistory(piDir, MAX_HISTORY_ENTRIES);
76
+ }
77
+
78
+ export function writeHistory(piDir: string, entries: HistoryEntry[]): void {
79
+ const file = getHistoryPath(piDir);
80
+ mkdirSync(dirname(file), { recursive: true });
81
+ writeFileSync(file, `${JSON.stringify(entries, null, 2)}\n`, "utf-8");
82
+ }
83
+
84
+ export function findHistory(piDir: string, id: string): HistoryEntry | undefined {
85
+ return readHistory(piDir).find((e) => e.id === id);
86
+ }
87
+
88
+ /**
89
+ * On session start, mark any `running` entries from a prior session as
90
+ * `interrupted`. We cannot resume them (in-process SDK sessions don't survive
91
+ * a restart). Honest about the architectural ceiling.
92
+ */
93
+ export function markInterruptedOnRestart(piDir: string): number {
94
+ const entries = readHistory(piDir);
95
+ let changed = 0;
96
+ for (const e of entries) {
97
+ if (e.status === "running") {
98
+ e.status = "interrupted";
99
+ changed++;
100
+ }
101
+ }
102
+ if (changed > 0) writeHistory(piDir, entries);
103
+ return changed;
104
+ }
105
+
106
+ /**
107
+ * Keep the history file bounded — trim to the most recent N entries by
108
+ * completedAt (or startedAt as fallback). Returns the new length.
109
+ */
110
+ export function trimHistory(piDir: string, maxEntries = 200): number {
111
+ const entries = readHistory(piDir);
112
+ if (entries.length <= maxEntries) return entries.length;
113
+ entries.sort((a, b) => (b.completedAt ?? b.startedAt) - (a.completedAt ?? a.startedAt));
114
+ const trimmed = entries.slice(0, maxEntries);
115
+ writeHistory(piDir, trimmed);
116
+ return trimmed.length;
117
+ }