Antigravity CLI
DEMOAn authentic, character-grid-aligned terminal interface modeled after Antigravity CLI (agy), composed entirely of standard svtui primitives (Input, Divider, Accordion, Card, RadioButtonGroup, Dialog, CodeBlock, BlockLoader).
Live Terminal Demo
▄▀▀▄ ▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀ ▄▀▀ ▀▀▄ ▄▀▀ ▀▀▄
>
agy.svelte
001 <script lang="ts">
002 import Card from '$lib/components/card.svelte';
003 import CodeBlock from '$lib/components/code-block.svelte';
004 import BlockLoader from '$lib/components/block-loader.svelte';
005 import Accordion from '$lib/components/accordion.svelte';
006 import Input from '$lib/components/input.svelte';
007 import Divider from '$lib/components/divider.svelte';
008 import RadioButtonGroup, { type RadioOption } from '$lib/components/radio-button-group.svelte';
009 import * as Dialog from '$lib/components/dialog/index.js';
010 import Button from '$lib/components/button.svelte';
011
012 // ── Types ─────────────────────────────────────────────
013 type Step = {
014 id: string;
015 kind: 'user' | 'thinking' | 'tool' | 'response' | 'system' | 'interview';
016 text?: string;
017 thinkDuration?: number;
018 thinkContent?: string;
019 toolName?: string;
020 toolArgs?: string;
021 toolOutput?: string;
022 question?: string;
023 options?: RadioOption[];
024 };
025
026 // ── State ─────────────────────────────────────────────
027 let inputVal = $state('');
028 let isRunning = $state(false);
029 let steps = $state<Step[]>([]);
030 let feedEl: HTMLDivElement;
031 let promptEl: HTMLDivElement;
032
033 let dialogOpen = $state(false);
034 let pendingCmd = $state('');
035
036 const uid = () => Math.random().toString(36).slice(2);
037
038 function scroll() {
039 setTimeout(() => promptEl?.scrollIntoView({ block: 'nearest' }), 30);
040 }
041
042 function push(...s: Step[]) {
043 steps = [...steps, ...s];
044 scroll();
045 }
046
047 function handleKey(e: KeyboardEvent) {
048 if (e.key === 'Enter') {
049 e.preventDefault();
050 submit();
051 }
052 }
053
054 function submit() {
055 const text = inputVal.trim();
056 if (!text || isRunning) return;
057 inputVal = '';
058
059 push({ id: uid(), kind: 'user', text });
060
061 if (text === '/clear') {
062 steps = [];
063 return;
064 }
065
066 if (text === '/plan') return doPlan();
067 if (text === '/tools') return doTools();
068 if (text === '/grill-me') return doInterview();
069
070 if (/\b(rm\s+-rf|delete|deploy|publish)\b/i.test(text)) {
071 pendingCmd = text;
072 dialogOpen = true;
073 return;
074 }
075
076 doNaturalTask(text);
077 }
078
079 // ── Simulated flows ──────────────────────────────────
080
081 function doPlan() {
082 isRunning = true;
083 scroll();
084 setTimeout(() => {
085 push(
086 {
087 id: uid(),
088 kind: 'thinking',
089 thinkDuration: 2,
090 thinkContent: "The user wants a plan. I'll outline the implementation steps."
091 },
092 {
093 id: uid(),
094 kind: 'response',
095 text: `Here's my implementation plan:
096
097 1. Survey existing component files in \`src/lib/components/\`
098 2. Validate self-containment with \`flatten.ts\`
099 3. Run \`bun run check\` and \`bun run build\`
100 4. Commit changes to feature branch
101
102 I'll start with step 1.`
103 }
104 );
105 isRunning = false;
106 }, 1200);
107 }
108
109 function doTools() {
110 isRunning = true;
111 scroll();
112 setTimeout(() => {
113 push(
114 { id: uid(), kind: 'thinking', thinkDuration: 1, thinkContent: 'Simple lookup.' },
115 {
116 id: uid(),
117 kind: 'tool',
118 toolName: 'ListDir',
119 toolArgs: '~/code/svtui/src/lib/components',
120 toolOutput: `accordion.svelte alert-banner.svelte ascii-canvas.svelte
121 badge.svelte bar-progress.svelte block-loader.svelte
122 breadcrumbs.svelte button.svelte card.svelte
123 checkbox.svelte code-block.svelte dialog/
124 divider.svelte input.svelte navigation.svelte
125 radio-button-group.svelte table/ text-area.svelte
126
127 6 directories, 15 files.`
128 },
129 {
130 id: uid(),
131 kind: 'response',
132 text: 'There are 21 components total — 15 single-file `.svelte` components plus `dialog/` and `table/` multi-file components.'
133 }
134 );
135 isRunning = false;
136 }, 1000);
137 }
138
139 function doInterview() {
140 push({
141 id: uid(),
142 kind: 'interview',
143 question: 'How should privileged shell commands be gated during autonomous execution?',
144 options: [
145 { value: 'confirm', label: 'Ask for confirmation each time' },
146 { value: 'session', label: 'Approve all for this session' },
147 { value: 'dryrun', label: 'Dry-run only (no execution)' }
148 ]
149 });
150 }
151
152 function doNaturalTask(instruction: string) {
153 isRunning = true;
154 scroll();
155
156 setTimeout(() => {
157 push({
158 id: uid(),
159 kind: 'thinking',
160 thinkDuration: 3,
161 thinkContent: `The user wants to "${instruction}". I'll search the codebase first, then make targeted edits.`
162 });
163 scroll();
164
165 setTimeout(() => {
166 push({
167 id: uid(),
168 kind: 'tool',
169 toolName: 'Search',
170 toolArgs: `"export default" in src/lib/components`,
171 toolOutput: `src/lib/components/button.svelte
172 src/lib/components/card.svelte
173 src/lib/components/input.svelte
174 src/lib/components/code-block.svelte`
175 });
176 scroll();
177
178 setTimeout(() => {
179 push(
180 {
181 id: uid(),
182 kind: 'tool',
183 toolName: 'Edit',
184 toolArgs: 'src/lib/components/input.svelte',
185 toolOutput: `--- a/src/lib/components/input.svelte
186 +++ b/src/lib/components/input.svelte
187 @@ -42,2 +42,4 @@
188 + const displayStr = $derived(
189 + type === 'password' ? '•'.repeat(value.length) : value
190 + );`
191 },
192 {
193 id: uid(),
194 kind: 'response',
195 text: `Done. Applied edit to \`input.svelte\`. All checks pass.`
196 }
197 );
198 isRunning = false;
199 }, 600);
200 }, 800);
201 }, 600);
202 }
203
204 function approveDialog() {
205 dialogOpen = false;
206 const cmd = pendingCmd;
207 pendingCmd = '';
208 isRunning = true;
209 scroll();
210 setTimeout(() => {
211 push({ id: uid(), kind: 'system', text: `Ran \`${cmd}\` — exited with code 0.` });
212 isRunning = false;
213 }, 600);
214 }
215
216 function rejectDialog() {
217 dialogOpen = false;
218 pendingCmd = '';
219 push({ id: uid(), kind: 'system', text: 'Aborted by user.' });
220 }
221 </script>
222
223 <div class="agy-terminal">
224 <!-- ── HEADER ────────────────────────────────── -->
225 <header class="header">
226 <pre class="logo"> <span style="color:rgb(219,177,49)">▄</span><span
227 style="color:rgb(242,146,46);background-color:rgb(246,145,46)">▀</span
228 ><span style="color:rgb(240,114,54);background-color:rgb(243,115,55)">▀</span><span
229 style="color:rgb(240,88,59)">▄</span
230 >
231 <span style="color:rgb(158,195,69);background-color:rgb(134,198,78)">▀</span><span
232 style="color:rgb(181,180,62);background-color:rgb(117,180,94)">▀</span
233 ><span style="color:rgb(226,153,61);background-color:rgb(204,149,77)">▀</span><span
234 style="color:rgb(246,122,52);background-color:rgb(239,121,71)">▀</span
235 ><span style="color:rgb(248,106,53);background-color:rgb(225,102,82)">▀</span><span
236 style="color:rgb(239,84,66);background-color:rgb(225,79,89)">▀</span
237 >
238 <span style="color:rgb(124,194,81);background-color:rgb(128,198,84)">▀</span><span
239 style="color:rgb(113,194,92);background-color:rgb(84,184,129)">▀</span
240 ><span style="color:rgb(92,169,143);background-color:rgb(64,151,222)">▀</span><span
241 style="color:rgb(92,145,179)">▀</span
242 ><span style="color:rgb(131,115,176)">▀</span><span
243 style="color:rgb(116,111,195);background-color:rgb(74,126,228)">▀</span
244 ><span style="color:rgb(153,93,168);background-color:rgb(112,110,206)">▀</span><span
245 style="color:rgb(156,91,151);background-color:rgb(143,100,180)">▀</span
246 >
247 <span style="color:rgb(109,198,148)">▄</span><span
248 style="color:rgb(97,195,125);background-color:rgb(98,186,213)">▀</span
249 ><span style="color:rgb(67,174,171);background-color:rgb(71,168,220)">▀</span> <span
250 style="color:rgb(74,128,234);background-color:rgb(61,137,251)">▀</span
251 ><span style="color:rgb(108,115,216);background-color:rgb(74,129,240)">▀</span><span
252 style="color:rgb(101,121,225)">▄</span
253 >
254 <span style="color:rgb(103,185,244)">▄</span><span
255 style="color:rgb(107,199,163);background-color:rgb(100,182,246)">▀</span
256 ><span style="color:rgb(100,182,246)">▀</span> <span style="color:rgb(56,134,251)"
257 >▀</span
258 ><span style="color:rgb(72,129,244);background-color:rgb(56,131,249)">▀</span><span
259 style="color:rgb(61,133,252)">▄</span
260 ></pre>
261 <div class="header-meta">
262 <span class="meta-line">Antigravity CLI 1.1.13</span>
263 <span class="meta-line dim">[email protected] (Google AI Pro)</span>
264 <span class="meta-line">Gemini 3.7 Flash · High</span>
265 <span class="meta-line dim">~/code/svtui</span>
266 </div>
267 </header>
268
269 <!-- ── FEED ──────────────────────────────────── -->
270 <div class="feed" class:has-items={steps.length > 0 || isRunning} bind:this={feedEl}>
271 {#each steps as step (step.id)}
272 <!-- USER INPUT -->
273 {#if step.kind === 'user'}
274 <Divider />
275 <div class="user-line">
276 <span class="prompt">></span>
277 <span class="user-text">{step.text}</span>
278 </div>
279
280 <!-- THINKING -->
281 {:else if step.kind === 'thinking'}
282 <div class="thinking">
283 <Accordion title="Thought for {step.thinkDuration}s">
284 <span class="dim">{step.thinkContent}</span>
285 </Accordion>
286 </div>
287
288 <!-- TOOL CALL -->
289 {:else if step.kind === 'tool'}
290 <div class="tool-line">
291 <Accordion title="{step.toolName}({step.toolArgs})">
292 <pre class="tool-output">{step.toolOutput}</pre>
293 </Accordion>
294 </div>
295
296 <!-- RESPONSE -->
297 {:else if step.kind === 'response'}
298 <div class="response">
299 <pre class="response-text">{step.text}</pre>
300 </div>
301
302 <!-- SYSTEM -->
303 {:else if step.kind === 'system'}
304 <div class="system-msg">
305 <span class="dim">{step.text}</span>
306 </div>
307
308 <!-- INTERVIEW -->
309 {:else if step.kind === 'interview'}
310 <div class="interview-box">
311 <Card title="Question">
312 <p class="q-text">{step.question}</p>
313 <RadioButtonGroup defaultValue="confirm" options={step.options ?? []} />
314 </Card>
315 </div>
316 {/if}
317 {/each}
318
319 {#if isRunning}
320 <div class="loader-line">
321 <BlockLoader />
322 </div>
323 {/if}
324 </div>
325
326 <!-- ── BOTTOM PROMPT ─────────────────────────── -->
327 <Divider />
328 <div class="prompt-row" bind:this={promptEl}>
329 <span class="prompt">></span>
330 <div class="input-wrap">
331 <Input
332 id="agy-input-demo"
333 placeholder=""
334 autofocus
335 bind:value={inputVal}
336 onkeydown={handleKey}
337 disabled={isRunning}
338 />
339 </div>
340 </div>
341 <Divider />
342 <div class="status-bar">
343 <span class="dim">? for shortcuts (/plan, /tools, /grill-me)</span>
344 <span class="dim">Gemini 3.7 Flash · High</span>
345 </div>
346
347 <!-- ── PERMISSION DIALOG ─────────────────────── -->
348 <Dialog.Root open={dialogOpen} onOpenChange={(v) => (dialogOpen = v)}>
349 <Dialog.Portal>
350 <Dialog.Overlay />
351 <Dialog.Content>
352 <Dialog.Title>Permission required</Dialog.Title>
353 <Dialog.Description>Allow this command to run?</Dialog.Description>
354 <div class="dialog-cmd">
355 <CodeBlock code={`$ ${pendingCmd}`} />
356 </div>
357 <div class="dialog-actions">
358 <Button variant="secondary" onclick={rejectDialog}>Reject</Button>
359 <Button variant="primary" onclick={approveDialog}>Allow</Button>
360 </div>
361 </Dialog.Content>
362 </Dialog.Portal>
363 </Dialog.Root>
364 </div>
365
366 <style>
367 .agy-terminal {
368 width: 100%;
369 box-sizing: border-box;
370 display: flex;
371 flex-direction: column;
372 font-family: var(--font-family-mono);
373 font-size: var(--font-size);
374 line-height: var(--line);
375 color: var(--text-primary);
376 }
377
378 .header {
379 display: flex;
380 gap: 3ch;
381 align-items: flex-start;
382 margin-bottom: var(--line);
383 height: calc(var(--line) * 5);
384 }
385
386 .logo {
387 margin: 0;
388 font-family: inherit;
389 font-size: inherit;
390 line-height: var(--line);
391 white-space: pre;
392 flex-shrink: 0;
393 height: calc(var(--line) * 5);
394 animation: rgb-breathe 6s ease-in-out infinite;
395 will-change: filter, opacity;
396 }
397
398 @keyframes rgb-breathe {
399 0% {
400 filter: hue-rotate(0deg) brightness(0.9);
401 opacity: 0.9;
402 }
403 50% {
404 filter: hue-rotate(180deg) brightness(1.25);
405 opacity: 1;
406 }
407 100% {
408 filter: hue-rotate(360deg) brightness(0.9);
409 opacity: 0.9;
410 }
411 }
412
413 .header-meta {
414 display: flex;
415 flex-direction: column;
416 height: calc(var(--line) * 5);
417 }
418
419 .meta-line {
420 height: var(--line);
421 line-height: var(--line);
422 }
423
424 .dim {
425 opacity: 0.5;
426 }
427
428 .feed {
429 display: flex;
430 flex-direction: column;
431 }
432
433 .feed.has-items {
434 margin-bottom: var(--line);
435 }
436
437 .user-line {
438 display: flex;
439 gap: 1ch;
440 height: var(--line);
441 line-height: var(--line);
442 margin-bottom: var(--line);
443 }
444
445 .user-text {
446 height: var(--line);
447 line-height: var(--line);
448 }
449
450 .prompt {
451 color: var(--text-primary);
452 font-weight: bold;
453 user-select: none;
454 flex-shrink: 0;
455 width: 1ch;
456 height: var(--line);
457 line-height: var(--line);
458 }
459
460 .thinking,
461 .tool-line {
462 display: flex;
463 flex-direction: column;
464 }
465
466 .tool-output {
467 margin: 0;
468 font-family: inherit;
469 font-size: inherit;
470 line-height: var(--line);
471 white-space: pre-wrap;
472 word-break: break-word;
473 padding: 0 2ch;
474 }
475
476 .response {
477 display: flex;
478 flex-direction: column;
479 }
480
481 .response-text {
482 margin: 0;
483 font-family: inherit;
484 font-size: inherit;
485 line-height: var(--line);
486 white-space: pre-wrap;
487 word-break: break-word;
488 }
489
490 .system-msg {
491 height: var(--line);
492 line-height: var(--line);
493 }
494
495 .interview-box {
496 margin: var(--line) 0;
497 }
498
499 .q-text {
500 margin: 0 0 var(--line) 0;
501 line-height: var(--line);
502 }
503
504 .loader-line {
505 height: var(--line);
506 display: flex;
507 align-items: center;
508 }
509
510 .prompt-row {
511 display: flex;
512 gap: 1ch;
513 align-items: center;
514 height: var(--line);
515 line-height: var(--line);
516 }
517
518 .input-wrap {
519 flex: 1;
520 min-width: 0;
521 height: var(--line);
522 }
523
524 .status-bar {
525 display: flex;
526 justify-content: space-between;
527 align-items: center;
528 height: var(--line);
529 line-height: var(--line);
530 }
531
532 .dialog-cmd {
533 margin: var(--line) 0;
534 }
535
536 .dialog-actions {
537 display: flex;
538 justify-content: flex-end;
539 gap: 1ch;
540 }
541 </style>
542