feat(initial): scaffold @push.rocks/smartchat with core, CLI, and web layers

Three-layer architecture built on @push.rocks/smartagent:
- ts/ — ChatSession wrapping runAgent() with conversation state management
- ts_cli/ — ink-based terminal chat TUI (React.createElement, no JSX)
- ts_web/ — Lit web components (smartchat-window, smartchat-message, smartchat-input)
This commit is contained in:
2026-03-06 23:20:12 +00:00
commit dd04edb420
24 changed files with 11344 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
export { SmartchatWindow } from './smartchat-window.js';
export { SmartchatMessage } from './smartchat-message.js';
export { SmartchatInput } from './smartchat-input.js';
+10
View File
@@ -0,0 +1,10 @@
// Lit
import { LitElement, html, css } from 'lit';
export { LitElement, html, css };
export type { TemplateResult, CSSResult } from 'lit';
// Core (cross-folder import)
import { ChatSession } from '../ts/smartchat.classes.chatsession.js';
export { ChatSession };
export type { IChatSessionOptions, IChatCallbacks, IChatUsage } from '../ts/smartchat.interfaces.js';
export type { IAgentRunResult } from '@push.rocks/smartagent';
+116
View File
@@ -0,0 +1,116 @@
import { LitElement, html, css } from './plugins.js';
import type { CSSResult, TemplateResult } from './plugins.js';
export class SmartchatInput extends LitElement {
declare disabled: boolean;
declare placeholder: string;
private value = '';
static properties = {
disabled: { type: Boolean },
placeholder: { type: String },
};
static styles: CSSResult = css`
:host {
display: block;
}
.input-row {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
background: var(--smartchat-input-bg, #1f2937);
border-radius: 8px;
border: 1px solid var(--smartchat-input-border, #374151);
}
.input-row:focus-within {
border-color: var(--smartchat-input-focus, #6366f1);
}
input {
flex: 1;
background: transparent;
border: none;
outline: none;
color: var(--smartchat-input-text, #e5e7eb);
font-size: 14px;
font-family: inherit;
}
input::placeholder {
color: var(--smartchat-input-placeholder, #6b7280);
}
button {
background: var(--smartchat-send-bg, #6366f1);
color: var(--smartchat-send-text, #fff);
border: none;
border-radius: 6px;
padding: 6px 16px;
cursor: pointer;
font-size: 14px;
}
button:hover {
opacity: 0.9;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
`;
constructor() {
super();
this.disabled = false;
this.placeholder = 'Type a message...';
}
private handleKeydown(e: KeyboardEvent) {
if (e.key === 'Enter' && !e.shiftKey && this.value.trim()) {
this.submit();
}
}
private handleInput(e: Event) {
this.value = (e.target as HTMLInputElement).value;
}
private submit() {
if (!this.value.trim() || this.disabled) return;
this.dispatchEvent(
new CustomEvent('send', {
detail: { text: this.value.trim() },
bubbles: true,
composed: true,
}),
);
this.value = '';
const input = this.shadowRoot?.querySelector('input');
if (input) input.value = '';
}
render(): TemplateResult {
return html`
<div class="input-row">
<input
type="text"
.value=${this.value}
@input=${this.handleInput}
@keydown=${this.handleKeydown}
placeholder=${this.disabled ? 'Waiting for response...' : this.placeholder}
?disabled=${this.disabled}
/>
<button @click=${this.submit} ?disabled=${this.disabled}>
Send
</button>
</div>
`;
}
}
customElements.define('smartchat-input', SmartchatInput);
+78
View File
@@ -0,0 +1,78 @@
import { LitElement, html, css } from './plugins.js';
import type { CSSResult, TemplateResult } from './plugins.js';
export class SmartchatMessage extends LitElement {
declare role: 'user' | 'assistant' | 'tool';
declare content: string;
declare toolName: string;
static properties = {
role: { type: String },
content: { type: String },
toolName: { type: String },
};
static styles: CSSResult = css`
:host {
display: block;
margin-bottom: 8px;
}
.message {
padding: 8px 12px;
border-radius: 8px;
max-width: 85%;
word-wrap: break-word;
white-space: pre-wrap;
}
.user {
background: var(--smartchat-user-bubble, #2563eb);
color: var(--smartchat-user-text, #fff);
margin-left: auto;
border-bottom-right-radius: 2px;
}
.assistant {
background: var(--smartchat-assistant-bubble, #374151);
color: var(--smartchat-assistant-text, #e5e7eb);
margin-right: auto;
border-bottom-left-radius: 2px;
}
.tool {
background: var(--smartchat-tool-bubble, #1e293b);
color: var(--smartchat-tool-text, #94a3b8);
margin-right: auto;
font-size: 0.85em;
font-family: monospace;
border-left: 3px solid var(--smartchat-tool-accent, #6366f1);
}
.tool-name {
font-weight: bold;
margin-bottom: 4px;
color: var(--smartchat-tool-accent, #6366f1);
}
`;
constructor() {
super();
this.role = 'user';
this.content = '';
this.toolName = '';
}
render(): TemplateResult {
return html`
<div class="message ${this.role}">
${this.role === 'tool' && this.toolName
? html`<div class="tool-name">${this.toolName}</div>`
: ''}
<div>${this.content}</div>
</div>
`;
}
}
customElements.define('smartchat-message', SmartchatMessage);
+179
View File
@@ -0,0 +1,179 @@
import { LitElement, html, css } from './plugins.js';
import type { CSSResult, TemplateResult, ChatSession, IAgentRunResult } from './plugins.js';
import './smartchat-message.js';
import './smartchat-input.js';
interface IDisplayMessage {
role: 'user' | 'assistant' | 'tool';
content: string;
toolName?: string;
}
export class SmartchatWindow extends LitElement {
declare chatSession: ChatSession;
declare placeholder: string;
private messages: IDisplayMessage[] = [];
private busy = false;
private streamingText = '';
static properties = {
chatSession: { attribute: false },
placeholder: { type: String },
};
static styles: CSSResult = css`
:host {
display: flex;
flex-direction: column;
height: 100%;
background: var(--smartchat-bg, #111827);
color: var(--smartchat-text, #e5e7eb);
font-family: var(--smartchat-font, system-ui, -apple-system, sans-serif);
border-radius: var(--smartchat-radius, 12px);
overflow: hidden;
}
.message-list {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 4px;
}
.empty-state {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
color: var(--smartchat-muted, #6b7280);
font-size: 14px;
}
.streaming {
padding: 8px 12px;
background: var(--smartchat-assistant-bubble, #374151);
color: var(--smartchat-assistant-text, #e5e7eb);
border-radius: 8px;
border-bottom-left-radius: 2px;
max-width: 85%;
white-space: pre-wrap;
}
.cursor {
display: inline-block;
width: 2px;
height: 1em;
background: var(--smartchat-cursor, #6366f1);
animation: blink 1s step-end infinite;
vertical-align: text-bottom;
}
@keyframes blink {
50% { opacity: 0; }
}
.input-area {
padding: 12px 16px;
border-top: 1px solid var(--smartchat-border, #1f2937);
}
`;
constructor() {
super();
this.placeholder = 'Type a message...';
}
connectedCallback() {
super.connectedCallback();
if (this.chatSession) {
this.chatSession.updateCallbacks({
onToken: (delta: string) => {
this.streamingText += delta;
this.requestUpdate();
this.scrollToBottom();
},
onToolCall: (name: string) => {
this.messages = [
...this.messages,
{ role: 'tool', content: '', toolName: name },
];
this.requestUpdate();
this.scrollToBottom();
},
});
}
}
private scrollToBottom() {
requestAnimationFrame(() => {
const list = this.shadowRoot?.querySelector('.message-list');
if (list) {
list.scrollTop = list.scrollHeight;
}
});
}
private async handleSend(e: CustomEvent<{ text: string }>) {
if (this.busy) return;
const text = e.detail.text;
this.messages = [...this.messages, { role: 'user', content: text }];
this.busy = true;
this.streamingText = '';
this.requestUpdate();
this.scrollToBottom();
try {
const result = await this.chatSession.send(text);
this.streamingText = '';
this.messages = [...this.messages, { role: 'assistant', content: result.text }];
} catch (error) {
const errMsg = error instanceof Error ? error.message : String(error);
this.messages = [
...this.messages,
{ role: 'assistant', content: `Error: ${errMsg}` },
];
} finally {
this.busy = false;
this.requestUpdate();
this.scrollToBottom();
}
}
render(): TemplateResult {
return html`
<div class="message-list">
${this.messages.length === 0 && !this.busy
? html`<div class="empty-state">Start a conversation...</div>`
: ''}
${this.messages.map(
(msg) => html`
<smartchat-message
role=${msg.role}
content=${msg.content}
.toolName=${msg.toolName ?? ''}
></smartchat-message>
`,
)}
${this.busy && this.streamingText
? html`
<div class="streaming">
${this.streamingText}<span class="cursor"></span>
</div>
`
: ''}
</div>
<div class="input-area">
<smartchat-input
?disabled=${this.busy}
placeholder=${this.placeholder}
@send=${this.handleSend}
></smartchat-input>
</div>
`;
}
}
customElements.define('smartchat-window', SmartchatWindow);