@huseynsnmz/pi-suite 0.0.0-stage → 0.2.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 +28 -0
- package/LICENSE +21 -0
- package/README.md +88 -2
- package/SECURITY.md +5 -0
- package/package.json +112 -4
- package/packages/board/CHANGELOG.md +340 -0
- package/packages/board/CONTRIBUTING.md +7 -0
- package/packages/board/LICENSE +21 -0
- package/packages/board/README.md +88 -0
- package/packages/board/SECURITY.md +19 -0
- package/packages/board/docs/how-it-works.md +31 -0
- package/packages/board/docs/reference.md +278 -0
- package/packages/board/package.json +81 -0
- package/packages/board/runner/job-runner-legacy.mjs +68 -0
- package/packages/board/runner/job-runner.mjs +834 -0
- package/packages/board/runner/pty-runner-legacy.mjs +50 -0
- package/packages/board/runner/pty-runner.mjs +2175 -0
- package/packages/board/runner/state-coordinator.mjs +673 -0
- package/packages/board/runner/state-runner.mjs +203 -0
- package/packages/board/runner/title-runner.mjs +80 -0
- package/packages/board/src/commands/agent-board.ts +394 -0
- package/packages/board/src/commands/attach-decision.mjs +66 -0
- package/packages/board/src/commands/attach-flow.ts +251 -0
- package/packages/board/src/commands/bg.ts +103 -0
- package/packages/board/src/commands/board-model.ts +21 -0
- package/packages/board/src/core/atomic.mjs +184 -0
- package/packages/board/src/core/attach-delete.mjs +24 -0
- package/packages/board/src/core/attach-frame-proof.mjs +870 -0
- package/packages/board/src/core/attach-frame-reporter.mjs +648 -0
- package/packages/board/src/core/attach-frame-stream.mjs +266 -0
- package/packages/board/src/core/attach-view-proof.mjs +709 -0
- package/packages/board/src/core/auto-state.mjs +377 -0
- package/packages/board/src/core/background-runs.mjs +273 -0
- package/packages/board/src/core/code-refs-store.mjs +315 -0
- package/packages/board/src/core/code-refs.mjs +913 -0
- package/packages/board/src/core/config.mjs +77 -0
- package/packages/board/src/core/control-protocol.mjs +482 -0
- package/packages/board/src/core/coordinator-client.mjs +633 -0
- package/packages/board/src/core/coordinator-journal.mjs +282 -0
- package/packages/board/src/core/coordinator-protocol.mjs +14 -0
- package/packages/board/src/core/cwd-stats.mjs +114 -0
- package/packages/board/src/core/dashboard-render.mjs +10 -0
- package/packages/board/src/core/derive.mjs +117 -0
- package/packages/board/src/core/diagnostics.mjs +133 -0
- package/packages/board/src/core/double-press.mjs +26 -0
- package/packages/board/src/core/editor-state-reporter.mjs +112 -0
- package/packages/board/src/core/events.mjs +286 -0
- package/packages/board/src/core/evidence.mjs +245 -0
- package/packages/board/src/core/follow-up-queue.mjs +211 -0
- package/packages/board/src/core/foreground-preview-cache.mjs +117 -0
- package/packages/board/src/core/foreground-projection-cache.mjs +99 -0
- package/packages/board/src/core/frame-witness-update.mjs +35 -0
- package/packages/board/src/core/heuristics.mjs +313 -0
- package/packages/board/src/core/hold-input.mjs +27 -0
- package/packages/board/src/core/host-coordination.mjs +300 -0
- package/packages/board/src/core/host-crash.mjs +79 -0
- package/packages/board/src/core/host-probe.mjs +207 -0
- package/packages/board/src/core/host-protocol.mjs +73 -0
- package/packages/board/src/core/ids.mjs +35 -0
- package/packages/board/src/core/ime-cursor-coalesce.mjs +193 -0
- package/packages/board/src/core/invocation.mjs +43 -0
- package/packages/board/src/core/launch-options.mjs +418 -0
- package/packages/board/src/core/launch.mjs +157 -0
- package/packages/board/src/core/load-config.mjs +4 -0
- package/packages/board/src/core/locks.mjs +530 -0
- package/packages/board/src/core/paths.mjs +183 -0
- package/packages/board/src/core/pid.mjs +75 -0
- package/packages/board/src/core/prewarm-schedule.mjs +41 -0
- package/packages/board/src/core/process-identity-probe.mjs +228 -0
- package/packages/board/src/core/prompt-transport.mjs +13 -0
- package/packages/board/src/core/pty-attach-jiggle-controller.mjs +356 -0
- package/packages/board/src/core/pty-attach-jiggle-retry.mjs +109 -0
- package/packages/board/src/core/pty-attach-reconnect.mjs +61 -0
- package/packages/board/src/core/pty-attach-render.mjs +229 -0
- package/packages/board/src/core/pty-links.mjs +81 -0
- package/packages/board/src/core/pty-scroll.mjs +522 -0
- package/packages/board/src/core/pty-support.mjs +327 -0
- package/packages/board/src/core/repo.mjs +103 -0
- package/packages/board/src/core/rows.mjs +414 -0
- package/packages/board/src/core/screen-log-gc.mjs +198 -0
- package/packages/board/src/core/screen-log.mjs +160 -0
- package/packages/board/src/core/session-view.mjs +174 -0
- package/packages/board/src/core/slash-commands.mjs +52 -0
- package/packages/board/src/core/state-commands.mjs +757 -0
- package/packages/board/src/core/status-consistency.mjs +98 -0
- package/packages/board/src/core/steering-prompts.mjs +34 -0
- package/packages/board/src/core/steering.mjs +133 -0
- package/packages/board/src/core/stop-latch.mjs +41 -0
- package/packages/board/src/core/store.mjs +523 -0
- package/packages/board/src/core/suspend.mjs +36 -0
- package/packages/board/src/core/terminal-attach-client.mjs +981 -0
- package/packages/board/src/core/terminal-attach-protocol.mjs +460 -0
- package/packages/board/src/core/terminal-model.mjs +222 -0
- package/packages/board/src/core/terminal-query-sequences.mjs +239 -0
- package/packages/board/src/core/terminal-reply-guard.mjs +222 -0
- package/packages/board/src/core/terminal-snapshot.mjs +440 -0
- package/packages/board/src/core/test-knobs.mjs +28 -0
- package/packages/board/src/core/title.mjs +43 -0
- package/packages/board/src/core/types.mjs +414 -0
- package/packages/board/src/core/warm-host-sweeper.mjs +168 -0
- package/packages/board/src/core/worktree.mjs +65 -0
- package/packages/board/src/index.ts +303 -0
- package/packages/board/src/runtime/service.mjs +3222 -0
- package/packages/board/src/ui/chrome.ts +20 -0
- package/packages/board/src/ui/dashboard-decisions.mjs +55 -0
- package/packages/board/src/ui/dashboard-evidence.mjs +85 -0
- package/packages/board/src/ui/dashboard.ts +2237 -0
- package/packages/board/src/ui/double-left.ts +30 -0
- package/packages/board/src/ui/external-editor.ts +25 -0
- package/packages/board/src/ui/focus.ts +32 -0
- package/packages/board/src/ui/pty-attach.ts +3184 -0
- package/packages/board/src/ui/waiting.ts +30 -0
- package/packages/commands/LICENSE +21 -0
- package/packages/commands/README.md +19 -0
- package/packages/commands/package.json +22 -0
- package/packages/commands/src/index.ts +43 -0
- package/packages/commands/src/security-review.ts +109 -0
- package/packages/frame/CHANGELOG.md +113 -0
- package/packages/frame/CONTRIBUTING.md +7 -0
- package/packages/frame/LICENSE +22 -0
- package/packages/frame/README.md +105 -0
- package/packages/frame/SECURITY.md +16 -0
- package/packages/frame/docs/how-it-works.md +53 -0
- package/packages/frame/docs/reference.md +78 -0
- package/packages/frame/package.json +72 -0
- package/packages/frame/src/data/git.ts +90 -0
- package/packages/frame/src/data/github.ts +140 -0
- package/packages/frame/src/data/gitlab.ts +63 -0
- package/packages/frame/src/data/mode.ts +49 -0
- package/packages/frame/src/data/past-prompts.ts +57 -0
- package/packages/frame/src/data/runner.ts +32 -0
- package/packages/frame/src/data/snapshot.ts +223 -0
- package/packages/frame/src/data/start-notes.ts +104 -0
- package/packages/frame/src/data/telemetry.ts +337 -0
- package/packages/frame/src/data/todos.ts +162 -0
- package/packages/frame/src/data/totals.ts +94 -0
- package/packages/frame/src/data/visits.ts +45 -0
- package/packages/frame/src/index.ts +696 -0
- package/packages/frame/src/kit/LICENSE +21 -0
- package/packages/frame/src/kit/app/attribution.ts +30 -0
- package/packages/frame/src/kit/app/builder.ts +186 -0
- package/packages/frame/src/kit/app/claim.ts +27 -0
- package/packages/frame/src/kit/app/feature.ts +91 -0
- package/packages/frame/src/kit/app/host.ts +214 -0
- package/packages/frame/src/kit/app/renderers.ts +62 -0
- package/packages/frame/src/kit/app/scope.ts +125 -0
- package/packages/frame/src/kit/board/model.ts +337 -0
- package/packages/frame/src/kit/contract.ts +79 -0
- package/packages/frame/src/kit/contracts/accounts.ts +33 -0
- package/packages/frame/src/kit/contracts/ask.ts +120 -0
- package/packages/frame/src/kit/contracts/board.ts +6 -0
- package/packages/frame/src/kit/contracts/by.ts +22 -0
- package/packages/frame/src/kit/contracts/calls.ts +46 -0
- package/packages/frame/src/kit/contracts/events.ts +89 -0
- package/packages/frame/src/kit/contracts/mode.ts +26 -0
- package/packages/frame/src/kit/contracts/names.ts +12 -0
- package/packages/frame/src/kit/contracts/peers.ts +77 -0
- package/packages/frame/src/kit/contracts/prompts.ts +8 -0
- package/packages/frame/src/kit/contracts/screen.ts +75 -0
- package/packages/frame/src/kit/contracts/skills.ts +81 -0
- package/packages/frame/src/kit/contracts/sources.ts +81 -0
- package/packages/frame/src/kit/contracts/views.ts +42 -0
- package/packages/frame/src/kit/control.ts +59 -0
- package/packages/frame/src/kit/dark-theme.ts +41 -0
- package/packages/frame/src/kit/decode.ts +210 -0
- package/packages/frame/src/kit/events/log.ts +313 -0
- package/packages/frame/src/kit/format.ts +25 -0
- package/packages/frame/src/kit/host.ts +4 -0
- package/packages/frame/src/kit/index.ts +25 -0
- package/packages/frame/src/kit/legacy-tasks.ts +39 -0
- package/packages/frame/src/kit/overlay-images.ts +36 -0
- package/packages/frame/src/kit/paths.ts +16 -0
- package/packages/frame/src/kit/screen/client.ts +74 -0
- package/packages/frame/src/kit/screen/feature.ts +64 -0
- package/packages/frame/src/kit/screen/model.ts +182 -0
- package/packages/frame/src/kit/screen/view.ts +838 -0
- package/packages/frame/src/kit/settings/files.ts +90 -0
- package/packages/frame/src/kit/settings/setting.ts +57 -0
- package/packages/frame/src/kit/settings/store.ts +192 -0
- package/packages/frame/src/kit/sources/file.ts +129 -0
- package/packages/frame/src/kit/sources/follow.ts +155 -0
- package/packages/frame/src/kit/testing.ts +260 -0
- package/packages/frame/src/kit/totals.ts +72 -0
- package/packages/frame/src/kit/ui/entry.ts +75 -0
- package/packages/frame/src/kit/ui/frame.ts +212 -0
- package/packages/frame/src/kit/ui/glyphs.ts +132 -0
- package/packages/frame/src/kit/ui/scroll.ts +14 -0
- package/packages/frame/src/render/diff.ts +157 -0
- package/packages/frame/src/render/fit.ts +86 -0
- package/packages/frame/src/render/format.ts +108 -0
- package/packages/frame/src/render/frame.ts +106 -0
- package/packages/frame/src/render/glyphs.ts +189 -0
- package/packages/frame/src/render/keys.ts +43 -0
- package/packages/frame/src/render/lead.ts +37 -0
- package/packages/frame/src/render/paint.ts +135 -0
- package/packages/frame/src/render/safe.ts +19 -0
- package/packages/frame/src/render/segments.ts +735 -0
- package/packages/frame/src/render/streaming.ts +223 -0
- package/packages/frame/src/render/timing.ts +106 -0
- package/packages/frame/src/render/tools.ts +167 -0
- package/packages/frame/src/settings.ts +105 -0
- package/packages/frame/src/ui/editor.ts +272 -0
- package/packages/frame/src/ui/effort.ts +36 -0
- package/packages/frame/src/ui/footer.ts +165 -0
- package/packages/frame/src/ui/header.ts +362 -0
- package/packages/frame/src/ui/history-search.ts +23 -0
- package/packages/frame/src/ui/interrupt.ts +21 -0
- package/packages/frame/src/ui/outputs.ts +170 -0
- package/packages/frame/src/ui/pi-entries.ts +298 -0
- package/packages/frame/src/ui/previews.ts +72 -0
- package/packages/frame/src/ui/queue.ts +75 -0
- package/packages/frame/src/ui/screen.ts +59 -0
- package/packages/frame/src/ui/timings.ts +48 -0
- package/packages/frame/src/ui/todos.ts +205 -0
- package/packages/frame/src/ui/tool-frame.ts +595 -0
- package/packages/frame/src/ui/working.ts +25 -0
- package/packages/frame/src/view/state.ts +97 -0
- package/packages/frame/src/view/transcript.ts +175 -0
- package/packages/frame/src/view/view.ts +162 -0
- package/packages/memory/CONTRIBUTING.md +7 -0
- package/packages/memory/LICENSE +21 -0
- package/packages/memory/README.md +44 -0
- package/packages/memory/SECURITY.md +20 -0
- package/packages/memory/package.json +55 -0
- package/packages/memory/skills/review-memory/SKILL.md +48 -0
- package/packages/memory/src/index.ts +163 -0
- package/packages/memory/src/prompt.ts +25 -0
- package/packages/memory/src/review.ts +150 -0
- package/packages/memory/src/secrets.ts +22 -0
- package/packages/memory/src/store.ts +173 -0
- package/packages/modes/CHANGELOG.md +104 -0
- package/packages/modes/CONTRIBUTING.md +7 -0
- package/packages/modes/LICENSE +21 -0
- package/packages/modes/README.md +174 -0
- package/packages/modes/SECURITY.md +17 -0
- package/packages/modes/package.json +55 -0
- package/packages/modes/skills/review-permissions/SKILL.md +54 -0
- package/packages/modes/src/bash.ts +218 -0
- package/packages/modes/src/deny-off.ts +62 -0
- package/packages/modes/src/git-safety.ts +72 -0
- package/packages/modes/src/index.ts +338 -0
- package/packages/modes/src/interview.ts +206 -0
- package/packages/modes/src/judge-gate.ts +113 -0
- package/packages/modes/src/modes.ts +121 -0
- package/packages/modes/src/review/apply.mjs +156 -0
- package/packages/modes/src/review/collect.mjs +208 -0
- package/packages/modes/src/review/tools.ts +71 -0
- package/packages/modes/src/sandbox.ts +37 -0
- package/packages/router/CHANGELOG.md +8 -0
- package/packages/router/LICENSE +21 -0
- package/packages/router/README.md +90 -0
- package/packages/router/package.json +51 -0
- package/packages/router/src/config.ts +65 -0
- package/packages/router/src/index.ts +172 -0
- package/packages/router/src/judge.ts +51 -0
- package/packages/router/src/route.ts +67 -0
- package/packages/web/CHANGELOG.md +36 -0
- package/packages/web/LICENSE +21 -0
- package/packages/web/README.md +87 -0
- package/packages/web/SECURITY.md +13 -0
- package/packages/web/package.json +59 -0
- package/packages/web/src/config.ts +109 -0
- package/packages/web/src/dnsfilter.ts +97 -0
- package/packages/web/src/extract.ts +65 -0
- package/packages/web/src/index.ts +217 -0
- package/packages/web/src/markdown.ts +122 -0
- package/packages/web/src/net.ts +109 -0
- package/packages/web/src/reader.ts +73 -0
- package/packages/web/src/sanitize.ts +50 -0
- package/packages/web/src/search.ts +76 -0
- package/packages/web/src/secrets.ts +22 -0
- package/packages/web/src/validate.ts +186 -0
- package/src/index.ts +156 -0
- package/src/settings.ts +79 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal query sequence extraction and color-scheme report mapping for the
|
|
3
|
+
* PTY attach client (issue #128).
|
|
4
|
+
*
|
|
5
|
+
* The attach client rebuilds the child screen in a headless terminal model, so
|
|
6
|
+
* the child's terminal capability queries (OSC 11 background color, color
|
|
7
|
+
* scheme change notifications) are consumed locally and never reach the real
|
|
8
|
+
* terminal. extractOscQuerySequences pulls exactly those query/notify forms
|
|
9
|
+
* out of the child output stream so the attach client can forward them to the
|
|
10
|
+
* real terminal; the replies travel back through the existing input
|
|
11
|
+
* passthrough (handleInput fallback). Setting forms (e.g. `\x1b]11;rgb:...`)
|
|
12
|
+
* are deliberately NOT extracted: forwarding those would repaint the local
|
|
13
|
+
* terminal's own colors.
|
|
14
|
+
*
|
|
15
|
+
* Kitty keyboard protocol / DA1 negotiation (`\x1b[>7u`, `\x1b[?u`, `\x1b[c`)
|
|
16
|
+
* is intentionally out of scope: the child negotiates once at spawn, before
|
|
17
|
+
* any attach client exists, and pushing kitty flags would retune the shared
|
|
18
|
+
* real-terminal keyboard stack under the local Pi's feet.
|
|
19
|
+
*
|
|
20
|
+
* Pure functions only — no I/O, no pi-tui import (the report format below is
|
|
21
|
+
* pinned by unit tests that round-trip it through pi-tui's parser).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// OSC 11 background-color QUERY. The `?` payload is what makes it a query;
|
|
25
|
+
// the SET form `\x1b]11;rgb:...` never matches this prefix (`;r` vs `;?`).
|
|
26
|
+
const OSC11_QUERY_PREFIX = "\x1b]11;?";
|
|
27
|
+
const OSC11_QUERY_BEL = OSC11_QUERY_PREFIX + "\x07";
|
|
28
|
+
const OSC11_QUERY_ST = OSC11_QUERY_PREFIX + "\x1b\\";
|
|
29
|
+
|
|
30
|
+
// Color-scheme change notification switch (host Pi toggles it at startup and
|
|
31
|
+
// when its own setting changes; the real terminal then reports scheme changes
|
|
32
|
+
// as `\x1b[?997;{1|2}n` reports). Fixed-form CSI, no variadic fields.
|
|
33
|
+
const COLOR_SCHEME_NOTIFY_ON = "\x1b[?2031h";
|
|
34
|
+
const COLOR_SCHEME_NOTIFY_OFF = "\x1b[?2031l";
|
|
35
|
+
|
|
36
|
+
// Carry slack aligned with the extractor family in pty-attach.ts (payload cap
|
|
37
|
+
// + 4096). Our longest target is 8 bytes, so 4096 is ample even for a
|
|
38
|
+
// malformed no-terminator tail, and slice(-N) keeps the carry bounded.
|
|
39
|
+
export const OSC_QUERY_CARRY_MAX_BYTES = 4096;
|
|
40
|
+
|
|
41
|
+
const TARGETS = [OSC11_QUERY_PREFIX, COLOR_SCHEME_NOTIFY_ON, COLOR_SCHEME_NOTIFY_OFF];
|
|
42
|
+
const MAX_TARGET_LENGTH = Math.max(...TARGETS.map((t) => t.length));
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Extract forwardable terminal query / notify sequences from a child output
|
|
46
|
+
* chunk. Returns the sequences (in order) and the carry: the unmatched tail
|
|
47
|
+
* to prepend to the next chunk. The carry is either a target prefix suffix
|
|
48
|
+
* (sequence split across chunks) or an unterminated OSC 11 query waiting for
|
|
49
|
+
* its BEL/ST terminator.
|
|
50
|
+
*/
|
|
51
|
+
export function extractOscQuerySequences(input) {
|
|
52
|
+
const sequences = [];
|
|
53
|
+
let scanFrom = 0;
|
|
54
|
+
let carryStart = -1;
|
|
55
|
+
while (scanFrom < input.length) {
|
|
56
|
+
const osc = input.indexOf(OSC11_QUERY_PREFIX, scanFrom);
|
|
57
|
+
const notifyOn = input.indexOf(COLOR_SCHEME_NOTIFY_ON, scanFrom);
|
|
58
|
+
const notifyOff = input.indexOf(COLOR_SCHEME_NOTIFY_OFF, scanFrom);
|
|
59
|
+
const start = firstIndex(firstIndex(osc, notifyOn), notifyOff);
|
|
60
|
+
if (start < 0) break;
|
|
61
|
+
if (start === osc) {
|
|
62
|
+
const after = start + OSC11_QUERY_PREFIX.length;
|
|
63
|
+
const bel = input.indexOf("\x07", after);
|
|
64
|
+
const st = input.indexOf("\x1b\\", after);
|
|
65
|
+
const end = firstTerminator(bel, st);
|
|
66
|
+
if (!end) {
|
|
67
|
+
carryStart = start;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
const [endIndex, terminatorLength] = end;
|
|
71
|
+
const seq = input.slice(start, endIndex + terminatorLength);
|
|
72
|
+
// Strict form: an OSC 11 query has no payload between `?` and the
|
|
73
|
+
// terminator. Anything else (e.g. `\x1b]11;?<junk>\x07`) is not a
|
|
74
|
+
// query we forward.
|
|
75
|
+
if (seq === OSC11_QUERY_BEL || seq === OSC11_QUERY_ST) sequences.push(seq);
|
|
76
|
+
scanFrom = endIndex + terminatorLength;
|
|
77
|
+
} else {
|
|
78
|
+
// Fixed-form CSI notify switch: an indexOf hit is always complete.
|
|
79
|
+
const seq = start === notifyOn ? COLOR_SCHEME_NOTIFY_ON : COLOR_SCHEME_NOTIFY_OFF;
|
|
80
|
+
sequences.push(seq);
|
|
81
|
+
scanFrom = start + seq.length;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const carry = carryStart >= 0
|
|
85
|
+
? input.slice(carryStart).slice(-OSC_QUERY_CARRY_MAX_BYTES)
|
|
86
|
+
: queryPrefixSuffix(input);
|
|
87
|
+
return { sequences, carry };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Longest suffix of `input` that is a strict prefix of one of our target
|
|
92
|
+
* sequences (empty when none matches). Used as the carry when every target
|
|
93
|
+
* occurrence in this chunk was complete.
|
|
94
|
+
*/
|
|
95
|
+
function queryPrefixSuffix(input) {
|
|
96
|
+
const max = Math.min(input.length, MAX_TARGET_LENGTH - 1);
|
|
97
|
+
for (let len = max; len > 0; len--) {
|
|
98
|
+
const suffix = input.slice(-len);
|
|
99
|
+
if (TARGETS.some((t) => t.startsWith(suffix))) return suffix;
|
|
100
|
+
}
|
|
101
|
+
return "";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function firstIndex(a, b) {
|
|
105
|
+
if (a < 0) return b;
|
|
106
|
+
if (b < 0) return a;
|
|
107
|
+
return Math.min(a, b);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function firstTerminator(bel, st) {
|
|
111
|
+
if (bel < 0 && st < 0) return null;
|
|
112
|
+
if (bel >= 0 && (st < 0 || bel < st)) return [bel, 1];
|
|
113
|
+
return [st, 2];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Color-scheme report expected by pi-tui's parseTerminalColorSchemeReport
|
|
117
|
+
// (node_modules/@earendil-works/pi-tui/dist/terminal-colors.js:19):
|
|
118
|
+
// /^\x1b\[\?997;(1|2)n$/ with "2" => light, "1" => dark (line 57). Note the
|
|
119
|
+
// report code point is 997 — the QUERY the host sends is `\x1b[?996n`.
|
|
120
|
+
const COLOR_SCHEME_REPORTS = {
|
|
121
|
+
light: "\x1b[?997;2n",
|
|
122
|
+
dark: "\x1b[?997;1n",
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Map a color scheme ("light" | "dark") to the report sequence the host Pi's
|
|
127
|
+
* pi-tui parser consumes. Returns "" for anything else; callers skip sending
|
|
128
|
+
* on empty.
|
|
129
|
+
*/
|
|
130
|
+
export function toColorSchemeReport(scheme) {
|
|
131
|
+
return Object.hasOwn(COLOR_SCHEME_REPORTS, scheme) ? COLOR_SCHEME_REPORTS[scheme] : "";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Issue #148: derive the color scheme from an OSC 11 background reply, so the
|
|
136
|
+
* attach client can answer the scheme question itself instead of relying on
|
|
137
|
+
* the child's own probe being resolved (Pi's leaked pending state swallows the
|
|
138
|
+
* first late reply — see the issue for the exact swallow path).
|
|
139
|
+
*
|
|
140
|
+
* Mirrors Pi's own threshold (`getRgbColorLuminance` + `>= 0.5` in the theme
|
|
141
|
+
* detection code) so a client-side answer matches what Pi would have
|
|
142
|
+
* concluded from the same reply. Relative luminance per WCAG/sRGB.
|
|
143
|
+
*
|
|
144
|
+
* @param rgb - parsed reply channels ({r,g,b} in 0-255), or undefined.
|
|
145
|
+
* @returns "light" / "dark", or undefined when the input is not a usable rgb.
|
|
146
|
+
*/
|
|
147
|
+
export function colorSchemeForBackgroundRgb(rgb) {
|
|
148
|
+
if (!rgb || typeof rgb !== "object") return undefined;
|
|
149
|
+
const { r, g, b } = rgb;
|
|
150
|
+
if (![r, g, b].every((channel) => Number.isFinite(channel))) return undefined;
|
|
151
|
+
const luminance = 0.2126 * toLinearChannel(r) + 0.7152 * toLinearChannel(g) + 0.0722 * toLinearChannel(b);
|
|
152
|
+
return luminance >= 0.5 ? "light" : "dark";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function toLinearChannel(channel) {
|
|
156
|
+
const value = channel / 255;
|
|
157
|
+
return value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// --- Attach settle scheme probe (issue #161) -------------------------------
|
|
161
|
+
// pi-tui >= 0.99 renamed queryTerminalBackgroundColor -> queryTerminalColors.
|
|
162
|
+
// This repo compiles against pi-tui 0.79.8 but the extension runs against the
|
|
163
|
+
// host pi's pi-tui (peerDependencies "*"), so the probe feature-detects at
|
|
164
|
+
// runtime. Local structural typedefs only — TerminalColors does not exist on
|
|
165
|
+
// the 0.79.8 compile face and must not be imported.
|
|
166
|
+
|
|
167
|
+
/** @typedef {{ r: number, g: number, b: number }} SettleRgb */
|
|
168
|
+
/** @typedef {{ background?: SettleRgb, foreground?: SettleRgb, palette?: SettleRgb[] }} TerminalColorsLike */
|
|
169
|
+
/** @typedef {{ queryTerminalColors?: (opts: { timeoutMs: number, onLateReply?: (colors: TerminalColorsLike) => void }) => Promise<TerminalColorsLike>, queryTerminalBackgroundColor?: (opts: { timeoutMs: number }) => Promise<SettleRgb|undefined> }} SettleProbeSurface */
|
|
170
|
+
/** @typedef {{ rgb?: SettleRgb, anyColors: boolean }} SettleProbeResult */
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Issue #161: detect which terminal color query API a TUI surface exposes.
|
|
174
|
+
* Detection is pure; the returned adapter only calls the passed-in surface.
|
|
175
|
+
* @param {SettleProbeSurface} tuiLike
|
|
176
|
+
* @returns {{ api: "colors"|"background"|"none", invoke: (tui: SettleProbeSurface, timeoutMs: number, onLateReply?: (colors: TerminalColorsLike) => void) => Promise<SettleProbeResult> }}
|
|
177
|
+
*/
|
|
178
|
+
export function resolveProbeApi(tuiLike) {
|
|
179
|
+
if (tuiLike && typeof tuiLike.queryTerminalColors === "function") {
|
|
180
|
+
return {
|
|
181
|
+
api: "colors",
|
|
182
|
+
invoke: async (tui, timeoutMs, onLateReply) => {
|
|
183
|
+
const colors = await tui.queryTerminalColors({ timeoutMs, onLateReply });
|
|
184
|
+
return { rgb: backgroundRgbFromTerminalColors(colors), anyColors: hasAnyColor(colors) };
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (tuiLike && typeof tuiLike.queryTerminalBackgroundColor === "function") {
|
|
189
|
+
return {
|
|
190
|
+
api: "background",
|
|
191
|
+
invoke: async (tui, timeoutMs) => {
|
|
192
|
+
const rgb = await tui.queryTerminalBackgroundColor({ timeoutMs });
|
|
193
|
+
return { rgb, anyColors: rgb !== undefined };
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
return { api: "none", invoke: async () => ({ rgb: undefined, anyColors: false }) };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** @param {TerminalColorsLike} colors @returns {SettleRgb|undefined} */
|
|
201
|
+
export function backgroundRgbFromTerminalColors(colors) {
|
|
202
|
+
return colors && typeof colors === "object" ? colors.background : undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** @param {TerminalColorsLike} colors @returns {boolean} */
|
|
206
|
+
function hasAnyColor(colors) {
|
|
207
|
+
if (!colors || typeof colors !== "object") return false;
|
|
208
|
+
if (colors.background !== undefined || colors.foreground !== undefined) return true;
|
|
209
|
+
return Array.isArray(colors.palette) && colors.palette.some((c) => c !== undefined);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const SETTLE_SCHEME_WARN_OUTCOMES = new Set(["timeout", "error", "no_background", "no_probe_api", "dropped_disconnected"]);
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Issue #161: build the DiagnosticEvent-compatible patch for the settle scheme
|
|
216
|
+
* flow. Field names MUST stay inside normalizeDiagnostic's whitelist
|
|
217
|
+
* (level/code/runId/source/message/details) — anything else is silently
|
|
218
|
+
* dropped. Warn only when delivery was expected but failed; routine outcomes
|
|
219
|
+
* (dedup, quick detach, kill switch) stay info so warningCount stays meaningful.
|
|
220
|
+
* @param {{ probeApi?: "colors"|"background"|"none", outcome: "reported"|"timeout"|"error"|"no_background"|"no_probe_api"|"dropped_closed"|"dropped_disconnected"|"duplicate_skipped"|"suppressed", report?: string, late?: boolean }} details
|
|
221
|
+
* @returns {{ source: string, level: "info"|"warn", code: string, message: string, details: { probeApi?: string, outcome: string, report?: string, late?: boolean } }}
|
|
222
|
+
*/
|
|
223
|
+
export function buildSettleSchemePatch(details) {
|
|
224
|
+
const parts = [details.outcome];
|
|
225
|
+
if (details.probeApi) parts.push(`via ${details.probeApi}`);
|
|
226
|
+
if (details.report) parts.push(details.report);
|
|
227
|
+
if (details.late) parts.push("late");
|
|
228
|
+
const patch = {
|
|
229
|
+
source: "attach",
|
|
230
|
+
level: SETTLE_SCHEME_WARN_OUTCOMES.has(details.outcome) ? "warn" : "info",
|
|
231
|
+
code: "attach_settle_scheme",
|
|
232
|
+
message: `attach settle scheme: ${parts.join(" ")}`,
|
|
233
|
+
details: { outcome: details.outcome },
|
|
234
|
+
};
|
|
235
|
+
if (details.probeApi !== undefined) patch.details.probeApi = details.probeApi;
|
|
236
|
+
if (details.report !== undefined) patch.details.report = details.report;
|
|
237
|
+
if (details.late !== undefined) patch.details.late = details.late;
|
|
238
|
+
return patch;
|
|
239
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inbound terminal reply guard for the PTY attach client (issue #170).
|
|
3
|
+
*
|
|
4
|
+
* pi-tui hands replies to forwarded terminal queries to this overlay as input
|
|
5
|
+
* fragments: the prefix arms a 50ms sequence timeout in StdinBuffer and a
|
|
6
|
+
* 150ms negotiation flush in ProcessTerminal, so a CSI-shaped reply head is
|
|
7
|
+
* emitted as an input event and the remaining fragments arrive as plain text.
|
|
8
|
+
* The overlay's passthrough then feeds that garbage into the child's editor
|
|
9
|
+
* (upstream earendil-works/pi#10657 — a DA1 reply arrives as four reads and
|
|
10
|
+
* shows up in the child as a stable `c7c`).
|
|
11
|
+
*
|
|
12
|
+
* This module classifies every inbound event and the state machine below
|
|
13
|
+
* swallows reply fragments until the sequence completes. Color replies (OSC
|
|
14
|
+
* 4/10/11/12 and the 997 scheme report) are forwarded as ONE whole sequence,
|
|
15
|
+
* because the child's pi-tui parsers anchor `^...$`; everything else falls
|
|
16
|
+
* through untouched (issue #128 decision: the child negotiates at spawn, a late
|
|
17
|
+
* DA/kitty reply is meaningless to it, and forwarding kitty flags would retune
|
|
18
|
+
* the shared real-terminal keyboard stack).
|
|
19
|
+
*
|
|
20
|
+
* Issue #213: `\x1b[6;` is not proof of a reply — kitty's functional-key encoding puts
|
|
21
|
+
* PageDown at `CSI 6;<mods>u` (and modifyOtherKeys at `\x1b[6;<mods>~`). The rule is
|
|
22
|
+
* therefore key-first: a complete CSI key sequence (`u`/`~` terminated) beats the head
|
|
23
|
+
* prefixes, so the head list is a heuristic rather than an invariant.
|
|
24
|
+
* Fragments without a terminator still arm.
|
|
25
|
+
*
|
|
26
|
+
* Pure only: no I/O, no timers, no `process.env`, no pi-tui import. Time is
|
|
27
|
+
* injected by the caller (see `armReplyGuardTimer` in src/ui/pty-attach.ts).
|
|
28
|
+
*
|
|
29
|
+
* Known boundary: a FRAGMENTED DA3 reply (`\x1b[=...c`) is not armed — this
|
|
30
|
+
* module's head list deliberately omits `\x1b[=` because the attach client
|
|
31
|
+
* never queries DA3 — so such a fragment passes through as it did before this
|
|
32
|
+
* guard. The complete DA3 form is still dropped (it is a DROP_SHAPES entry).
|
|
33
|
+
*
|
|
34
|
+
* Known trade-offs in the head list (raised in review): `\x1bP` is also the
|
|
35
|
+
* legacy xterm encoding of Alt+Shift+P on terminals without the kitty
|
|
36
|
+
* protocol, so that chord can arm a window and absorb up to the capped bytes
|
|
37
|
+
* of plain text typed right after it. It is kept because a fragmented DCS
|
|
38
|
+
* reply (XTVersion) is otherwise unprotected, and the absorption is bounded
|
|
39
|
+
* (64 bytes / 250ms) and kill-switchable. `\x1b]1` covers the whole OSC 1xx
|
|
40
|
+
* range, so a fragmented reply outside the forwarded 4/10/11/12 set is
|
|
41
|
+
* absorbed rather than leaked — the safe direction for an unrecognized OSC.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/** How long an open reply window stays open for trailing fragments (spec §3.3). */
|
|
45
|
+
export const REPLY_GUARD_WINDOW_MS = 250;
|
|
46
|
+
|
|
47
|
+
/** Bound on how much of a user's fast typing an open window may absorb. */
|
|
48
|
+
export const REPLY_GUARD_MAX_ABSORB_BYTES = 64;
|
|
49
|
+
|
|
50
|
+
// Complete reply shapes that are dropped. The child's negotiation happened at
|
|
51
|
+
// spawn, before any attach client existed, so a late reply can only pollute its
|
|
52
|
+
// input or retune keyboards that no longer asked.
|
|
53
|
+
const DROP_SHAPES = [
|
|
54
|
+
// DA1 / DA3 (`\x1b[?...c`, `\x1b[=...c`) and DA2 (`\x1b[>...c`).
|
|
55
|
+
{ family: "device-attributes", pattern: /^\x1b\[[?>=][0-9;]*c$/ },
|
|
56
|
+
// kitty keyboard flags reply.
|
|
57
|
+
{ family: "kitty-flags", pattern: /^\x1b\[\?[0-9;]*u$/ },
|
|
58
|
+
// XTWINOPS cell-size report (`\x1b[6;<rows>;<cols>t`).
|
|
59
|
+
{ family: "cell-size", pattern: /^\x1b\[6;[0-9]+;[0-9]+t$/ },
|
|
60
|
+
// DCS replies such as XTVersion.
|
|
61
|
+
{ family: "dcs", pattern: /^\x1bP[\s\S]*\x1b\\$/ },
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
// Complete reply shapes that are forwarded whole: the child's pi-tui parsers
|
|
65
|
+
// consume them (automatic light/dark theme) and only as one sequence.
|
|
66
|
+
const FORWARD_SHAPES = [
|
|
67
|
+
// `\x1b[?997;<n>n` color-scheme report. Any numeric value (one or more digits)
|
|
68
|
+
// is forwarded — value validation belongs to the child's parser, not to this
|
|
69
|
+
// guard.
|
|
70
|
+
{ family: "color-scheme", pattern: /^\x1b\[\?997;[0-9]+n$/ },
|
|
71
|
+
// OSC 4 palette / 10 foreground / 11 background / 12 cursor color replies,
|
|
72
|
+
// BEL- or ST-terminated.
|
|
73
|
+
{ family: "osc-color", pattern: /^\x1b\](?:4;[0-9]+|10|11|12);[\s\S]*(?:\x07|\x1b\\)$/ },
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
// Prefixes that arm an absorption window for a possibly-fragmented reply. These are
|
|
77
|
+
// heuristics, not proofs: `\x1b[6;` also prefixes kitty's PageDown (`CSI 6;<mods>u`,
|
|
78
|
+
// see CSI_KEY_SEQUENCE) and modifyOtherKeys' `\x1b[6;<mods>~`, which is why a COMPLETE
|
|
79
|
+
// CSI key is classified before this list is consulted. Bare `\x1b`, `\x1b[`, `\x1b[<`
|
|
80
|
+
// (SGR mouse) and `\x1bO` (SS3) are deliberately absent.
|
|
81
|
+
const REPLY_HEADS = [
|
|
82
|
+
{ family: "device-attributes", prefix: "\x1b[?" },
|
|
83
|
+
{ family: "device-attributes", prefix: "\x1b[>" },
|
|
84
|
+
{ family: "cell-size", prefix: "\x1b[6;" },
|
|
85
|
+
{ family: "dcs", prefix: "\x1bP" },
|
|
86
|
+
{ family: "osc-color", prefix: "\x1b]1" },
|
|
87
|
+
{ family: "osc-color", prefix: "\x1b]4" },
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
// Issue #213: kitty keyboard protocol keys that carry a modifier use the legacy
|
|
91
|
+
// functional-key encoding `CSI <num>;<mods>u`, and PageDown's legacy number IS 6
|
|
92
|
+
// (`\x1b[6;1u` = no modifier, the mods field is bitmask+1). modifyOtherKeys uses the
|
|
93
|
+
// same prefix with `~` (`\x1b[6;5~`). Both therefore share the cell-size head prefix
|
|
94
|
+
// (`\x1b[6;`), which used to arm the guard and swallow the key — asymmetric with
|
|
95
|
+
// PageUp (key number 5) and invisible to the previous tests.
|
|
96
|
+
//
|
|
97
|
+
// A COMPLETE CSI key is user input, never a reply fragment: check it before the head
|
|
98
|
+
// prefixes. Only the terminator decides — genuine cell-size fragments (`\x1b[6;`,
|
|
99
|
+
// `\x1b[6;24`) carry no `u`/`~` and still arm below. `(?:[;:]\d+)*` also covers the
|
|
100
|
+
// alternate-key form (`\x1b[6;1:2;65u`), which a narrower `(?:;\d+)?(?::\d+)?` misses.
|
|
101
|
+
const CSI_KEY_SEQUENCE = /^\x1b\[\d+(?:[;:]\d+)*[u~]$/;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Classify one inbound input event. Complete reply shapes win over head
|
|
105
|
+
* prefixes (a complete kitty reply also starts with `\x1b[?`); escape-led
|
|
106
|
+
* events that are neither are keys, mouse or paste data.
|
|
107
|
+
*/
|
|
108
|
+
export function classifyReplyEvent(data) {
|
|
109
|
+
if (typeof data !== "string" || data.length === 0) return { kind: "plain" };
|
|
110
|
+
for (const shape of DROP_SHAPES) {
|
|
111
|
+
if (shape.pattern.test(data)) return { kind: "reply-complete", family: shape.family, action: "drop" };
|
|
112
|
+
}
|
|
113
|
+
for (const shape of FORWARD_SHAPES) {
|
|
114
|
+
if (shape.pattern.test(data)) return { kind: "reply-complete", family: shape.family, action: "forward" };
|
|
115
|
+
}
|
|
116
|
+
// Issue #213: a complete CSI key wins over the head prefixes (see CSI_KEY_SEQUENCE).
|
|
117
|
+
if (CSI_KEY_SEQUENCE.test(data)) return { kind: "other" };
|
|
118
|
+
for (const head of REPLY_HEADS) {
|
|
119
|
+
if (data.startsWith(head.prefix)) return { kind: "reply-head", family: head.family };
|
|
120
|
+
}
|
|
121
|
+
// Everything else that starts with ESC is a key/mouse/paste encoding; plain
|
|
122
|
+
// text may be a reply tail fragment (those never start with ESC after the
|
|
123
|
+
// upstream split, see the field evidence in the issue).
|
|
124
|
+
return data.startsWith("\x1b") ? { kind: "other" } : { kind: "plain" };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const SWALLOW = Object.freeze({ action: "swallow" });
|
|
128
|
+
const PASS = Object.freeze({ action: "pass" });
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Issue #170 kill switch: `0`, `off` or `false` (case/space-insensitive)
|
|
132
|
+
* disable the guard; unset or anything else keeps it enabled.
|
|
133
|
+
*/
|
|
134
|
+
export function isReplyGuardEnabled(value) {
|
|
135
|
+
if (typeof value !== "string") return true;
|
|
136
|
+
return !["0", "off", "false"].includes(value.trim().toLowerCase());
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Pure reply-fragment state machine (spec §3.3). Two states only — idle and
|
|
141
|
+
* armed — and no timer of its own: the caller feeds `now` and arms a host
|
|
142
|
+
* timer from `pendingSince()`.
|
|
143
|
+
*
|
|
144
|
+
* idle + head → armed (buffer = event, since = now) → swallow
|
|
145
|
+
* idle + complete → idle → drop/forward
|
|
146
|
+
* idle + plain | other → idle → pass
|
|
147
|
+
* armed + plain → armed, buffer += event → swallow
|
|
148
|
+
* armed + plain over cap → idle (buffer and event are dropped) → swallow
|
|
149
|
+
* armed + joined complete → idle → drop/forward joined
|
|
150
|
+
* armed + head → armed (stale buffer dropped) → swallow
|
|
151
|
+
* armed + complete → idle (stale buffer dropped) → handle event alone
|
|
152
|
+
* armed + other → idle (stale buffer dropped) → pass
|
|
153
|
+
* armed + expire in time → idle → swallow
|
|
154
|
+
*/
|
|
155
|
+
export function createReplyGuard({
|
|
156
|
+
windowMs = REPLY_GUARD_WINDOW_MS,
|
|
157
|
+
maxAbsorbBytes = REPLY_GUARD_MAX_ABSORB_BYTES,
|
|
158
|
+
} = {}) {
|
|
159
|
+
let armed = false;
|
|
160
|
+
let buffer = "";
|
|
161
|
+
let since = null;
|
|
162
|
+
|
|
163
|
+
const reset = () => {
|
|
164
|
+
armed = false;
|
|
165
|
+
buffer = "";
|
|
166
|
+
since = null;
|
|
167
|
+
};
|
|
168
|
+
const decisionFor = (event, data) => (event.action === "forward" ? { action: "forward", data } : SWALLOW);
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
feed(data, now) {
|
|
172
|
+
const event = classifyReplyEvent(data);
|
|
173
|
+
if (!armed) {
|
|
174
|
+
if (event.kind === "reply-head") {
|
|
175
|
+
armed = true;
|
|
176
|
+
buffer = data;
|
|
177
|
+
since = now;
|
|
178
|
+
return SWALLOW;
|
|
179
|
+
}
|
|
180
|
+
if (event.kind === "reply-complete") return decisionFor(event, data);
|
|
181
|
+
return PASS;
|
|
182
|
+
}
|
|
183
|
+
if (event.kind === "other") {
|
|
184
|
+
// Keys, mouse and paste win over absorbing one more fragment.
|
|
185
|
+
reset();
|
|
186
|
+
return PASS;
|
|
187
|
+
}
|
|
188
|
+
if (event.kind === "reply-head") {
|
|
189
|
+
// A fresh head means the previous sequence will never complete.
|
|
190
|
+
buffer = data;
|
|
191
|
+
since = now;
|
|
192
|
+
return SWALLOW;
|
|
193
|
+
}
|
|
194
|
+
if (event.kind === "reply-complete") {
|
|
195
|
+
reset();
|
|
196
|
+
return decisionFor(event, data);
|
|
197
|
+
}
|
|
198
|
+
const joined = buffer + data;
|
|
199
|
+
// Bound in UTF-16 code units — byte-exact for the ASCII reply corpus.
|
|
200
|
+
if (joined.length > maxAbsorbBytes) {
|
|
201
|
+
reset();
|
|
202
|
+
return SWALLOW;
|
|
203
|
+
}
|
|
204
|
+
const joinedEvent = classifyReplyEvent(joined);
|
|
205
|
+
if (joinedEvent.kind === "reply-complete") {
|
|
206
|
+
reset();
|
|
207
|
+
return decisionFor(joinedEvent, joined);
|
|
208
|
+
}
|
|
209
|
+
buffer = joined;
|
|
210
|
+
return SWALLOW;
|
|
211
|
+
},
|
|
212
|
+
expire(now) {
|
|
213
|
+
if (!armed || now - since < windowMs) return null;
|
|
214
|
+
reset();
|
|
215
|
+
return SWALLOW;
|
|
216
|
+
},
|
|
217
|
+
pendingSince() {
|
|
218
|
+
return armed ? since : null;
|
|
219
|
+
},
|
|
220
|
+
reset,
|
|
221
|
+
};
|
|
222
|
+
}
|