smolcoder 0.4.2 → 0.5.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.
@@ -0,0 +1,222 @@
1
+ "use strict";
2
+ // Stylesheet for the web page. One dark, monospace theme shared with the TUI.
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.STYLES = void 0;
5
+ exports.STYLES = String.raw `
6
+ :root {
7
+ --bg: #0b0d0e; --fg: #d6dbde; --dim: #6b7480; --gray: #4a525c;
8
+ --accent: #35bfd4; --yellow: #e0af68; --red: #f7768e; --green: #9ece6a;
9
+ --magenta: #bb9af7; --box: #14181a; --sel: #1a7f94; --line: #232a2f; --side: #0e1113;
10
+ }
11
+ * { box-sizing: border-box; }
12
+ /* Class rules below set display; the hidden attribute must still win. */
13
+ [hidden] { display: none !important; }
14
+ /* Scrollbars in the page's own colors (native ones are light and chunky). */
15
+ html { color-scheme: dark; scrollbar-color: #2c343a transparent; }
16
+ * { scrollbar-width: thin; }
17
+ ::-webkit-scrollbar { width: 9px; height: 9px; }
18
+ ::-webkit-scrollbar-track { background: transparent; }
19
+ ::-webkit-scrollbar-thumb { background: #2c343a; border-radius: 5px; border: 2px solid transparent; background-clip: padding-box; }
20
+ ::-webkit-scrollbar-thumb:hover { background: #3d474f; background-clip: padding-box; }
21
+ ::-webkit-scrollbar-corner { background: transparent; }
22
+ html, body { height: 100%; }
23
+ body {
24
+ margin: 0; background: var(--bg); color: var(--fg); display: flex; overflow: hidden;
25
+ font: 14px/1.5 ui-monospace, "Cascadia Code", Consolas, monospace;
26
+ }
27
+ button { font: inherit; }
28
+ a { color: var(--accent); }
29
+ .grow { flex: 1; }
30
+ .iconbtn { background: transparent; border: 1px solid transparent; color: var(--dim); cursor: pointer; border-radius: 4px; padding: 2px 6px; line-height: 1.2; display: inline-flex; align-items: center; gap: 4px; }
31
+ .iconbtn:hover { color: var(--fg); border-color: var(--line); background: #1a2023; }
32
+ .iconbtn.on { color: var(--accent); border-color: var(--line); background: #13202a; }
33
+ .iconbtn svg { display: block; }
34
+ .primary { background: #173f47; color: #eef3f5; border: 1px solid #1f5d68; padding: 5px 12px; border-radius: 4px; cursor: pointer; }
35
+ .primary:hover { border-color: var(--accent); }
36
+ .ghost { background: #151b1e; color: var(--fg); border: 1px solid var(--line); border-radius: 4px; padding: 5px 10px; cursor: pointer; }
37
+ .ghost:hover { border-color: var(--accent); }
38
+ .hint { color: var(--dim); font-size: 12px; }
39
+
40
+ /* ---- left sidebar ---- */
41
+ #side { width: 272px; flex: none; background: var(--side); border-right: 1px solid var(--line); display: flex; flex-direction: column; min-height: 0; }
42
+ #side.collapsed { display: none; }
43
+ .sidehdr { display: flex; align-items: center; gap: 8px; padding: 12px 10px 8px 14px; }
44
+ .brand { font-weight: 700; color: var(--accent); letter-spacing: .5px; }
45
+ .brand .coder { color: var(--dim); }
46
+ #openfolder { margin: 2px 10px 10px; text-align: left; }
47
+ #wslist { flex: 1; overflow-y: auto; padding: 0 6px 10px; }
48
+ .sidehint { color: var(--dim); font-size: 12px; padding: 8px 10px; }
49
+ .ws { margin: 2px 0 10px; }
50
+ .wshdr { display: flex; align-items: center; gap: 6px; padding: 4px 4px 4px 8px; border-radius: 4px; color: var(--dim); font-size: 12.5px; }
51
+ .wshdr:hover { background: #141a1d; }
52
+ .wsname { color: var(--fg); font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: none; max-width: 55%; }
53
+ .wspath { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; color: var(--gray); }
54
+ .wshdr .iconbtn { visibility: hidden; padding: 0 5px; }
55
+ .wshdr:hover .iconbtn { visibility: visible; }
56
+ .sess { display: flex; align-items: center; gap: 8px; padding: 4px 4px 4px 12px; border-radius: 4px; cursor: pointer; color: var(--dim); font-size: 13px; }
57
+ .sess:hover { background: #141a1d; color: var(--fg); }
58
+ .sess.active { background: #17232a; color: #eef3f5; }
59
+ .sess .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--gray); flex: none; }
60
+ .sess.busy .dot, .sess.starting .dot { background: var(--accent); animation: pulse 1s infinite; }
61
+ .sess.waiting .dot { background: var(--yellow); box-shadow: 0 0 6px var(--yellow); }
62
+ .sess.error .dot { background: var(--red); }
63
+ .sess.stored .dot { background: transparent; border: 1px solid var(--gray); }
64
+ .sess.unread .stitle::after { content: " •"; color: var(--accent); }
65
+ .stitle { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
66
+ .stitle.untitled { font-style: italic; }
67
+ .stime { font-size: 11px; color: var(--gray); flex: none; }
68
+ .sess .iconbtn { visibility: hidden; padding: 0 5px; }
69
+ .sess:hover .iconbtn { visibility: visible; }
70
+ .sidefoot { padding: 8px 14px; font-size: 11.5px; color: var(--gray); border-top: 1px solid var(--line); display: flex; gap: 10px; }
71
+
72
+ /* ---- main column ---- */
73
+ #main { flex: 1; min-width: 0; display: flex; flex-direction: column; min-height: 0; }
74
+ #top { display: flex; align-items: center; gap: 6px; padding: 7px 10px; border-bottom: 1px solid var(--line); font-size: 12.5px; color: var(--dim); min-height: 40px; }
75
+ #crumb { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding-left: 4px; }
76
+ #crumb .ws { color: var(--fg); font-weight: 700; }
77
+ #crumb .sep { margin: 0 6px; color: var(--gray); }
78
+ #crumb .model { color: var(--gray); margin-left: 10px; }
79
+ #logwrap { flex: 1; overflow-y: auto; overflow-x: hidden; min-height: 0; }
80
+ #logs, #busywrap { max-width: 920px; margin: 0 auto; padding: 16px 16px 0; }
81
+ #logs { overflow-wrap: anywhere; }
82
+ #wslist, #fslist, .tabbody.term .out { overflow-x: hidden; }
83
+ #busywrap { padding-bottom: 20px; }
84
+ #welcome { max-width: 920px; margin: 0 auto; padding: 48px 16px; }
85
+ #welcome p { color: var(--dim); max-width: 60ch; }
86
+ #welcome .row { display: flex; gap: 10px; align-items: center; margin: 14px 0 22px; }
87
+ #recent { display: flex; flex-direction: column; gap: 4px; align-items: flex-start; }
88
+ .wsbtn { background: transparent; border: 1px solid transparent; color: var(--fg); padding: 4px 8px; border-radius: 4px; cursor: pointer; text-align: left; }
89
+ .wsbtn:hover { border-color: var(--line); background: #141a1d; }
90
+ .wsbtn .dim { color: var(--gray); font-size: 12px; }
91
+ #logo { color: var(--accent); white-space: pre; font-size: 11px; line-height: 1.15; margin: 8px 0 12px; }
92
+ #logo .coder { color: var(--dim); }
93
+
94
+ .user { border-left: 3px solid var(--accent); background: var(--box); padding: 8px 12px; margin: 18px 0 10px; font-weight: 600; white-space: pre-wrap; }
95
+ .thought { color: var(--gray); white-space: nowrap; overflow: hidden; margin-top: 4px; }
96
+ .md { white-space: normal; }
97
+ .md p { margin: 6px 0; }
98
+ .md h1, .md h2, .md h3, .md h4, .md h5, .md h6 { margin: 14px 0 6px; line-height: 1.3; color: #eef3f5; }
99
+ .md h1 { font-size: 1.3em; } .md h2 { font-size: 1.17em; } .md h3 { font-size: 1.06em; }
100
+ .md h4, .md h5, .md h6 { font-size: 1em; }
101
+ .md ul, .md ol { margin: 6px 0; padding-left: 22px; }
102
+ .md li { margin: 2px 0; }
103
+ .md strong { color: #eef3f5; }
104
+ .md code { background: #1b2124; padding: 1px 5px; border-radius: 3px; color: var(--yellow); }
105
+ .md pre { background: #11161a; border: 1px solid var(--line); border-radius: 4px; padding: 10px 12px; overflow-x: auto; margin: 8px 0; }
106
+ .md pre code { background: none; padding: 0; color: var(--fg); }
107
+ .md table { border-collapse: collapse; margin: 8px 0; display: block; overflow-x: auto; max-width: 100%; }
108
+ .md th, .md td { border: 1px solid var(--line); padding: 4px 10px; text-align: left; }
109
+ .md th { background: #171d20; color: #eef3f5; }
110
+ .md blockquote { border-left: 3px solid #2c343a; margin: 8px 0; padding-left: 12px; color: var(--dim); }
111
+ .md hr { border: 0; border-top: 1px solid var(--line); margin: 12px 0; }
112
+ .tool { color: var(--dim); margin-top: 4px; }
113
+ .tool .name { color: var(--accent); font-weight: 600; }
114
+ .result { color: var(--dim); padding-left: 16px; }
115
+ .result.err { color: var(--red); }
116
+ .plan { background: var(--box); border-left: 3px solid var(--accent); padding: 8px 12px; margin: 10px 0; }
117
+ .plan .hdr { font-weight: 700; } .plan .hdr small { color: var(--dim); font-weight: 400; }
118
+ .plan .done { color: var(--gray); text-decoration: line-through; }
119
+ .plan .cur { color: var(--accent); font-weight: 600; }
120
+ .plan .todo { color: var(--dim); }
121
+ .turnend { color: var(--gray); margin: 8px 0 4px; }
122
+ .line-status { color: var(--gray); white-space: pre-wrap; } .line-warn { color: var(--yellow); white-space: pre-wrap; } .line-error { color: var(--red); white-space: pre-wrap; }
123
+ #busy { color: var(--dim); display: none; }
124
+ #busy.on { display: block; }
125
+ #busy .spin { display: inline-block; color: var(--accent); animation: pulse 1s infinite; }
126
+ @keyframes pulse { 50% { opacity: .3; } }
127
+ .ask { background: var(--box); border-left: 3px solid var(--yellow); padding: 10px 12px; margin: 10px 0; }
128
+ .ask .cmd { font-weight: 700; }
129
+ .ask button, .ask .opt { margin: 6px 8px 0 0; background: #1e2428; color: var(--fg); border: 1px solid #2c343a; padding: 4px 12px; cursor: pointer; font: inherit; border-radius: 3px; }
130
+ .ask button:hover, .ask .opt:hover { border-color: var(--accent); }
131
+ .ask .opt.current { border-color: var(--green); }
132
+ .ask .opt .hint { color: var(--dim); font-size: 12px; margin-left: 8px; }
133
+ .ask > .hint { color: var(--dim); font-size: 12px; margin-top: 2px; }
134
+
135
+ #bottom { flex: none; padding: 8px 16px 12px; background: var(--bg); }
136
+ #bottom .inner { max-width: 920px; margin: 0 auto; position: relative; }
137
+ #menu { position: absolute; bottom: 100%; left: 0; right: 0; background: var(--box); border: 1px solid var(--line); display: none; z-index: 5; }
138
+ #menu .item { padding: 4px 10px; cursor: pointer; }
139
+ #menu .item .nm { font-weight: 700; } #menu .item .ds { color: var(--dim); margin-left: 10px; }
140
+ #menu .item.sel { background: var(--sel); color: #f2f7f8; }
141
+ #menu .item.sel .ds { color: #c8dde2; }
142
+ #inputbox { border-left: 3px solid var(--accent); background: var(--box); padding: 8px 12px; }
143
+ .inputrow { display: flex; align-items: flex-end; gap: 10px; }
144
+ #input { flex: 1; background: transparent; border: 0; outline: 0; color: var(--fg); font: inherit; resize: none; }
145
+ #actionbtn { flex: none; background: #1e2428; color: var(--dim); border: 1px solid #2c343a; padding: 3px 14px; cursor: pointer; font: inherit; font-size: 12px; border-radius: 3px; }
146
+ #actionbtn:hover { border-color: var(--accent); color: var(--accent); }
147
+ #actionbtn.stop { color: var(--red); border-color: #3d2d31; }
148
+ #actionbtn.stop:hover { border-color: var(--red); color: var(--red); }
149
+ #status { margin-top: 6px; font-size: 12.5px; color: var(--dim); }
150
+ #status .mode { font-weight: 700; }
151
+ #status .mode.edit { color: var(--accent); } #status .mode.bypass { color: var(--red); } #status .mode.ro { color: var(--magenta); }
152
+ #status .eff { color: var(--yellow); } #status .plan-chip { color: var(--accent); } #status .plan-chip.done { color: var(--green); }
153
+ #hintrow { font-size: 12px; color: var(--gray); margin-top: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
154
+
155
+ /* ---- right panel: browser + terminal tabs ---- */
156
+ #panel { flex: none; width: 520px; min-width: 300px; max-width: 80vw; border-left: 1px solid var(--line); display: flex; flex-direction: column; min-height: 0; position: relative; background: var(--side); }
157
+ #panel[hidden] { display: none; }
158
+ #panelgrip { position: absolute; left: -3px; top: 0; bottom: 0; width: 7px; cursor: col-resize; z-index: 6; }
159
+ #panelgrip:hover, body.dragging #panelgrip { background: var(--sel); }
160
+ body.dragging { cursor: col-resize; user-select: none; }
161
+ body.dragging iframe { pointer-events: none; }
162
+ #paneltabs { display: flex; align-items: center; gap: 2px; padding: 5px 6px; border-bottom: 1px solid var(--line); overflow-x: auto; flex: none; min-height: 40px; }
163
+ .ptab { display: flex; align-items: center; gap: 6px; padding: 3px 6px 3px 8px; border-radius: 4px; color: var(--dim); cursor: pointer; font-size: 12px; white-space: nowrap; max-width: 210px; border: 1px solid transparent; }
164
+ .ptab:hover { color: var(--fg); background: #141a1d; }
165
+ .ptab.on { color: #eef3f5; background: #17232a; border-color: var(--line); }
166
+ .ptab .ico { color: var(--accent); font-size: 11px; }
167
+ .ptab .lbl { overflow: hidden; text-overflow: ellipsis; }
168
+ .ptab .x { color: var(--gray); padding: 0 2px; }
169
+ .ptab .x:hover { color: var(--red); }
170
+ #panelviews { flex: 1; min-height: 0; display: flex; }
171
+ .panelview { flex: 1; min-width: 0; display: flex; flex-direction: column; min-height: 0; }
172
+ .panelview[hidden] { display: none; }
173
+ .tabbody { flex: 1; min-height: 0; display: flex; flex-direction: column; }
174
+ .tabbody[hidden] { display: none; }
175
+ .tabbody.browser .bar { display: flex; gap: 6px; padding: 6px 8px; border-bottom: 1px solid var(--line); align-items: center; flex: none; }
176
+ .tabbody.browser .bar input { flex: 1; min-width: 0; background: var(--box); border: 1px solid var(--line); color: var(--fg); font: inherit; font-size: 12.5px; padding: 3px 8px; border-radius: 4px; outline: 0; }
177
+ .tabbody.browser .bar input:focus { border-color: var(--accent); }
178
+ .tabbody.browser iframe { flex: 1; border: 0; background: #fff; width: 100%; min-height: 0; }
179
+ .tabbody.browser iframe[hidden] { display: none; }
180
+ .tabbody.browser .empty { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; color: var(--dim); text-align: center; padding: 20px; gap: 8px; font-size: 13px; }
181
+ .tabbody.browser .empty[hidden] { display: none; }
182
+ .tabbody.browser .empty .urls { display: flex; flex-direction: column; gap: 4px; }
183
+ .tabbody.term { background: #0a0c0d; }
184
+ .tabbody.term .out { flex: 1; overflow-y: auto; margin: 0; padding: 10px 12px; white-space: pre-wrap; word-break: break-word; font-size: 12.5px; line-height: 1.4; font-family: inherit; }
185
+ .tabbody.term .out .l { min-height: 1.4em; }
186
+ .tabbody.term .trow { display: flex; gap: 8px; padding: 6px 12px 8px; border-top: 1px solid var(--line); align-items: baseline; flex: none; }
187
+ .tabbody.term .prompt { color: var(--accent); white-space: nowrap; max-width: 45%; overflow: hidden; text-overflow: ellipsis; font-size: 12.5px; }
188
+ .tabbody.term input { flex: 1; min-width: 0; background: transparent; border: 0; outline: 0; color: var(--fg); font: inherit; font-size: 12.5px; }
189
+ .ab { font-weight: 700; } .ad { opacity: .6; }
190
+ .a30 { color: #3b4048; } .a31 { color: var(--red); } .a32 { color: var(--green); } .a33 { color: var(--yellow); }
191
+ .a34 { color: #7aa2f7; } .a35 { color: var(--magenta); } .a36 { color: var(--accent); } .a37 { color: #c0caf5; }
192
+ .a90 { color: var(--gray); } .a91 { color: #ff9e9e; } .a92 { color: #b9f27c; } .a93 { color: #ffd580; }
193
+ .a94 { color: #8db4ff; } .a95 { color: #d0b4ff; } .a96 { color: #74e0f0; } .a97 { color: #eef3f5; }
194
+
195
+ /* Narrow windows: the sidebar floats over the chat instead of squeezing it,
196
+ and the panel cannot take more than half the width. */
197
+ @media (max-width: 1000px) {
198
+ #side { position: absolute; left: 0; top: 0; bottom: 0; z-index: 20; box-shadow: 8px 0 30px rgba(0,0,0,.5); }
199
+ #main { padding-left: 0; }
200
+ #sidetoggle { display: inline-flex; }
201
+ #panel { max-width: 55vw; }
202
+ }
203
+
204
+ /* ---- folder picker ---- */
205
+ #modal { position: fixed; inset: 0; background: rgba(0,0,0,.55); display: flex; align-items: center; justify-content: center; z-index: 50; }
206
+ #modal[hidden] { display: none; }
207
+ .dlg { width: min(680px, 92vw); max-height: 82vh; background: var(--box); border: 1px solid var(--line); border-radius: 6px; display: flex; flex-direction: column; box-shadow: 0 12px 40px rgba(0,0,0,.5); }
208
+ .dlghdr { display: flex; align-items: center; padding: 10px 14px; border-bottom: 1px solid var(--line); font-weight: 700; gap: 8px; }
209
+ .pathrow { display: flex; gap: 6px; padding: 10px 14px 6px; }
210
+ .pathrow input { flex: 1; background: var(--bg); border: 1px solid var(--line); color: var(--fg); font: inherit; font-size: 13px; padding: 4px 8px; border-radius: 4px; outline: 0; }
211
+ .pathrow input:focus { border-color: var(--accent); }
212
+ #fsroots { padding: 2px 14px 8px; display: flex; gap: 6px; flex-wrap: wrap; }
213
+ .chip { background: #1a2023; border: 1px solid var(--line); color: var(--dim); border-radius: 12px; padding: 1px 10px; font-size: 12px; cursor: pointer; }
214
+ .chip:hover { color: var(--fg); border-color: var(--accent); }
215
+ #fslist { flex: 1; overflow-y: auto; padding: 0 8px 8px; min-height: 240px; }
216
+ .fsitem { padding: 4px 8px; border-radius: 4px; cursor: pointer; display: flex; gap: 10px; align-items: baseline; }
217
+ .fsitem:hover { background: #1a2023; }
218
+ .fsitem .proj { color: var(--accent); font-size: 11px; }
219
+ .fsitem.up { color: var(--dim); }
220
+ .dlgfoot { display: flex; align-items: center; gap: 14px; padding: 10px 14px; border-top: 1px solid var(--line); font-size: 12.5px; color: var(--dim); }
221
+ .dlgfoot label { display: flex; align-items: center; gap: 6px; cursor: pointer; }
222
+ `;
@@ -0,0 +1,190 @@
1
+ "use strict";
2
+ // Embedded terminal for the web panel, without a PTY (that would need a native
3
+ // dependency). A persistent shell reads commands from a pipe: state such as
4
+ // cwd and env carries across commands, output streams back, and a sentinel
5
+ // after every command reports its exit code and the shell's cwd. No TTY means
6
+ // no colors from most tools and no interactive programs — the panel says so.
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.Terminal = void 0;
9
+ exports.toOsPath = toOsPath;
10
+ exports.stripControl = stripControl;
11
+ const child_process_1 = require("child_process");
12
+ const shell_1 = require("../tools/shell");
13
+ const RS = "\x1e"; // record separator — never appears in normal output
14
+ const SENTINEL = /\x1e(-?\d+)\x1e([^\x1e]*)\x1e\r?\n?/;
15
+ const BUFFER_CAP = 200_000;
16
+ const HOLD_CAP = 600; // a partial sentinel is held back at most this long
17
+ class Terminal {
18
+ id;
19
+ hooks;
20
+ cwd;
21
+ /** Recent output for replay when a page (re)connects. */
22
+ buffer = "";
23
+ closed = false;
24
+ shell;
25
+ proc = null;
26
+ acc = "";
27
+ kind;
28
+ interrupting = false;
29
+ spawnedAt = 0;
30
+ quickExits = 0;
31
+ constructor(id, cwd, hooks) {
32
+ this.id = id;
33
+ this.hooks = hooks;
34
+ this.cwd = cwd;
35
+ this.shell = (0, shell_1.pickShell)();
36
+ this.kind = /powershell|pwsh/i.test(this.shell.exe) ? "powershell" : "posix";
37
+ this.emit(`\x1b[2m${this.shell.label} · no TTY: interactive programs will not work · ctrl+c interrupts\x1b[0m\n`);
38
+ this.spawn();
39
+ }
40
+ spawn() {
41
+ const args = this.kind === "posix" ? ["-l"] : ["-NoProfile", "-NonInteractive", "-Command", "-"];
42
+ this.spawnedAt = Date.now();
43
+ let proc;
44
+ try {
45
+ proc = (0, child_process_1.spawn)(this.shell.exe, args, {
46
+ cwd: this.cwd,
47
+ env: { ...process.env, TERM: "dumb" },
48
+ detached: process.platform !== "win32",
49
+ windowsHide: true,
50
+ stdio: ["pipe", "pipe", "pipe"],
51
+ });
52
+ }
53
+ catch (err) {
54
+ this.emit(`[could not start ${this.shell.label}: ${err?.message ?? err}]\n`);
55
+ return;
56
+ }
57
+ this.proc = proc;
58
+ proc.stdout?.on("data", (d) => this.ingest(d.toString("utf8")));
59
+ proc.stderr?.on("data", (d) => this.ingest(d.toString("utf8")));
60
+ proc.on("error", (err) => this.emit(`[could not start ${this.shell.label}: ${err.message}]\n`));
61
+ proc.on("close", (code) => {
62
+ if (this.proc !== proc)
63
+ return;
64
+ this.proc = null;
65
+ if (this.acc) {
66
+ this.emit(this.acc);
67
+ this.acc = "";
68
+ }
69
+ if (this.closed)
70
+ return;
71
+ if (this.interrupting) {
72
+ this.interrupting = false;
73
+ this.emit("\n\x1b[33m[interrupted]\x1b[0m\n");
74
+ this.spawn();
75
+ return;
76
+ }
77
+ // A shell that dies instantly, twice, is not going to work — stop.
78
+ const quick = Date.now() - this.spawnedAt < 1500;
79
+ this.quickExits = quick ? this.quickExits + 1 : 0;
80
+ if (this.quickExits >= 2) {
81
+ this.emit(`\n\x1b[31m[${this.shell.label} keeps exiting (code ${code}) — close this tab and open a new terminal]\x1b[0m\n`);
82
+ return;
83
+ }
84
+ this.emit(`\n\x1b[2m[shell exited with code ${code} — starting a new one]\x1b[0m\n`);
85
+ this.spawn();
86
+ });
87
+ }
88
+ /** Run one line. Echoed into the stream first, so every connected page (and
89
+ * the replay) shows the command with its output. */
90
+ write(line) {
91
+ if (this.closed)
92
+ return;
93
+ const cmd = line.replace(/\r?\n$/, "");
94
+ this.emit(`\x1b[36m❯\x1b[0m ${cmd}\n`);
95
+ if (!this.proc?.stdin?.writable) {
96
+ this.emit("\x1b[31m[no shell running]\x1b[0m\n");
97
+ return;
98
+ }
99
+ const payload = this.kind === "posix"
100
+ ? // The group redirect keeps a stdin-hungry command (cat, ssh) from
101
+ // eating the next command off our pipe; 2>&1 keeps output ordered.
102
+ `{\n${cmd}\n} </dev/null 2>&1\nprintf '${RS}%s${RS}%s${RS}\\n' "$?" "$PWD"\n`
103
+ : `${cmd}\nWrite-Output ([string][char]0x1e + $(if ($?) {0} else {1}) + [char]0x1e + (Get-Location).Path + [char]0x1e)\n`;
104
+ try {
105
+ this.proc.stdin.write(payload);
106
+ }
107
+ catch (err) {
108
+ this.emit(`[could not write to the shell: ${err?.message ?? err}]\n`);
109
+ }
110
+ }
111
+ /** ctrl+c: kill the shell (and whatever it is running), start a fresh one
112
+ * in the last known cwd. */
113
+ interrupt() {
114
+ if (this.closed)
115
+ return;
116
+ if (!this.proc) {
117
+ this.spawn();
118
+ return;
119
+ }
120
+ this.interrupting = true;
121
+ if (this.proc.pid)
122
+ (0, shell_1.killTree)(this.proc.pid);
123
+ }
124
+ close() {
125
+ this.closed = true;
126
+ if (this.proc?.pid)
127
+ (0, shell_1.killTree)(this.proc.pid);
128
+ this.proc = null;
129
+ }
130
+ ingest(text) {
131
+ this.acc += text;
132
+ for (;;) {
133
+ const m = SENTINEL.exec(this.acc);
134
+ if (!m)
135
+ break;
136
+ const before = this.acc.slice(0, m.index);
137
+ if (before)
138
+ this.emit(before);
139
+ this.acc = this.acc.slice(m.index + m[0].length);
140
+ const cwd = toOsPath(m[2].trim()) || this.cwd;
141
+ this.cwd = cwd;
142
+ this.hooks.done(Number(m[1]), cwd);
143
+ }
144
+ // Hold back a partial sentinel that may complete with the next chunk,
145
+ // but never for long — a stray RS in real output must not stall the view.
146
+ const i = this.acc.indexOf(RS);
147
+ if (i < 0) {
148
+ if (this.acc)
149
+ this.emit(this.acc);
150
+ this.acc = "";
151
+ }
152
+ else {
153
+ if (i > 0)
154
+ this.emit(this.acc.slice(0, i));
155
+ this.acc = this.acc.slice(i);
156
+ if (this.acc.length > HOLD_CAP) {
157
+ this.emit(this.acc);
158
+ this.acc = "";
159
+ }
160
+ }
161
+ }
162
+ emit(text) {
163
+ text = stripControl(text);
164
+ if (!text)
165
+ return;
166
+ this.buffer += text;
167
+ if (this.buffer.length > BUFFER_CAP)
168
+ this.buffer = this.buffer.slice(-Math.floor(BUFFER_CAP * 0.8));
169
+ this.hooks.output(text);
170
+ }
171
+ }
172
+ exports.Terminal = Terminal;
173
+ /** Git Bash reports /c/x for C:\x; spawn() needs the Windows form. */
174
+ function toOsPath(p) {
175
+ if (process.platform === "win32") {
176
+ const m = /^\/([a-zA-Z])(\/.*)?$/.exec(p);
177
+ if (m)
178
+ return `${m[1].toUpperCase()}:${(m[2] ?? "/").replace(/\//g, "\\")}`;
179
+ }
180
+ return p;
181
+ }
182
+ /** Keep SGR color codes (the page renders them) and \r (progress bars), drop
183
+ * every other escape sequence: cursor moves, screen clears, OSC titles. */
184
+ function stripControl(s) {
185
+ return s
186
+ .replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "")
187
+ .replace(/\x1b\[[0-9;?]*[ -/]*[@-ln-~]/g, "")
188
+ .replace(/\x1b[^[\]]/g, "")
189
+ .replace(/[\x00-\x08\x0b\x0c\x0e-\x1a\x1c-\x1f]/g, "");
190
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smolcoder",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "A smol, zero-config CLI coding agent for local models (Ollama & LM Studio). It just works.",
5
5
  "keywords": [
6
6
  "cli",