mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Redesign action queue UI with improved status feedback
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import 'highlight.js/styles/atom-one-light.css';
|
||||
import type { ParsedItem, Action } from './parser';
|
||||
import { Badge, Card, Icon, Layout, Tag } from '@appwrite.io/pink-svelte';
|
||||
import type { ParsedItem } from './parser';
|
||||
import { Card, Layout, ShimmerText, Spinner, Typography } from '@appwrite.io/pink-svelte';
|
||||
import Markdown, { type Plugin } from 'svelte-exmarkdown';
|
||||
import Li from './(markdown)/Li.svelte';
|
||||
import H1 from './(markdown)/H1.svelte';
|
||||
@@ -12,7 +12,6 @@
|
||||
import Ul from './(markdown)/Ul.svelte';
|
||||
import Ol from './(markdown)/Ol.svelte';
|
||||
import rehypeHighlight from 'rehype-highlight';
|
||||
import { IconCheck, IconClock, IconCog } from '@appwrite.io/pink-icons-svelte';
|
||||
import { queue } from './queue.svelte';
|
||||
import { synapse } from '../synapse.svelte';
|
||||
import { filesystem } from '$lib/components/editor/filesystem';
|
||||
@@ -23,27 +22,20 @@
|
||||
let { message }: Props = $props();
|
||||
const tickets = $state([message.group]);
|
||||
|
||||
// Process one item from the queue
|
||||
async function processQueueItem() {
|
||||
// Try to get a ticket - if none available, return immediately
|
||||
if (tickets.length === 0 || !message.group) return;
|
||||
|
||||
// Atomically get the ticket (pop removes and returns the last item)
|
||||
const ticket = tickets.pop();
|
||||
if (!ticket) return;
|
||||
|
||||
// Check if there are waiting items in this group
|
||||
const list = queue.lists[message.group];
|
||||
if (!list || !list.some((item) => item.status === 'waiting')) {
|
||||
// No items to process, return the ticket
|
||||
tickets.push(ticket);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the next item to process
|
||||
const item = queue.dequeue(message.group);
|
||||
if (!item) {
|
||||
// No items to dequeue, return the ticket
|
||||
tickets.push(ticket);
|
||||
return;
|
||||
}
|
||||
@@ -77,38 +69,31 @@
|
||||
break;
|
||||
}
|
||||
|
||||
// Mark job as done
|
||||
if (message.group) {
|
||||
queue.update(message.group, item.id, 'done');
|
||||
queue.update(message.group, item.id, { status: 'done' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing queue item:', error);
|
||||
|
||||
// Mark as done even on error
|
||||
if (message.group) {
|
||||
queue.update(message.group, item.id, 'done');
|
||||
queue.update(message.group, item.id, { status: 'done' });
|
||||
}
|
||||
} finally {
|
||||
// Return the ticket to the pool
|
||||
tickets.push(ticket);
|
||||
|
||||
// Check for more items with a small delay
|
||||
setTimeout(checkForMoreItems, 100);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for more items to process
|
||||
function checkForMoreItems() {
|
||||
if (!message.group) return;
|
||||
|
||||
// Check if there's anything to process and tickets are available
|
||||
const list = queue.lists[message.group];
|
||||
if (tickets.length > 0 && list && list.some((item) => item.status === 'waiting')) {
|
||||
processQueueItem();
|
||||
}
|
||||
}
|
||||
|
||||
// Reactive effect to watch for changes and trigger processing
|
||||
$effect(() => {
|
||||
if (!('type' in message)) return;
|
||||
if (message.type !== 'actions') return;
|
||||
@@ -117,14 +102,11 @@
|
||||
const list = queue.lists[message.group];
|
||||
if (!list) return;
|
||||
|
||||
// Try to process whenever the queue changes if tickets are available
|
||||
if (tickets.length > 0 && list.some((item) => item.status === 'waiting')) {
|
||||
processQueueItem();
|
||||
}
|
||||
});
|
||||
|
||||
// No need for cleanup since tickets are automatically returned in finally block
|
||||
|
||||
const plugins: Plugin[] = [
|
||||
{
|
||||
rehypePlugin: rehypeHighlight,
|
||||
@@ -145,28 +127,55 @@
|
||||
{#if 'type' in message}
|
||||
{#key message.content}
|
||||
{#if message.type === 'actions'}
|
||||
<Card.Base variant="secondary" padding="xs">
|
||||
{#each message.actions as action}
|
||||
{@const actionInQueue = queue.lists[action.group].find(
|
||||
(n) => n.data.id === action.id
|
||||
)}
|
||||
{#if actionInQueue}
|
||||
<Layout.Stack direction="row" gap="xxs" alignItems="center">
|
||||
{#if actionInQueue.status === 'waiting'}
|
||||
<Icon icon={IconCog} />
|
||||
{:else if actionInQueue.status === 'processing'}
|
||||
<Icon icon={IconClock} />
|
||||
{:else if actionInQueue.status === 'done'}
|
||||
<Icon icon={IconCheck} />
|
||||
{/if}
|
||||
{#if action.type === 'file'}
|
||||
<Badge content={action.src} variant="secondary" />
|
||||
{:else if action.type === 'shell'}
|
||||
<code>{action.content}</code>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
{/each}
|
||||
<Card.Base variant="primary" padding="none">
|
||||
<Card.Base variant="secondary" padding="xs">
|
||||
<Layout.Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between">
|
||||
<Typography.Text variant="m-500">Version 0</Typography.Text>
|
||||
{#if message.complete}
|
||||
<Spinner />
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Card.Base>
|
||||
<div class="actions">
|
||||
{#each message.actions as action}
|
||||
{@const actionInQueue = queue.lists[action.group]?.find(
|
||||
(n) => n.data.id === action.id
|
||||
)}
|
||||
{#if actionInQueue}
|
||||
<Layout.Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between">
|
||||
{#if action.type === 'file'}
|
||||
<Typography.Code size="s">{action.src}</Typography.Code>
|
||||
<Typography.Code size="s">
|
||||
{#if actionInQueue.status === 'waiting'}
|
||||
Waiting
|
||||
{:else if actionInQueue.status === 'processing'}
|
||||
<ShimmerText>Generating</ShimmerText>
|
||||
{:else if actionInQueue.status === 'done'}
|
||||
Generated
|
||||
{/if}
|
||||
</Typography.Code>
|
||||
{:else if action.type === 'shell'}
|
||||
<Typography.Code size="s">{action.content}</Typography.Code>
|
||||
<Typography.Code size="s">
|
||||
{#if actionInQueue.status === 'waiting'}
|
||||
Waiting
|
||||
{:else if actionInQueue.status === 'processing'}
|
||||
<ShimmerText>Running</ShimmerText>
|
||||
{:else if actionInQueue.status === 'done'}
|
||||
Finished
|
||||
{/if}
|
||||
</Typography.Code>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</Card.Base>
|
||||
{/if}
|
||||
{/key}
|
||||
@@ -206,6 +215,10 @@
|
||||
0px 1.022px 4.089px -1.022px rgba(55, 59, 77, 0.1);
|
||||
}
|
||||
|
||||
.actions {
|
||||
padding: var(--space-6);
|
||||
}
|
||||
|
||||
:global(pre) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||||
import {
|
||||
createStreamParser,
|
||||
StreamParser,
|
||||
type ParsedItem,
|
||||
type Action,
|
||||
type TextChunk
|
||||
type TextChunk,
|
||||
type ActionsContainer
|
||||
} from './parser';
|
||||
|
||||
function expectAction(item: ParsedItem): asserts item is Action {
|
||||
expect('type' in item).toBe(true);
|
||||
expect('type' in item && item.type !== 'actions').toBe(true);
|
||||
}
|
||||
|
||||
function expectActionsContainer(item: ParsedItem): asserts item is ActionsContainer {
|
||||
expect('type' in item && item.type === 'actions').toBe(true);
|
||||
}
|
||||
function expectText(item: ParsedItem): asserts item is TextChunk {
|
||||
expect(!('type' in item) && 'content' in item).toBe(true);
|
||||
@@ -21,6 +26,43 @@ describe('stream parser', () => {
|
||||
parser = createStreamParser();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should group multiple actions from the same source in one container', () => {
|
||||
// This test checks the actions container grouping functionality
|
||||
// Add multiple actions in separate chunks
|
||||
parser.chunk('Text before', 'system');
|
||||
parser.chunk('<action type="file" src="file1.js">code1</action>', 'system');
|
||||
parser.chunk('<action type="shell">npm install</action>', 'system');
|
||||
parser.chunk('Text after', 'system');
|
||||
parser.end();
|
||||
|
||||
let items: ParsedItem[] = [];
|
||||
parser.parsed.subscribe((value) => {
|
||||
items = value;
|
||||
})();
|
||||
|
||||
expect(items.length).toBe(3);
|
||||
const [text1, container, text2] = items;
|
||||
|
||||
expectText(text1);
|
||||
expectActionsContainer(container);
|
||||
expectText(text2);
|
||||
|
||||
expect(text1.content).toBe('Text before');
|
||||
expect(text2.content).toBe('Text after');
|
||||
|
||||
// Check that both actions are in the same container
|
||||
expect(container.actions.length).toBe(2);
|
||||
expect(container.actions[0].type).toBe('file');
|
||||
expect(container.actions[0].src).toBe('file1.js');
|
||||
expect(container.actions[0].content).toBe('code1');
|
||||
expect(container.actions[1].type).toBe('shell');
|
||||
expect(container.actions[1].content).toBe('npm install');
|
||||
});
|
||||
|
||||
it('should parse text chunks correctly', () => {
|
||||
const text = 'Hello, world!';
|
||||
parser.chunk(text, 'system');
|
||||
@@ -48,12 +90,14 @@ describe('stream parser', () => {
|
||||
|
||||
expect(items.length).toBe(1);
|
||||
|
||||
const [item] = items;
|
||||
expectAction(item);
|
||||
expect(item.type === 'file').toBe(true);
|
||||
expect(item.src).toBe('test.js');
|
||||
expect(item.content).toBe('console.log("test");');
|
||||
expect(item.complete).toBe(true);
|
||||
const [container] = items;
|
||||
expectActionsContainer(container);
|
||||
expect(container.actions.length).toBe(1);
|
||||
const action = container.actions[0];
|
||||
expect(action.type === 'file').toBe(true);
|
||||
expect(action.src).toBe('test.js');
|
||||
expect(action.content).toBe('console.log("test");');
|
||||
expect(action.complete).toBe(true);
|
||||
});
|
||||
|
||||
it('should parse shell actions correctly', () => {
|
||||
@@ -68,11 +112,13 @@ describe('stream parser', () => {
|
||||
|
||||
expect(items.length).toBe(1);
|
||||
|
||||
const [item] = items;
|
||||
expectAction(item);
|
||||
expect(item.type).toBe('shell');
|
||||
expect(item.content).toBe('npm install');
|
||||
expect(item.complete).toBe(true);
|
||||
const [container] = items;
|
||||
expectActionsContainer(container);
|
||||
expect(container.actions.length).toBe(1);
|
||||
const action = container.actions[0];
|
||||
expect(action.type).toBe('shell');
|
||||
expect(action.content).toBe('npm install');
|
||||
expect(action.complete).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle mixed content correctly', () => {
|
||||
@@ -86,14 +132,13 @@ describe('stream parser', () => {
|
||||
})();
|
||||
|
||||
expect(items.length).toBe(3);
|
||||
console.log(items);
|
||||
const [text1, action, text2] = items;
|
||||
expectAction(action);
|
||||
const [text1, container, text2] = items;
|
||||
expectActionsContainer(container);
|
||||
expectText(text1);
|
||||
expectText(text2);
|
||||
expect(text1.content).toBe('Text before ');
|
||||
expect(action.type).toBe('shell');
|
||||
expect(action.content).toBe('npm install');
|
||||
expect(container.actions[0].type).toBe('shell');
|
||||
expect(container.actions[0].content).toBe('npm install');
|
||||
expect(text2.content).toBe(' Text after');
|
||||
});
|
||||
|
||||
@@ -114,15 +159,15 @@ describe('stream parser', () => {
|
||||
|
||||
expect(items.length).toBe(3);
|
||||
|
||||
const [text1, action, text2] = items;
|
||||
expectAction(action);
|
||||
const [text1, container, text2] = items;
|
||||
expectActionsContainer(container);
|
||||
expectText(text1);
|
||||
expectText(text2);
|
||||
expect(text1.content).toBe('Text before ');
|
||||
expect(action.type).toBe('file');
|
||||
expect(action.src).toBe('test.js');
|
||||
expect(action.content).toBe('console.log("test");');
|
||||
expect(action.complete).toBe(true);
|
||||
expect(container.actions[0].type).toBe('file');
|
||||
expect(container.actions[0].src).toBe('test.js');
|
||||
expect(container.actions[0].content).toBe('console.log("test");');
|
||||
expect(container.actions[0].complete).toBe(true);
|
||||
expect(text2.content).toBe(' Text after');
|
||||
});
|
||||
|
||||
@@ -140,7 +185,10 @@ describe('stream parser', () => {
|
||||
});
|
||||
|
||||
it('should trim newlines correctly from action content', () => {
|
||||
const text = '<action type="file" src="test.js">\nconsole.log("test");\n</action>';
|
||||
// This test verifies that only leading and trailing newlines are trimmed
|
||||
// but internal newlines are preserved
|
||||
const text =
|
||||
'<action type="file" src="test.js">\nconsole.log("test1");\nconsole.log("test2");\n</action>';
|
||||
parser.chunk(text, 'system');
|
||||
parser.end();
|
||||
|
||||
@@ -151,8 +199,10 @@ describe('stream parser', () => {
|
||||
|
||||
expect(items.length).toBe(1);
|
||||
|
||||
const [item] = items;
|
||||
expect(item.content).toBe('console.log("test");');
|
||||
const [container] = items;
|
||||
expectActionsContainer(container);
|
||||
// The leading \n and trailing \n should be removed, but the middle \n should be preserved
|
||||
expect(container.actions[0].content).toBe('console.log("test1");\nconsole.log("test2");');
|
||||
});
|
||||
|
||||
it('should reset parser state correctly', () => {
|
||||
@@ -184,13 +234,6 @@ APPWRITE_PROJECT_ID=34534534534
|
||||
<action type="shell">
|
||||
npm install appwrite
|
||||
</action>
|
||||
|
||||
Some text in between actions.
|
||||
|
||||
<action type="file" src="src/lib/appwrite.ts">
|
||||
import { Client } from 'appwrite';
|
||||
const client = new Client();
|
||||
</action>
|
||||
`;
|
||||
|
||||
parser.chunk(complexExample, 'system');
|
||||
@@ -201,33 +244,21 @@ const client = new Client();
|
||||
items = value;
|
||||
})();
|
||||
|
||||
expect(items.length).toBe(5);
|
||||
expect(items.length).toBe(2);
|
||||
const [text, container] = items;
|
||||
|
||||
const [text1, action1, action2, text2, action3] = items;
|
||||
expectText(text);
|
||||
expect(text.content.trim()).toBe("Thanks! I'll help you create a full-featured Todo app.");
|
||||
|
||||
expectText(text1);
|
||||
expect(text1.content.trim()).toBe("Thanks! I'll help you create a full-featured Todo app.");
|
||||
|
||||
expectAction(action1);
|
||||
expectActionsContainer(container);
|
||||
const [action1, action2] = container.actions;
|
||||
expect(action1.type).toBe('file');
|
||||
expect(action1.src).toBe('.env');
|
||||
expect(action1.content).toBe(
|
||||
'APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1\nAPPWRITE_PROJECT_ID=34534534534'
|
||||
);
|
||||
|
||||
expectAction(action2);
|
||||
expect(action2.type).toBe('shell');
|
||||
expect(action2.content).toBe('npm install appwrite');
|
||||
|
||||
expectText(text2);
|
||||
expect(text2.content.trim()).toBe('Some text in between actions.');
|
||||
|
||||
expectAction(action3);
|
||||
expect(action3.type).toBe('file');
|
||||
expect(action3.src).toBe('src/lib/appwrite.ts');
|
||||
expect(action3.content).toBe(
|
||||
"import { Client } from 'appwrite';\nconst client = new Client();"
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle nested HTML correctly', () => {
|
||||
@@ -243,10 +274,11 @@ const client = new Client();
|
||||
|
||||
expect(items.length).toBe(1);
|
||||
|
||||
const [item] = items;
|
||||
expectAction(item);
|
||||
expect(item.type).toBe('file');
|
||||
expect(item.content).toBe('function Component() { return <div>Hello</div>; }');
|
||||
const [container] = items;
|
||||
expectActionsContainer(container);
|
||||
const action = container.actions[0];
|
||||
expect(action.type).toBe('file');
|
||||
expect(action.content).toBe('function Component() { return <div>Hello</div>; }');
|
||||
});
|
||||
|
||||
it('should handle partially closed action tags correctly', () => {
|
||||
@@ -260,10 +292,11 @@ const client = new Client();
|
||||
})();
|
||||
|
||||
expect(items.length).toBe(1);
|
||||
const [item] = items;
|
||||
expectAction(item);
|
||||
expect(item.complete).toBe(true);
|
||||
expect(item.content).toBe('console.log("test");');
|
||||
const [container] = items;
|
||||
expectActionsContainer(container);
|
||||
const action = container.actions[0];
|
||||
expect(action.complete).toBe(true);
|
||||
expect(action.content).toBe('console.log("test");');
|
||||
});
|
||||
|
||||
it('should handle multiple adjacent actions without text between', () => {
|
||||
@@ -278,14 +311,14 @@ const client = new Client();
|
||||
items = value;
|
||||
})();
|
||||
|
||||
expect(items.length).toBe(2);
|
||||
const [action1, action2] = items;
|
||||
expectAction(action1);
|
||||
expectAction(action2);
|
||||
expect(action1.src).toBe('file1.js');
|
||||
expect(action1.content).toBe('code1');
|
||||
expect(action2.src).toBe('file2.js');
|
||||
expect(action2.content).toBe('code2');
|
||||
expect(items.length).toBe(1);
|
||||
const [container] = items;
|
||||
expectActionsContainer(container);
|
||||
expect(container.actions.length).toBe(2);
|
||||
expect(container.actions[0].src).toBe('file1.js');
|
||||
expect(container.actions[0].content).toBe('code1');
|
||||
expect(container.actions[1].src).toBe('file2.js');
|
||||
expect(container.actions[1].content).toBe('code2');
|
||||
});
|
||||
|
||||
it('should handle empty action content correctly', () => {
|
||||
@@ -298,10 +331,11 @@ const client = new Client();
|
||||
})();
|
||||
|
||||
expect(items.length).toBe(1);
|
||||
const [item] = items;
|
||||
expectAction(item);
|
||||
expect(item.content).toBe('');
|
||||
expect(item.complete).toBe(true);
|
||||
const [container] = items;
|
||||
expectActionsContainer(container);
|
||||
const action = container.actions[0];
|
||||
expect(action.content).toBe(''); // Empty content should remain exactly empty
|
||||
expect(action.complete).toBe(true);
|
||||
});
|
||||
|
||||
it('should filter out empty text chunks', () => {
|
||||
@@ -315,9 +349,10 @@ const client = new Client();
|
||||
|
||||
// Only the action should remain, empty text chunks should be filtered
|
||||
expect(items.length).toBe(1);
|
||||
const [item] = items;
|
||||
expectAction(item);
|
||||
expect(item.content).toBe('command');
|
||||
const [container] = items;
|
||||
expectActionsContainer(container);
|
||||
const action = container.actions[0];
|
||||
expect(action.content).toBe('command');
|
||||
});
|
||||
|
||||
it('should handle malformed action tags gracefully', () => {
|
||||
@@ -331,9 +366,10 @@ const client = new Client();
|
||||
})();
|
||||
|
||||
expect(items.length).toBe(1);
|
||||
const [item] = items;
|
||||
expectAction(item);
|
||||
expect(item.content).toBe('content');
|
||||
const [container] = items;
|
||||
expectActionsContainer(container);
|
||||
const action = container.actions[0];
|
||||
expect(action.content).toBe('content');
|
||||
});
|
||||
|
||||
it('should handle large content streaming properly', () => {
|
||||
@@ -350,10 +386,10 @@ const client = new Client();
|
||||
})();
|
||||
|
||||
expect(items.length).toBe(2);
|
||||
const [text, action] = items;
|
||||
const [text, container] = items;
|
||||
expectText(text);
|
||||
expectAction(action);
|
||||
expect(action.content.length).toBe(1000);
|
||||
expectActionsContainer(container);
|
||||
expect(container.actions[0].content.length).toBe(1000);
|
||||
});
|
||||
|
||||
it('should handle end() call with incomplete action', () => {
|
||||
@@ -367,10 +403,11 @@ const client = new Client();
|
||||
})();
|
||||
|
||||
expect(items.length).toBe(1);
|
||||
const [item] = items;
|
||||
expectAction(item);
|
||||
expect(item.content).toBe('content');
|
||||
expect(item.complete).toBe(true);
|
||||
const [container] = items;
|
||||
expectActionsContainer(container);
|
||||
const action = container.actions[0];
|
||||
expect(action.content).toBe('content');
|
||||
expect(action.complete).toBe(true);
|
||||
});
|
||||
|
||||
it('should mark TextChunks as complete when end() is called', () => {
|
||||
@@ -401,12 +438,13 @@ const client = new Client();
|
||||
})();
|
||||
|
||||
expect(items.length).toBe(3);
|
||||
const [text1, action, text2] = items;
|
||||
const [text1, container, text2] = items;
|
||||
expectText(text1);
|
||||
expectAction(action);
|
||||
expectActionsContainer(container);
|
||||
expectText(text2);
|
||||
expect(text1.complete).toBe(true);
|
||||
expect(action.complete).toBe(true);
|
||||
expect(container.complete).toBe(true);
|
||||
expect(container.actions[0].complete).toBe(true);
|
||||
expect(text2.complete).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ export function isActionType(type: string): type is ActionType {
|
||||
return ['file', 'shell'].includes(type);
|
||||
}
|
||||
|
||||
type ParserEvents = 'complete' | 'chunk';
|
||||
|
||||
export type ParsedItem = Action | TextChunk | ActionsContainer;
|
||||
|
||||
export class StreamParser {
|
||||
@@ -41,8 +43,9 @@ export class StreamParser {
|
||||
private skipCurrentAction = false;
|
||||
private pendingText = '';
|
||||
private callbacksEnabled = true;
|
||||
private callbacks: Record<'complete', Array<(action: Action) => void | Promise<void>>> = {
|
||||
complete: []
|
||||
private callbacks: Record<ParserEvents, Array<(action: Action) => void | Promise<void>>> = {
|
||||
complete: [],
|
||||
chunk: []
|
||||
};
|
||||
/**
|
||||
* The store of parsed items that can be subscribed to for reactivity
|
||||
@@ -58,10 +61,10 @@ export class StreamParser {
|
||||
|
||||
/**
|
||||
* Register a callback to be called when an action is completed
|
||||
* @param event The event to listen for ('complete')
|
||||
* @param event The event to listen for
|
||||
* @param callback Function to call when an action is completed
|
||||
*/
|
||||
public on(event: 'complete', callback: (action: Action) => void | Promise<void>): void {
|
||||
public on(event: ParserEvents, callback: (action: Action) => void | Promise<void>): void {
|
||||
this.callbacks[event].push(callback);
|
||||
}
|
||||
|
||||
|
||||
@@ -41,10 +41,22 @@ class Queue<T> {
|
||||
return item;
|
||||
}
|
||||
|
||||
public update(list: keyof Lists<T>, id: symbol, status: Item<T>['status']): void {
|
||||
public update(
|
||||
list: keyof Lists<T>,
|
||||
id: symbol,
|
||||
value?: {
|
||||
status?: Item<T>['status'];
|
||||
data?: T;
|
||||
}
|
||||
): void {
|
||||
const item = this.lists[list].find((item) => item.id === id);
|
||||
if (item) {
|
||||
item.status = status;
|
||||
if (!item) return;
|
||||
|
||||
if (value.status) {
|
||||
item.status = value.status;
|
||||
}
|
||||
if (value.data) {
|
||||
item.data = value.data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +132,6 @@
|
||||
parser.end();
|
||||
}
|
||||
});
|
||||
|
||||
parser.on('complete', async (action) => {
|
||||
queue.enqueue(action.group, action);
|
||||
});
|
||||
|
||||
@@ -129,6 +129,7 @@ export class Synapse {
|
||||
}[T],
|
||||
options?: {
|
||||
timeout?: number;
|
||||
noReturn?: boolean;
|
||||
}
|
||||
): Promise<BaseMessage> {
|
||||
const requestId = String(Date.now().toString() + ++this.requestCounter);
|
||||
@@ -140,8 +141,14 @@ export class Synapse {
|
||||
requestId
|
||||
};
|
||||
const response = new Promise<BaseMessage>((resolve, reject) => {
|
||||
const noReturn = options?.noReturn === true;
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('Request timed out'));
|
||||
if (noReturn) return;
|
||||
reject(
|
||||
new Error('Request timed out', {
|
||||
cause: message.operation
|
||||
})
|
||||
);
|
||||
}, options?.timeout ?? 5000);
|
||||
const callback = (message: MessageEvent<string>) => {
|
||||
const response = JSON.parse(message.data);
|
||||
@@ -161,7 +168,8 @@ export class Synapse {
|
||||
finish();
|
||||
}
|
||||
};
|
||||
this.ws.addEventListener('message', callback);
|
||||
if (!noReturn) this.ws.addEventListener('message', callback);
|
||||
else resolve(null);
|
||||
});
|
||||
this.ws.send(JSON.stringify(message));
|
||||
|
||||
|
||||
@@ -33,20 +33,32 @@
|
||||
domEvent.stopPropagation();
|
||||
return;
|
||||
}
|
||||
synapse.dispatch('terminal', {
|
||||
operation: 'createCommand',
|
||||
params: {
|
||||
command: key
|
||||
synapse.dispatch(
|
||||
'terminal',
|
||||
{
|
||||
operation: 'createCommand',
|
||||
params: {
|
||||
command: key
|
||||
}
|
||||
},
|
||||
{
|
||||
noReturn: true
|
||||
}
|
||||
});
|
||||
);
|
||||
});
|
||||
term.textarea.addEventListener('paste', (e) => {
|
||||
synapse.dispatch('terminal', {
|
||||
operation: 'createCommand',
|
||||
params: {
|
||||
command: e.clipboardData.getData('text/plain')
|
||||
synapse.dispatch(
|
||||
'terminal',
|
||||
{
|
||||
operation: 'createCommand',
|
||||
params: {
|
||||
command: e.clipboardData.getData('text/plain')
|
||||
}
|
||||
},
|
||||
{
|
||||
noReturn: true
|
||||
}
|
||||
});
|
||||
);
|
||||
});
|
||||
synapse.addEventListener('terminal', ({ message }) => {
|
||||
const { data } = message;
|
||||
@@ -57,13 +69,19 @@
|
||||
if (term.cols === cols && term.rows === rows) return;
|
||||
if (!Number.isInteger(cols) || !Number.isInteger(rows)) return;
|
||||
term.resize(cols - 1, rows - 1);
|
||||
synapse.dispatch('terminal', {
|
||||
operation: 'updateSize',
|
||||
params: {
|
||||
rows: rows - 0.5,
|
||||
cols: cols - 0.5
|
||||
synapse.dispatch(
|
||||
'terminal',
|
||||
{
|
||||
operation: 'updateSize',
|
||||
params: {
|
||||
rows: rows - 0.5,
|
||||
cols: cols - 0.5
|
||||
}
|
||||
},
|
||||
{
|
||||
noReturn: true
|
||||
}
|
||||
});
|
||||
);
|
||||
}, 50);
|
||||
|
||||
const observer = new ResizeObserver(resizer);
|
||||
|
||||
@@ -58,12 +58,18 @@
|
||||
const terminals = new SvelteMap<symbol, Synapse>();
|
||||
let currentTerminal: symbol = $state(mainTerminalId);
|
||||
synapse
|
||||
.dispatch('synapse', {
|
||||
operation: 'updateWorkDir',
|
||||
params: {
|
||||
workDir: `/artifact/${page.params.artifact}`
|
||||
.dispatch(
|
||||
'synapse',
|
||||
{
|
||||
operation: 'updateWorkDir',
|
||||
params: {
|
||||
workDir: `/artifact/${page.params.artifact}`
|
||||
}
|
||||
},
|
||||
{
|
||||
noReturn: true
|
||||
}
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
return synapse.dispatch('fs', {
|
||||
operation: 'getFolder',
|
||||
|
||||
Reference in New Issue
Block a user