@wizardingcode/shibaox-tui 0.1.3 → 0.1.4

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.
Files changed (64) hide show
  1. package/dist/main.js +8188 -0
  2. package/dist/main.js.map +67 -0
  3. package/package.json +8 -12
  4. package/src/app.tsx +0 -300
  5. package/src/component/dialogs/confirm.tsx +0 -29
  6. package/src/component/dialogs/diff.tsx +0 -143
  7. package/src/component/dialogs/help.tsx +0 -99
  8. package/src/component/dialogs/keys.tsx +0 -201
  9. package/src/component/dialogs/palette.tsx +0 -95
  10. package/src/component/dialogs/runs.tsx +0 -115
  11. package/src/component/dialogs/tiers.tsx +0 -240
  12. package/src/component/footer.tsx +0 -66
  13. package/src/component/logo.tsx +0 -29
  14. package/src/component/prompt/autocomplete.tsx +0 -44
  15. package/src/component/prompt/index.tsx +0 -213
  16. package/src/component/reconnecting.tsx +0 -44
  17. package/src/component/sidebar.tsx +0 -235
  18. package/src/component/tabs.tsx +0 -116
  19. package/src/component/too-small.tsx +0 -15
  20. package/src/context/client.tsx +0 -54
  21. package/src/context/commands.tsx +0 -44
  22. package/src/context/config.tsx +0 -22
  23. package/src/context/data-state.ts +0 -41
  24. package/src/context/data.tsx +0 -308
  25. package/src/context/exit.tsx +0 -13
  26. package/src/context/keys.tsx +0 -70
  27. package/src/context/poller.ts +0 -352
  28. package/src/context/prefs.tsx +0 -54
  29. package/src/context/route.tsx +0 -23
  30. package/src/main.tsx +0 -55
  31. package/src/model/format.ts +0 -38
  32. package/src/model/prompt-commands.ts +0 -315
  33. package/src/model/status.ts +0 -55
  34. package/src/model/stream.ts +0 -464
  35. package/src/motion/animation.ts +0 -235
  36. package/src/motion/config.tsx +0 -15
  37. package/src/motion/fade-in-text.tsx +0 -93
  38. package/src/motion/masked-text.ts +0 -62
  39. package/src/motion/one-cell-motion.ts +0 -100
  40. package/src/motion/pulse.ts +0 -38
  41. package/src/motion/shimmer-text.tsx +0 -78
  42. package/src/motion/spinner-renderable.ts +0 -171
  43. package/src/motion/spinner.tsx +0 -100
  44. package/src/motion/subcell.ts +0 -40
  45. package/src/routes/home.tsx +0 -365
  46. package/src/routes/session/approval-bar.tsx +0 -136
  47. package/src/routes/session/cards.tsx +0 -495
  48. package/src/routes/session/context-line.tsx +0 -129
  49. package/src/routes/session/continue.tsx +0 -104
  50. package/src/routes/session/index.tsx +0 -340
  51. package/src/routes/session/request.tsx +0 -87
  52. package/src/routes/session/timeline.tsx +0 -313
  53. package/src/testing/fake-client.ts +0 -249
  54. package/src/theme/context.tsx +0 -44
  55. package/src/theme/resolve.ts +0 -196
  56. package/src/theme/shibaox.json +0 -159
  57. package/src/ui/border.ts +0 -20
  58. package/src/ui/delayed-presence.ts +0 -23
  59. package/src/ui/dialog.tsx +0 -123
  60. package/src/ui/layout.ts +0 -23
  61. package/src/ui/marquee.ts +0 -75
  62. package/src/ui/pane-resize.ts +0 -77
  63. package/src/ui/toast.tsx +0 -203
  64. package/tsconfig.json +0 -19
@@ -1,352 +0,0 @@
1
- import type { RunState } from '@wizardingcode/shibaox-core';
2
- import type { Envelope, InboxId, SubmitRequest } from '@wizardingcode/shibaox-daemon';
3
- import { DaemonHttpError } from '@wizardingcode/shibaox-daemon/client';
4
- import type { SetStoreFunction } from 'solid-js/store';
5
- import { RUN_EVENT_REFRESH } from '../model/stream.js';
6
- import type { Feedback } from '../theme/resolve.js';
7
- import type { DaemonClientLike } from './client.js';
8
- import type { DataState } from './data-state.js';
9
-
10
- export interface PollerIntervals {
11
- fast: number;
12
- normal: number;
13
- slow: number;
14
- health: number;
15
- }
16
-
17
- export interface PollerToast {
18
- message: string;
19
- variant: Feedback;
20
- action?: { label: string; run: () => void };
21
- }
22
-
23
- /** Where the frames of a run go: a plain array per run, outside the reactive store. */
24
- export interface FrameSink {
25
- reset(runId: string): void;
26
- append(runId: string, batch: Envelope[]): void;
27
- }
28
-
29
- export interface PollerOptions {
30
- client: DaemonClientLike;
31
- set: SetStoreFunction<DataState>;
32
- get: () => DataState;
33
- frames: FrameSink;
34
- toast: (t: PollerToast) => void;
35
- /** Opens a run's tab (the toast's Open action). */
36
- open?: (runId: string) => void;
37
- /** Where stream failures and malformed frames are noted (`~/.shibaox/tui.log`). */
38
- log?: (line: string) => void;
39
- now?: () => number;
40
- intervals?: Partial<PollerIntervals>;
41
- }
42
-
43
- interface Sub {
44
- runId: string;
45
- controller: AbortController;
46
- cursor?: string;
47
- /** Frames received but not yet written to the store (flushed every FLUSH_MS). */
48
- pending: Envelope[];
49
- flushTimer?: NodeJS.Timeout;
50
- /** The stream closed without an `end` frame (daemon restart, socket error): reopen on the next tick. */
51
- broken?: boolean;
52
- }
53
-
54
- const DEFAULTS: PollerIntervals = { fast: 500, normal: 1000, slow: 5000, health: 5000 };
55
- const FAST_WINDOW_MS = 3000;
56
- const REFRESH_THROTTLE_MS = 200;
57
- /** Frames are written to the store in batches: one store update per burst, not per frame. */
58
- const FLUSH_MS = 50;
59
-
60
- /**
61
- * Feeds the data store: `listRuns` + `inbox` on a timer (faster after an action, slower while
62
- * the daemon is unreachable), `health` on its own timer, and one SSE subscription per open tab
63
- * (reopened with its cursor when the daemon comes back). Actions call the client and toast.
64
- */
65
- export class Poller {
66
- private readonly client: DaemonClientLike;
67
- private readonly set: SetStoreFunction<DataState>;
68
- private readonly get: () => DataState;
69
- private readonly frames: FrameSink;
70
- private readonly toast: (t: PollerToast) => void;
71
- private readonly open: (runId: string) => void;
72
- private readonly log: (line: string) => void;
73
- private readonly now: () => number;
74
- private readonly intervals: PollerIntervals;
75
- private tickTimer: NodeJS.Timeout | undefined;
76
- private healthTimer: NodeJS.Timeout | undefined;
77
- private readonly subs = new Map<string, Sub>();
78
- private readonly refreshTimers = new Map<string, NodeJS.Timeout>();
79
- private fastUntil = 0;
80
- private running = false;
81
- private ticking = false;
82
-
83
- constructor(o: PollerOptions) {
84
- this.client = o.client;
85
- this.set = o.set;
86
- this.get = o.get;
87
- this.frames = o.frames;
88
- this.toast = o.toast;
89
- this.open = o.open ?? (() => undefined);
90
- this.log = o.log ?? (() => undefined);
91
- this.now = o.now ?? (() => Date.now());
92
- this.intervals = { ...DEFAULTS, ...o.intervals };
93
- }
94
-
95
- start(): void {
96
- if (this.running) return;
97
- this.running = true;
98
- this.schedule(0);
99
- this.healthTimer = setInterval(() => void this.healthTick(), this.intervals.health);
100
- void this.healthTick();
101
- }
102
-
103
- stop(): void {
104
- this.running = false;
105
- if (this.tickTimer) clearTimeout(this.tickTimer);
106
- if (this.healthTimer) clearInterval(this.healthTimer);
107
- this.tickTimer = undefined;
108
- this.healthTimer = undefined;
109
- for (const t of this.refreshTimers.values()) clearTimeout(t);
110
- this.refreshTimers.clear();
111
- for (const runId of [...this.subs.keys()]) this.close(runId);
112
- }
113
-
114
- private schedule(delay: number): void {
115
- if (!this.running) return;
116
- if (this.tickTimer) clearTimeout(this.tickTimer);
117
- this.tickTimer = setTimeout(() => void this.tick(), delay);
118
- }
119
-
120
- private nextDelay(): number {
121
- if (!this.get().reachable) return this.intervals.slow;
122
- return this.now() < this.fastUntil ? this.intervals.fast : this.intervals.normal;
123
- }
124
-
125
- /** One `listRuns` + `inbox` round; reschedules itself while started. */
126
- async tick(): Promise<void> {
127
- if (this.ticking) return;
128
- this.ticking = true;
129
- try {
130
- const [runs, inbox] = await Promise.all([this.client.listRuns(), this.client.inbox()]);
131
- const wasDown = !this.get().reachable;
132
- this.set('reachable', true);
133
- this.set('unreachableSince', undefined);
134
- this.set('runs', runs);
135
- this.set('inbox', inbox);
136
- for (const sub of [...this.subs.values()]) if (wasDown || sub.broken) this.reopen(sub);
137
- } catch {
138
- if (this.get().reachable) {
139
- this.set('reachable', false);
140
- this.set('unreachableSince', this.now());
141
- }
142
- for (const sub of this.subs.values()) sub.controller.abort();
143
- } finally {
144
- this.ticking = false;
145
- this.schedule(this.nextDelay());
146
- }
147
- }
148
-
149
- async healthTick(): Promise<void> {
150
- try {
151
- this.set('health', await this.client.health());
152
- } catch {
153
- this.set('health', undefined);
154
- }
155
- }
156
-
157
- /** Opens the run's SSE stream (history first); a no-op when already subscribed. */
158
- subscribe(runId: string): void {
159
- if (this.subs.has(runId)) return;
160
- const sub: Sub = { runId, controller: new AbortController(), pending: [] };
161
- this.subs.set(runId, sub);
162
- if (this.get().reachable) void this.consume(sub);
163
- }
164
-
165
- unsubscribe(runId: string): void {
166
- this.close(runId);
167
- }
168
-
169
- private close(runId: string): void {
170
- const sub = this.subs.get(runId);
171
- if (!sub) return;
172
- this.flush(sub);
173
- sub.controller.abort();
174
- this.subs.delete(runId);
175
- }
176
-
177
- private flush(sub: Sub): void {
178
- if (sub.flushTimer) clearTimeout(sub.flushTimer);
179
- sub.flushTimer = undefined;
180
- if (sub.pending.length === 0) return;
181
- const batch = sub.pending;
182
- sub.pending = [];
183
- this.frames.append(sub.runId, batch);
184
- }
185
-
186
- private queue(sub: Sub, env: Envelope): void {
187
- sub.pending.push(env);
188
- if (!sub.flushTimer) sub.flushTimer = setTimeout(() => this.flush(sub), FLUSH_MS);
189
- }
190
-
191
- private reopen(old: Sub): void {
192
- this.flush(old);
193
- old.controller.abort();
194
- const sub: Sub = {
195
- runId: old.runId,
196
- controller: new AbortController(),
197
- cursor: old.cursor,
198
- pending: [],
199
- };
200
- this.subs.set(old.runId, sub);
201
- void this.consume(sub);
202
- }
203
-
204
- private async consume(sub: Sub): Promise<void> {
205
- const { runId, controller } = sub;
206
- const mine = () => this.subs.get(runId) === sub && !controller.signal.aborted;
207
- let ended = false;
208
- // a fresh subscription replays the history: start the run's frames over, and show the
209
- // run's nodes right away instead of waiting for its first event
210
- if (!sub.cursor) this.frames.reset(runId);
211
- await this.refreshRun(runId);
212
- if (!mine()) return;
213
- try {
214
- for await (const env of this.client.events(runId, {
215
- signal: controller.signal,
216
- since: sub.cursor,
217
- })) {
218
- if (!mine()) return;
219
- if (env.kind !== 'end') {
220
- sub.cursor = env.cursor;
221
- this.queue(sub, env);
222
- }
223
- if (env.kind === 'run' && RUN_EVENT_REFRESH.has(env.event.type)) this.refreshSoon(runId);
224
- if (env.kind === 'end') {
225
- ended = true;
226
- this.flush(sub);
227
- await this.refreshRun(runId);
228
- this.set('ended', runId, env.status);
229
- this.subs.delete(runId);
230
- if (this.get().active !== runId) {
231
- this.set('unread', runId, 'done');
232
- const failed = env.status !== 'completed';
233
- this.toast({
234
- message: `Run ${runId.slice(0, 8)} ${failed ? env.status : 'is done'}`,
235
- variant: failed ? 'error' : 'success',
236
- action: { label: 'Open', run: () => this.open(runId) },
237
- });
238
- }
239
- return;
240
- }
241
- }
242
- } catch (e) {
243
- // aborted, or the daemon went away: the poll loop notices and reopens
244
- if (!controller.signal.aborted)
245
- this.log(`stream ${runId}: ${e instanceof Error ? e.message : String(e)}`);
246
- } finally {
247
- // closed without an end frame while still ours: the next successful tick reopens it
248
- if (!ended && mine()) sub.broken = true;
249
- }
250
- }
251
-
252
- /** While the daemon is known as down, actions only say so (no call, no error toast). */
253
- private down(): boolean {
254
- if (this.get().reachable) return false;
255
- this.toast({ message: 'Daemon unreachable', variant: 'info' });
256
- return true;
257
- }
258
-
259
- private refreshSoon(runId: string): void {
260
- if (this.refreshTimers.has(runId)) return;
261
- this.refreshTimers.set(
262
- runId,
263
- setTimeout(() => {
264
- this.refreshTimers.delete(runId);
265
- void this.refreshRun(runId);
266
- }, REFRESH_THROTTLE_MS),
267
- );
268
- }
269
-
270
- /** Fetches the run's state; marks it unread as "needs" when it waits and is not active. */
271
- async refreshRun(runId: string): Promise<void> {
272
- try {
273
- const state = await this.client.getRun(runId);
274
- this.setState(state);
275
- } catch {
276
- // gone (404) or unreachable: the list refresh handles it
277
- }
278
- }
279
-
280
- private setState(state: RunState): void {
281
- const before = this.get().states[state.runId];
282
- // read through the store proxy before writing: it reflects the write immediately
283
- const waited = (before?.pendingApprovals.length ?? 0) + (before?.pendingHumans.length ?? 0) > 0;
284
- this.set('states', state.runId, state);
285
- const waits = state.pendingApprovals.length + state.pendingHumans.length > 0;
286
- if (waits && !waited && this.get().active !== state.runId)
287
- this.set('unread', state.runId, 'needs');
288
- }
289
-
290
- private toastError(e: unknown, fallback: string): void {
291
- if (e instanceof DaemonHttpError && e.status === 409 && /already answered/i.test(e.message)) {
292
- this.toast({ message: 'Already answered elsewhere', variant: 'info' });
293
- return;
294
- }
295
- const message = e instanceof Error ? e.message : String(e);
296
- this.toast({ message: message || fallback, variant: 'error' });
297
- }
298
-
299
- private afterAction(): void {
300
- this.fastUntil = this.now() + FAST_WINDOW_MS;
301
- this.schedule(0);
302
- }
303
-
304
- async answer(id: InboxId, approved: boolean, note?: string): Promise<void> {
305
- if (this.down()) return;
306
- try {
307
- await this.client.answer(id, { approved, note, via: 'cli' });
308
- this.toast({ message: approved ? 'Approved' : 'Denied', variant: 'success' });
309
- } catch (e) {
310
- this.toastError(e, 'Could not record the answer');
311
- }
312
- this.afterAction();
313
- }
314
-
315
- async cancel(runId: string): Promise<void> {
316
- if (this.down()) return;
317
- try {
318
- this.setState(await this.client.cancel(runId));
319
- this.toast({ message: `Run ${runId.slice(0, 8)} cancelled`, variant: 'success' });
320
- } catch (e) {
321
- this.toastError(e, 'Could not cancel the run');
322
- }
323
- this.afterAction();
324
- }
325
-
326
- async resume(runId: string, budgetUsd?: number): Promise<void> {
327
- if (this.down()) return;
328
- try {
329
- this.setState(await this.client.resume(runId, { budgetUsd }));
330
- this.toast({ message: `Run ${runId.slice(0, 8)} resumed`, variant: 'success' });
331
- } catch (e) {
332
- this.toastError(e, 'Could not resume the run');
333
- }
334
- this.afterAction();
335
- }
336
-
337
- /** Submits a run; returns its id, or undefined on failure (toast shown). */
338
- async submit(req: SubmitRequest): Promise<string | undefined> {
339
- if (this.down()) return undefined;
340
- try {
341
- const { runId, warnings } = await this.client.submitRun(req);
342
- const note = warnings.length ? ` (${warnings.join('; ')})` : '';
343
- this.toast({ message: `Run ${runId.slice(0, 8)} queued${note}`, variant: 'success' });
344
- this.afterAction();
345
- return runId;
346
- } catch (e) {
347
- this.toastError(e, 'Could not submit the run');
348
- this.afterAction();
349
- return undefined;
350
- }
351
- }
352
- }
@@ -1,54 +0,0 @@
1
- import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { createContext, type JSX, type ParentProps, useContext } from 'solid-js';
4
- import { createStore } from 'solid-js/store';
5
-
6
- export interface Prefs {
7
- lastOrg?: string;
8
- lastAdapter?: string;
9
- lastWorkflow?: string;
10
- /** The model chosen with `/model` (`provider/model`). */
11
- lastModel?: string;
12
- animations?: boolean;
13
- sidebarWidth?: number;
14
- sidebar?: 'auto' | 'hide';
15
- }
16
-
17
- const FILE = 'ui.json';
18
-
19
- /** `~/.shibaox/ui.json`; unreadable or malformed means empty. */
20
- export function loadPrefs(home: string): Prefs {
21
- try {
22
- const raw = JSON.parse(readFileSync(join(home, FILE), 'utf8')) as unknown;
23
- return raw && typeof raw === 'object' ? (raw as Prefs) : {};
24
- } catch {
25
- return {};
26
- }
27
- }
28
-
29
- export function savePrefs(home: string, p: Prefs): void {
30
- try {
31
- mkdirSync(home, { recursive: true });
32
- writeFileSync(join(home, FILE), `${JSON.stringify(p, null, 2)}\n`);
33
- } catch {
34
- // preferences are a convenience; never fail the UI over them
35
- }
36
- }
37
-
38
- const Context = createContext<{ data: Prefs; update: (patch: Partial<Prefs>) => void }>();
39
-
40
- export function PrefsProvider(props: ParentProps<{ home: string }>): JSX.Element {
41
- const [data, set] = createStore<Prefs>(loadPrefs(props.home));
42
- const update = (patch: Partial<Prefs>) => {
43
- set(patch);
44
- const next = Object.fromEntries(Object.entries({ ...data }).filter(([, v]) => v !== undefined));
45
- savePrefs(props.home, next);
46
- };
47
- return <Context.Provider value={{ data, update }}>{props.children}</Context.Provider>;
48
- }
49
-
50
- export function usePrefs(): { data: Prefs; update: (patch: Partial<Prefs>) => void } {
51
- const c = useContext(Context);
52
- if (!c) throw new Error('usePrefs outside PrefsProvider');
53
- return c;
54
- }
@@ -1,23 +0,0 @@
1
- import {
2
- type Accessor,
3
- createContext,
4
- createSignal,
5
- type JSX,
6
- type ParentProps,
7
- useContext,
8
- } from 'solid-js';
9
-
10
- export type Route = { type: 'home' } | { type: 'session'; runId: string };
11
-
12
- const Context = createContext<{ data: Accessor<Route>; navigate: (r: Route) => void }>();
13
-
14
- export function RouteProvider(props: ParentProps<{ initial?: Route }>): JSX.Element {
15
- const [data, setData] = createSignal<Route>(props.initial ?? { type: 'home' });
16
- return <Context.Provider value={{ data, navigate: setData }}>{props.children}</Context.Provider>;
17
- }
18
-
19
- export function useRoute(): { data: Accessor<Route>; navigate: (r: Route) => void } {
20
- const c = useContext(Context);
21
- if (!c) throw new Error('useRoute outside RouteProvider');
22
- return c;
23
- }
package/src/main.tsx DELETED
@@ -1,55 +0,0 @@
1
- /**
2
- * The Bun entry the CLI spawns (from the apps/tui root, with @opentui/solid/preload):
3
- * bun --preload @opentui/solid/preload src/main.tsx dashboard --socket <path> --home <dir> --version <v> [--cwd <dir>]
4
- * bun --preload @opentui/solid/preload src/main.tsx stream <runId> --socket <path> --home <dir>
5
- */
6
- import { appendFileSync } from 'node:fs';
7
- import { join } from 'node:path';
8
- import { DaemonClient } from '@wizardingcode/shibaox-daemon/client';
9
- import { runDashboard, runStream } from './app.js';
10
-
11
- function arg(name: string): string | undefined {
12
- const i = process.argv.indexOf(`--${name}`);
13
- return i >= 0 ? process.argv[i + 1] : undefined;
14
- }
15
-
16
- const mode = process.argv[2];
17
- const socket = arg('socket');
18
- const home = arg('home');
19
- if (!socket || !home || (mode !== 'dashboard' && mode !== 'stream')) {
20
- console.error(
21
- 'usage: main.tsx dashboard|stream [runId] --socket <path> --home <dir> [--version <v>] [--cwd <dir>]',
22
- );
23
- process.exit(2);
24
- }
25
- const client = new DaemonClient(socket);
26
- const log = (line: string) => {
27
- try {
28
- appendFileSync(join(home, 'tui.log'), `${new Date().toISOString()} ${line}\n`);
29
- } catch {
30
- // the log is a convenience; never fail the UI over it
31
- }
32
- };
33
- if (mode === 'dashboard') {
34
- const code = await runDashboard(client, {
35
- version: arg('version') ?? '0.0.0',
36
- home,
37
- cwd: arg('cwd'),
38
- log,
39
- });
40
- process.exit(code);
41
- } else {
42
- const runId = process.argv[3];
43
- if (!runId) {
44
- console.error('usage: main.tsx stream <runId> ...');
45
- process.exit(2);
46
- }
47
- const r = await runStream(client, runId, {
48
- version: arg('version') ?? '0.0.0',
49
- home,
50
- cwd: arg('cwd'),
51
- log,
52
- });
53
- if (r.message) console.log(r.message);
54
- process.exit(r.code);
55
- }
@@ -1,38 +0,0 @@
1
- /** `$0.0020` under a dollar, `$1.50` from a dollar up. */
2
- export const money = (usd: number): string =>
3
- usd < 1 ? `$${usd.toFixed(4)}` : `$${usd.toFixed(2)}`;
4
-
5
- /** `41 s`, `2m 13s`, `1h 04m`. */
6
- export const duration = (ms: number): string => {
7
- const s = Math.floor(ms / 1000);
8
- if (s < 60) return `${s} s`;
9
- const m = Math.floor(s / 60);
10
- if (m < 60) return `${m}m ${String(s % 60).padStart(2, '0')}s`;
11
- const h = Math.floor(m / 60);
12
- return `${h}h ${String(m % 60).padStart(2, '0')}m`;
13
- };
14
-
15
- /** How long ago an ISO timestamp was: `12 s`, `3 m`, `2 h`, `4 d`. */
16
- export const age = (iso: string, now = Date.now()): string => {
17
- const s = Math.max(0, Math.floor((now - Date.parse(iso)) / 1000));
18
- if (s < 60) return `${s} s`;
19
- const m = Math.floor(s / 60);
20
- if (m < 60) return `${m} m`;
21
- const h = Math.floor(m / 60);
22
- if (h < 24) return `${h} h`;
23
- return `${Math.floor(h / 24)} d`;
24
- };
25
-
26
- export const shortId = (id: string): string => id.slice(0, 8);
27
-
28
- /** `~/dir` for paths under the home directory. */
29
- export function tilde(p: string, home: string | undefined = process.env.HOME): string {
30
- return home && p.startsWith(home) ? `~${p.slice(home.length)}` : p;
31
- }
32
-
33
- /** `24.0k` / `1.2M` for token counts. */
34
- export function tokens(n: number): string {
35
- if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
36
- if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
37
- return String(n);
38
- }