pi-diff-widget 0.0.0-stage → 0.1.1

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/src/index.ts ADDED
@@ -0,0 +1,80 @@
1
+ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
2
+ import { collectUncommitted, type DiffFile } from './git.ts';
3
+ import type { ExecFn } from './exec.ts';
4
+ import { createBaselineTracker } from './baseline.ts';
5
+ import { collectTurns, createWriteDiffTracker } from './turns.ts';
6
+ import { DiffViewer } from './ui.ts';
7
+ import { toTurnViews, toUncommittedSections, type ViewMode } from './view.ts';
8
+ import { registerDiffWidget } from './widget.ts';
9
+
10
+ export default function (pi: ExtensionAPI) {
11
+ const lookupWrite = createWriteDiffTracker(pi);
12
+ const baselines = createBaselineTracker(pi);
13
+ const exec: ExecFn = (command, args, options) =>
14
+ pi.exec(command, args, options);
15
+
16
+ registerDiffWidget(pi, baselines, exec);
17
+
18
+ pi.registerCommand('diff', {
19
+ description:
20
+ 'Show uncommitted changes and per-turn edits in a scrollable viewer',
21
+ handler: async (_args, ctx) => {
22
+ if (ctx.mode !== 'tui') {
23
+ ctx.ui.notify('/diff needs the interactive TUI', 'warning');
24
+ return;
25
+ }
26
+
27
+ await ctx.waitForIdle();
28
+
29
+ let files: DiffFile[] = [];
30
+ let gitError: string | undefined;
31
+ try {
32
+ files = await collectUncommitted(exec, ctx.cwd);
33
+ } catch (error) {
34
+ gitError =
35
+ error instanceof Error ? error.message : String(error);
36
+ }
37
+
38
+ const turns = toTurnViews(
39
+ collectTurns(ctx.sessionManager, lookupWrite),
40
+ );
41
+
42
+ const uncommitted = toUncommittedSections(files);
43
+
44
+ if (uncommitted.length === 0 && turns.length === 0) {
45
+ if (gitError) ctx.ui.notify(`/diff: ${gitError}`, 'error');
46
+ else
47
+ ctx.ui.notify(
48
+ 'No uncommitted changes and no edits this session',
49
+ 'info',
50
+ );
51
+ return;
52
+ }
53
+
54
+ const initialMode: ViewMode =
55
+ uncommitted.length === 0 ? 'turns' : 'uncommitted';
56
+
57
+ await ctx.ui.custom<void>(
58
+ (tui, theme, _keybindings, done) =>
59
+ new DiffViewer({
60
+ title: 'pi diff',
61
+ uncommitted,
62
+ turns,
63
+ initialMode,
64
+ tui,
65
+ theme,
66
+ onClose: () => done(undefined),
67
+ }),
68
+ {
69
+ overlay: true,
70
+ overlayOptions: {
71
+ width: '100%',
72
+ maxHeight: '100%',
73
+ margin: 0,
74
+ anchor: 'center',
75
+ },
76
+ },
77
+ );
78
+ },
79
+ });
80
+ }
package/src/summary.ts ADDED
@@ -0,0 +1,117 @@
1
+ import { mkdir, unlink, writeFile } from 'node:fs/promises';
2
+ import { dirname, relative } from 'node:path';
3
+ import { generateDiffString } from '@earendil-works/pi-coding-agent';
4
+ import { readFileCapped, locationGone, type BaselineTracker } from './baseline.ts';
5
+ import { listDirtyPaths } from './git.ts';
6
+ import type { ExecFn } from './exec.ts';
7
+ import { summarizeDiffs, type EditSummary } from './view.ts';
8
+
9
+ /**
10
+ * Current content for a tracked path, or undefined when it cannot be accounted
11
+ * for: too large, unreadable, or its whole location is gone.
12
+ */
13
+ async function readCurrent(absolute: string): Promise<string | undefined> {
14
+ if (await locationGone(absolute)) return undefined;
15
+ return readFileCapped(absolute);
16
+ }
17
+
18
+ /**
19
+ * Net agent changes: for each file the agent touched, diff its current content
20
+ * against the baseline captured before the first touch. Reverted files vanish.
21
+ */
22
+ export async function computeNetSummary(
23
+ baselines: BaselineTracker,
24
+ cwd: string,
25
+ ): Promise<EditSummary> {
26
+ const diffs: Array<{ path: string; diff: string }> = [];
27
+
28
+ for (const absolute of baselines.paths()) {
29
+ const current = await readCurrent(absolute);
30
+ if (current === undefined) continue;
31
+ const baseline = baselines.get(absolute) ?? '';
32
+ const { diff } = generateDiffString(baseline, current);
33
+ if (!diff) continue;
34
+
35
+ const rel = relative(cwd, absolute);
36
+ diffs.push({ path: rel.startsWith('..') ? absolute : rel, diff });
37
+ }
38
+
39
+ return summarizeDiffs(diffs);
40
+ }
41
+
42
+ export interface RevertTarget {
43
+ absolutePath: string;
44
+ relativePath: string;
45
+ baseline: string;
46
+ missing: boolean;
47
+ }
48
+
49
+ /** Files whose current content differs from their baseline. */
50
+ export async function collectRevertTargets(
51
+ baselines: BaselineTracker,
52
+ cwd: string,
53
+ ): Promise<RevertTarget[]> {
54
+ const targets: RevertTarget[] = [];
55
+ for (const absolute of baselines.paths()) {
56
+ const current = await readCurrent(absolute);
57
+ if (current === undefined) continue;
58
+ const baseline = baselines.get(absolute) ?? '';
59
+ const { diff } = generateDiffString(baseline, current);
60
+ if (!diff) continue;
61
+ const rel = relative(cwd, absolute);
62
+ targets.push({
63
+ absolutePath: absolute,
64
+ relativePath: rel.startsWith('..') ? absolute : rel,
65
+ baseline,
66
+ missing: baselines.wasMissing(absolute),
67
+ });
68
+ }
69
+ return targets;
70
+ }
71
+
72
+ /**
73
+ * Restore files to their baselines: write the original content back, or delete
74
+ * files the agent created. The caller confirms first, since this is destructive.
75
+ */
76
+ export async function revertTargets(targets: RevertTarget[]): Promise<void> {
77
+ for (const target of targets) {
78
+ if (target.missing) {
79
+ await unlink(target.absolutePath).catch(() => undefined);
80
+ continue;
81
+ }
82
+ await mkdir(dirname(target.absolutePath), { recursive: true });
83
+ await writeFile(target.absolutePath, target.baseline, 'utf8');
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Drop files that no longer have uncommitted changes, so committing clears the
89
+ * panel. Files outside the repository, or sessions outside git, are kept.
90
+ */
91
+ export async function pruneCleanBaselines(
92
+ baselines: BaselineTracker,
93
+ exec: ExecFn,
94
+ cwd: string,
95
+ ): Promise<void> {
96
+ // A baseline whose location is gone can never be shown or reverted again,
97
+ // and keeping it would let reject rebuild a directory that was moved away.
98
+ const live = new Set<string>();
99
+ for (const absolute of baselines.paths()) {
100
+ if (!(await locationGone(absolute))) live.add(absolute);
101
+ }
102
+ baselines.retain((absolute) => live.has(absolute));
103
+
104
+ let dirty: Set<string> | undefined;
105
+ try {
106
+ dirty = await listDirtyPaths(exec, cwd);
107
+ } catch {
108
+ return;
109
+ }
110
+ if (!dirty) return;
111
+
112
+ baselines.retain((absolute) => {
113
+ const rel = relative(cwd, absolute);
114
+ if (rel.startsWith('..')) return true; // outside the repo, cannot judge
115
+ return dirty.has(rel);
116
+ });
117
+ }
package/src/turns.ts ADDED
@@ -0,0 +1,221 @@
1
+ import { readFile, stat } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ import {
4
+ generateDiffString,
5
+ isToolCallEventType,
6
+ isWriteToolResult,
7
+ } from '@earendil-works/pi-coding-agent';
8
+ import type {
9
+ ExtensionAPI,
10
+ ExtensionContext,
11
+ SessionEntry,
12
+ } from '@earendil-works/pi-coding-agent';
13
+
14
+ export interface TurnEdit {
15
+ path: string;
16
+ /** Raw display diff in pi's line-numbered format (`+123 text`). Render with `renderDiff`. */
17
+ diff: string;
18
+ }
19
+
20
+ export interface Turn {
21
+ prompt: string;
22
+ edits: TurnEdit[];
23
+ }
24
+
25
+ export interface WriteDiff {
26
+ path: string;
27
+ diff: string;
28
+ }
29
+
30
+ /** Session entry type used to persist write diffs across reloads. */
31
+ export const WRITE_DIFF_ENTRY = 'pi-diff:writes';
32
+
33
+ export interface WriteDiffRecord extends WriteDiff {
34
+ toolCallId: string;
35
+ }
36
+
37
+ /** Maps a `write` tool call id to the diff its execution produced. */
38
+ export type WriteDiffLookup = (toolCallId: string) => WriteDiff | undefined;
39
+
40
+ /** The subset of the session manager `collectTurns` reads. */
41
+ export type SessionBranchSource = Pick<
42
+ ExtensionContext['sessionManager'],
43
+ 'getBranch'
44
+ >;
45
+
46
+ const MAX_TRACKED_WRITES = 2000;
47
+ const MAX_TURNS = 100;
48
+ const MAX_SNAPSHOT_BYTES = 1024 * 1024;
49
+
50
+ type SnapshotRead =
51
+ | { kind: 'text'; text: string }
52
+ | { kind: 'missing' }
53
+ | { kind: 'too-large' };
54
+
55
+ async function readSnapshot(path: string): Promise<SnapshotRead> {
56
+ const info = await stat(path).catch(() => undefined);
57
+ if (!info || !info.isFile()) return { kind: 'missing' };
58
+ if (info.size > MAX_SNAPSHOT_BYTES) return { kind: 'too-large' };
59
+ const text = await readFile(path, 'utf8').catch(() => undefined);
60
+ return text === undefined ? { kind: 'missing' } : { kind: 'text', text };
61
+ }
62
+
63
+ function extractText(content: unknown): string {
64
+ if (typeof content === 'string') return content;
65
+ if (!Array.isArray(content)) return '';
66
+ return content
67
+ .filter(
68
+ (block): block is { type: 'text'; text: string } =>
69
+ typeof block === 'object' &&
70
+ block !== null &&
71
+ (block as { type?: unknown }).type === 'text',
72
+ )
73
+ .map((block) => block.text)
74
+ .join(' ')
75
+ .trim();
76
+ }
77
+
78
+ /**
79
+ * `write` results carry no diff, so snapshot the old content and synthesize one
80
+ * after the write, keyed by tool call id. Diffs are persisted per turn.
81
+ */
82
+ export function createWriteDiffTracker(pi: ExtensionAPI): WriteDiffLookup {
83
+ const pending = new Map<string, { path: string; before?: string }>();
84
+ const diffs = new Map<string, WriteDiff>();
85
+ let unflushed: string[] = [];
86
+
87
+ const flush = () => {
88
+ if (unflushed.length === 0) return;
89
+ const records: WriteDiffRecord[] = [];
90
+ for (const toolCallId of unflushed) {
91
+ const diff = diffs.get(toolCallId);
92
+ if (diff) records.push({ toolCallId, ...diff });
93
+ }
94
+ unflushed = [];
95
+ if (records.length > 0) {
96
+ pi.appendEntry(WRITE_DIFF_ENTRY, { diffs: records });
97
+ }
98
+ };
99
+
100
+ // Persist at turn boundaries so write diffs survive a session reload. At
101
+ // turn_end no tool result is in flight, so it is safe to append an entry.
102
+ pi.on('turn_end', flush);
103
+ pi.on('agent_settled', () => {
104
+ // A write without a matching result (aborted run) would otherwise linger.
105
+ pending.clear();
106
+ flush();
107
+ });
108
+
109
+ pi.on('tool_call', async (event, ctx) => {
110
+ if (!isToolCallEventType('write', event)) return;
111
+ const read = await readSnapshot(resolve(ctx.cwd, event.input.path));
112
+ if (read.kind === 'too-large') return; // don't read or diff oversized files
113
+ pending.set(event.toolCallId, {
114
+ path: event.input.path,
115
+ before: read.kind === 'text' ? read.text : undefined,
116
+ });
117
+ });
118
+
119
+ pi.on('tool_result', async (event, ctx) => {
120
+ if (!isWriteToolResult(event)) return;
121
+ const snapshot = pending.get(event.toolCallId);
122
+ if (!snapshot) return;
123
+ pending.delete(event.toolCallId);
124
+
125
+ const read = await readSnapshot(resolve(ctx.cwd, snapshot.path));
126
+ const after = read.kind === 'text' ? read.text : '';
127
+ const { diff } = generateDiffString(snapshot.before ?? '', after);
128
+ if (!diff) return;
129
+
130
+ diffs.set(event.toolCallId, { path: snapshot.path, diff });
131
+ unflushed.push(event.toolCallId);
132
+ while (diffs.size > MAX_TRACKED_WRITES) {
133
+ const oldest = diffs.keys().next().value;
134
+ if (oldest === undefined) break;
135
+ diffs.delete(oldest);
136
+ }
137
+ });
138
+
139
+ return (toolCallId) => diffs.get(toolCallId);
140
+ }
141
+
142
+ /** Collect write diffs persisted by past turns. */
143
+ function readPersistedWrites(branch: SessionEntry[]): Map<string, WriteDiff> {
144
+ const writes = new Map<string, WriteDiff>();
145
+ for (const entry of branch) {
146
+ if (entry.type !== 'custom' || entry.customType !== WRITE_DIFF_ENTRY) {
147
+ continue;
148
+ }
149
+ const records = (entry.data as { diffs?: WriteDiffRecord[] } | undefined)
150
+ ?.diffs;
151
+ if (!Array.isArray(records)) continue;
152
+ for (const record of records) {
153
+ writes.set(record.toolCallId, {
154
+ path: record.path,
155
+ diff: record.diff,
156
+ });
157
+ }
158
+ }
159
+ return writes;
160
+ }
161
+
162
+ /**
163
+ * Group edits by user message from persisted session data. No git, no filesystem.
164
+ * `edit` results already carry `details.diff`; `write` results use `lookupWrite`.
165
+ */
166
+ export function collectTurns(
167
+ session: SessionBranchSource,
168
+ lookupWrite: WriteDiffLookup,
169
+ ): Turn[] {
170
+ const branch = session.getBranch();
171
+ const persistedWrites = readPersistedWrites(branch);
172
+
173
+ const callArguments = new Map<string, Record<string, unknown>>();
174
+ for (const entry of branch) {
175
+ if (entry.type !== 'message' || entry.message.role !== 'assistant')
176
+ continue;
177
+ for (const block of entry.message.content) {
178
+ if (block.type === 'toolCall')
179
+ callArguments.set(block.id, block.arguments ?? {});
180
+ }
181
+ }
182
+
183
+ const turns: Turn[] = [];
184
+ let current: Turn | undefined;
185
+
186
+ for (const entry of branch) {
187
+ if (entry.type !== 'message') continue;
188
+ const message = entry.message;
189
+
190
+ if (message.role === 'user') {
191
+ current = {
192
+ prompt: extractText(message.content),
193
+ edits: [],
194
+ };
195
+ turns.push(current);
196
+ continue;
197
+ }
198
+
199
+ if (message.role !== 'toolResult') continue;
200
+
201
+ const args = callArguments.get(message.toolCallId);
202
+ const path = typeof args?.path === 'string' ? args.path : '(unknown)';
203
+
204
+ if (message.toolName === 'edit') {
205
+ const diff = (message.details as { diff?: string } | undefined)
206
+ ?.diff;
207
+ if (diff) current?.edits.push({ path, diff });
208
+ continue;
209
+ }
210
+
211
+ if (message.toolName === 'write') {
212
+ const tracked =
213
+ lookupWrite(message.toolCallId) ??
214
+ persistedWrites.get(message.toolCallId);
215
+ if (tracked)
216
+ current?.edits.push({ path: tracked.path, diff: tracked.diff });
217
+ }
218
+ }
219
+
220
+ return turns.length > MAX_TURNS ? turns.slice(-MAX_TURNS) : turns;
221
+ }
package/src/ui.ts ADDED
@@ -0,0 +1,234 @@
1
+ import { Key, matchesKey, truncateToWidth } from '@earendil-works/pi-tui';
2
+ import type { Component, TUI } from '@earendil-works/pi-tui';
3
+ import { renderDiff } from '@earendil-works/pi-coding-agent';
4
+ import type { DiffSection, TurnView, ViewMode } from './view.ts';
5
+ import {
6
+ buildDocument,
7
+ type DiffTextRenderer,
8
+ type DocumentTheme,
9
+ } from './document.ts';
10
+
11
+ export interface DiffViewerOptions {
12
+ title: string;
13
+ uncommitted: DiffSection[];
14
+ turns: TurnView[];
15
+ initialMode: ViewMode;
16
+ tui: TUI;
17
+ theme: DocumentTheme;
18
+ onClose: () => void;
19
+ /** Defaults to pi's `renderDiff`, which uses pi's active theme. */
20
+ renderDiffText?: DiffTextRenderer;
21
+ }
22
+
23
+ /**
24
+ * Full-screen scrollable diff view. Renders one source at a time: uncommitted
25
+ * changes or the edits of a single turn. `Tab` switches source, `[` and `]`
26
+ * walk turns. Used as an overlay, so it reads its own height from the terminal.
27
+ */
28
+ export class DiffViewer implements Component {
29
+ private readonly title: string;
30
+ private readonly uncommitted: DiffSection[];
31
+ private readonly turns: TurnView[];
32
+ private readonly tui: TUI;
33
+ private readonly theme: DocumentTheme;
34
+ private readonly renderDiffText: DiffTextRenderer;
35
+ private readonly onClose: () => void;
36
+
37
+ private mode: ViewMode;
38
+ private turnIndex: number;
39
+ private lines: string[] = [];
40
+ private sectionStarts: number[] = [];
41
+ private offset = 0;
42
+ private viewportHeight = 1;
43
+
44
+ constructor(options: DiffViewerOptions) {
45
+ this.title = options.title;
46
+ this.uncommitted = options.uncommitted;
47
+ this.turns = options.turns;
48
+ this.tui = options.tui;
49
+ this.theme = options.theme;
50
+ this.renderDiffText = options.renderDiffText ?? renderDiff;
51
+ this.onClose = options.onClose;
52
+ this.mode = options.initialMode;
53
+ this.turnIndex = Math.max(0, options.turns.length - 1);
54
+ this.rebuild();
55
+ }
56
+
57
+ private currentSections(): DiffSection[] {
58
+ if (this.mode === 'turns')
59
+ return this.turns[this.turnIndex]?.sections ?? [];
60
+ return this.uncommitted;
61
+ }
62
+
63
+ private rebuild(): void {
64
+ const document = buildDocument(
65
+ this.currentSections(),
66
+ this.theme,
67
+ this.renderDiffText,
68
+ );
69
+ this.lines = document.lines;
70
+ this.sectionStarts = document.sectionStarts;
71
+ }
72
+
73
+ private headerText(): string {
74
+ if (this.mode === 'turns') {
75
+ const counter = `[${this.turnIndex + 1}/${this.turns.length}]`;
76
+ const label = this.turns[this.turnIndex]?.label ?? '';
77
+ return `${this.title} · turns ${this.theme.fg('dim', `${counter} ${label}`)}`;
78
+ }
79
+ return `${this.title} · uncommitted ${this.theme.fg('dim', `[${this.uncommitted.length} files]`)}`;
80
+ }
81
+
82
+ private footerHint(): string {
83
+ const parts: string[] = [];
84
+ if (this.mode === 'turns') {
85
+ parts.push('[ / ] turn');
86
+ if (this.uncommitted.length > 0) parts.push('Tab uncommitted');
87
+ } else if (this.turns.length > 0) {
88
+ parts.push('Tab turns');
89
+ }
90
+ parts.push('up/down scroll', 'n/p section', 'g/G ends', 'q close');
91
+ return parts.join(' ');
92
+ }
93
+
94
+ private clamp(): void {
95
+ const max = Math.max(0, this.lines.length - this.viewportHeight);
96
+ this.offset = Math.max(0, Math.min(this.offset, max));
97
+ }
98
+
99
+ private currentSectionIndex(): number {
100
+ let index = 0;
101
+ for (let i = 0; i < this.sectionStarts.length; i++) {
102
+ if ((this.sectionStarts[i] ?? 0) <= this.offset) index = i;
103
+ }
104
+ return index;
105
+ }
106
+
107
+ private scroll(delta: number): void {
108
+ this.offset += delta;
109
+ this.clamp();
110
+ this.tui.requestRender();
111
+ }
112
+
113
+ private goTo(line: number): void {
114
+ this.offset = line;
115
+ this.clamp();
116
+ this.tui.requestRender();
117
+ }
118
+
119
+ private goToSection(direction: number): void {
120
+ const next = this.currentSectionIndex() + direction;
121
+ if (next < 0 || next >= this.sectionStarts.length) return;
122
+ this.goTo(this.sectionStarts[next] ?? 0);
123
+ }
124
+
125
+ private switchMode(): void {
126
+ if (this.mode === 'turns') {
127
+ this.mode = 'uncommitted';
128
+ } else {
129
+ if (this.turns.length === 0) return;
130
+ this.mode = 'turns';
131
+ this.turnIndex = this.turns.length - 1;
132
+ }
133
+ this.offset = 0;
134
+ this.rebuild();
135
+ this.tui.requestRender();
136
+ }
137
+
138
+ private stepTurn(direction: number): void {
139
+ const next = this.turnIndex + direction;
140
+ if (next < 0 || next >= this.turns.length) return;
141
+ this.turnIndex = next;
142
+ this.offset = 0;
143
+ this.rebuild();
144
+ this.tui.requestRender();
145
+ }
146
+
147
+ render(width: number): string[] {
148
+ const height = Math.max(6, this.tui.terminal.rows - 1);
149
+ this.viewportHeight = Math.max(1, height - 2);
150
+ this.clamp();
151
+
152
+ const out: string[] = [truncateToWidth(this.headerText(), width)];
153
+
154
+ for (let i = 0; i < this.viewportHeight; i++) {
155
+ const line = this.lines[this.offset + i];
156
+ out.push(line === undefined ? '' : truncateToWidth(line, width));
157
+ }
158
+
159
+ const last = Math.min(
160
+ this.offset + this.viewportHeight,
161
+ this.lines.length,
162
+ );
163
+ const range =
164
+ this.lines.length === 0
165
+ ? '0/0'
166
+ : `${this.offset + 1}-${last}/${this.lines.length}`;
167
+ out.push(
168
+ truncateToWidth(
169
+ this.theme.fg('dim', `${this.footerHint()} ${range}`),
170
+ width,
171
+ ),
172
+ );
173
+
174
+ return out;
175
+ }
176
+
177
+ handleInput(data: string): void {
178
+ if (
179
+ matchesKey(data, Key.escape) ||
180
+ matchesKey(data, Key.ctrl('c')) ||
181
+ data === 'q'
182
+ ) {
183
+ this.onClose();
184
+ return;
185
+ }
186
+ if (matchesKey(data, Key.tab)) {
187
+ this.switchMode();
188
+ return;
189
+ }
190
+ if (matchesKey(data, Key.up)) {
191
+ this.scroll(-1);
192
+ return;
193
+ }
194
+ if (matchesKey(data, Key.down)) {
195
+ this.scroll(1);
196
+ return;
197
+ }
198
+ if (matchesKey(data, Key.pageUp)) {
199
+ this.scroll(-this.viewportHeight);
200
+ return;
201
+ }
202
+ if (matchesKey(data, Key.pageDown)) {
203
+ this.scroll(this.viewportHeight);
204
+ return;
205
+ }
206
+ if (this.mode === 'turns' && data === '[') {
207
+ this.stepTurn(-1);
208
+ return;
209
+ }
210
+ if (this.mode === 'turns' && data === ']') {
211
+ this.stepTurn(1);
212
+ return;
213
+ }
214
+ if (data === 'n') {
215
+ this.goToSection(1);
216
+ return;
217
+ }
218
+ if (data === 'p') {
219
+ this.goToSection(-1);
220
+ return;
221
+ }
222
+ if (data === 'g') {
223
+ this.goTo(0);
224
+ return;
225
+ }
226
+ if (data === 'G') {
227
+ this.goTo(this.lines.length);
228
+ }
229
+ }
230
+
231
+ invalidate(): void {
232
+ this.rebuild();
233
+ }
234
+ }