You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/10/12 10:35:38 UTC
(maka) branch feat/runtime-host-rust updated: refactor: name the main UI AgentScreen and separate its responsibilities
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a commit to branch feat/runtime-host-rust
in repository https://gitbox.apache.org/repos/asf/maka.git
The following commit(s) were added to refs/heads/feat/runtime-host-rust by this push:
new 3425217894 refactor: name the main UI AgentScreen and separate its responsibilities
3425217894 is described below
commit 34252178944b8b0167f00c40afc8b06bbdd96e55
Author: Wang <m4...@gmail.com>
AuthorDate: Mon Oct 12 18:35:31 2026 +0800
refactor: name the main UI AgentScreen and separate its responsibilities
---
platform/render-gpui/src/scene/tests.rs | 6 +-
plugins/README.md | 3 +
plugins/README.zh-CN.md | 3 +
plugins/agent/README.md | 2 +-
plugins/agent/README.zh-CN.md | 2 +-
plugins/agent/agent-screen/README.md | 36 +
plugins/agent/agent-screen/README.zh-CN.md | 36 +
plugins/agent/agent-screen/actions.ts | 1007 ++++
.../agent/{workspace => agent-screen}/desktop.ts | 10 +-
plugins/agent/agent-screen/events.ts | 1181 +++++
plugins/agent/agent-screen/frame.ts | 1223 +++++
plugins/agent/agent-screen/header-view.ts | 250 +
plugins/agent/agent-screen/index.ts | 141 +
plugins/agent/agent-screen/messages.ts | 163 +
plugins/agent/agent-screen/navigation-view.ts | 280 ++
.../agent/{workspace => agent-screen}/package.ts | 8 +-
plugins/agent/agent-screen/pages-view.ts | 284 ++
plugins/agent/agent-screen/results.ts | 825 +++
plugins/agent/agent-screen/state.ts | 333 ++
plugins/agent/{workspace => agent-screen}/tui.ts | 10 +-
plugins/agent/workspace/index.ts | 5248 --------------------
plugins/shared/contracts.ts | 6 +-
plugins/shared/tui-locale.ts | 20 +-
plugins/workhub/workhub-root/index.ts | 16 +-
plugins/workhub/workhub-root/package.ts | 6 +-
products/maka/bundle/package-inputs.json | 2 +-
products/maka/bundle/src/composition.ts | 22 +-
...space.test.mjs => product-composition.test.mjs} | 4 +-
products/maka/bundle/tests/ui-authoring.test.mjs | 10 +-
products/maka/cli/notices.json | 29 +-
.../tests/integration/terminal/bundled_product.rs | 2 +-
.../integration/terminal/product/executor-ui.js | 6 +-
.../tests/integration/terminal/product/executor.rs | 20 +-
.../maka-plugin-authoring/references/runtime-ui.md | 4 +-
34 files changed, 5862 insertions(+), 5336 deletions(-)
diff --git a/platform/render-gpui/src/scene/tests.rs b/platform/render-gpui/src/scene/tests.rs
index c349271b2e..0bc6a46e6c 100644
--- a/platform/render-gpui/src/scene/tests.rs
+++ b/platform/render-gpui/src/scene/tests.rs
@@ -349,7 +349,7 @@ fn packaged_session_navigation_renders_bounded_pages_and_keeps_controls_accessib
);
let output = std::process::Command::new("node").args(["--input-type=module","-e",r#"
import {component as directory} from './products/maka/bundle/dist/packages/navigation/desktop.js';
- import {component as workspace,activate as configure} from './products/maka/bundle/dist/packages/workspace/desktop.js';
+ import {component as agentScreen,activate as configure} from './products/maka/bundle/dist/packages/agent-screen/desktop.js';
import {activate} from './products/maka/bundle/dist/packages/appearance/desktop.js';
const frames=[];
function find(node,key) {
@@ -362,9 +362,9 @@ fn packaged_session_navigation_renders_bounded_pages_and_keeps_controls_accessib
configure({locale});
const values={appearance:[activate({scheme}).Appearance.value]};
const sessions=Array.from({length:32},(_,i)=>({id:'conversation-'+i,title:'Conversation '+i,labels:[],revision:1,updatedAt:1,archived:false,activity:i===0?'working':i===1?'waiting':'idle',workspace:{identity:'workspace',directoryIdentity:'directory',path:'/workspace'}}));
- const initial=workspace.mount({},values).frame.state;
+ const initial=agentScreen.mount({},values).frame.state;
const state={...initial,restoring:false,pending:status==='busy'?{catalog:{kind:'list',generation:initial.catalogGeneration}}:{},catalogFailed:status==='failed',catalog:{revision:'fixture',sessions,before:'before',after:'after'}};
- const panel=find(workspace.update(state,{},values).frame.view,'navigation-panel');
+ const panel=find(agentScreen.update(state,{},values).frame.view,'navigation-panel');
const slot=panel.children.findIndex(node=>node.key==='navigation');
panel.children[slot]=directory.mount(panel.children[slot].props,{}).frame.view;
frames.push(panel);
diff --git a/plugins/README.md b/plugins/README.md
index 6051a49830..facfaf8ab6 100644
--- a/plugins/README.md
+++ b/plugins/README.md
@@ -32,10 +32,13 @@ A plugin is declared by a leaf `package.ts`. It lists executable entries, config
| [interactions](interactions/native/README.md) | Questions and forms |
| [workhub](workhub/core/README.md) | Task coordination and worker results |
| `ui/` | Declarations for native UI capabilities |
+| `application/` | Deployment management, plugin installation and UI authoring |
| `shared/` | Shared TypeScript contracts and authoring helpers |
For an existing plugin, start with its declaration and follow its selected implementation. JavaScript entries name a local module such as `tui.js`, built from `tui.ts`. Native entries select an implementation supplied by the executable. Neither a family directory nor a linked crate activates a plugin.
+A family describes business ownership, not a mutually exclusive UI/service category. `package.ts` is authoritative: `host`, `tui` and `desktop` entries say where each implementation runs, and their exports distinguish components from services and values. For example, `application/authoring` has a Host service and a foreground UI worker. `ui/` contains reusable presentation capabilities; it is not a collection of every plugin with a UI. Large components can split plain modules by responsi [...]
+
Build a single package from the repository root:
```sh
diff --git a/plugins/README.zh-CN.md b/plugins/README.zh-CN.md
index c55746354a..6f090a2c2c 100644
--- a/plugins/README.zh-CN.md
+++ b/plugins/README.zh-CN.md
@@ -32,10 +32,13 @@
| [interactions](interactions/native/README.zh-CN.md) | 问题和表单 |
| [workhub](workhub/core/README.zh-CN.md) | 任务协调和 worker 结果 |
| `ui/` | 原生 UI 能力声明 |
+| `application/` | 部署管理、插件安装与 UI 编写 |
| `shared/` | 共用 TypeScript 契约与开发辅助函数 |
修改已有插件时,从声明入手,沿所选实现查找代码。JavaScript 入口指定本地模块,例如由 `tui.ts` 构建的 `tui.js`。原生入口选择可执行文件提供的实现。插件族目录和已链接 crate 都不会自动激活插件。
+插件族表示业务归属,不是互斥的 UI/服务分类。以 `package.ts` 为准:`host`、`tui`、`desktop` 入口说明实现运行位置,导出契约区分组件、服务和值。例如 `application/authoring` 同时包含 Host 服务与前台 UI worker。`ui/` 存放通用展示能力,并不收纳所有带 UI 的插件。大型组件可以按职责拆成普通模块;[AgentScreen](agent/agent-screen/README.zh-CN.md) 保留一个组件状态和生命周期,将命令、结果、事件及视图区域分开组织。
+
在仓库根目录构建单个包:
```sh
diff --git a/plugins/agent/README.md b/plugins/agent/README.md
index 508c399af2..004c0654d2 100644
--- a/plugins/agent/README.md
+++ b/plugins/agent/README.md
@@ -30,4 +30,4 @@ This family owns Agent behavior and its user interface. Leaf `package.ts` files
- [assistant](assistant/README.md), [goal](goal/README.md), [graph](graph/README.md), [scheduler](scheduler/engine/README.md), [insights](insights/README.md), [session-import](session-import/README.md), [session-recap](session-recap/README.md) and [session-title](session-title/README.md) own their respective features.
- [acp-executor](acp-executor/native/README.md) connects external ACP agents through explicit process and callback services.
-UI packages such as `workspace`, `composer` and `timeline` consume these contracts. Product assembly belongs to [products/maka](../../products/maka/README.md).
+UI packages such as `agent-screen`, `composer` and `timeline` consume these contracts. Product assembly belongs to [products/maka](../../products/maka/README.md).
diff --git a/plugins/agent/README.zh-CN.md b/plugins/agent/README.zh-CN.md
index c288d69306..2fd54bd5ee 100644
--- a/plugins/agent/README.zh-CN.md
+++ b/plugins/agent/README.zh-CN.md
@@ -30,4 +30,4 @@
- [assistant](assistant/README.zh-CN.md)、[goal](goal/README.zh-CN.md)、[graph](graph/README.zh-CN.md)、[scheduler](scheduler/engine/README.zh-CN.md)、[insights](insights/README.zh-CN.md)、[session-import](session-import/README.zh-CN.md)、[session-recap](session-recap/README.zh-CN.md) 和 [session-title](session-title/README.zh-CN.md)管理各自功能。
- [acp-executor](acp-executor/native/README.zh-CN.md)通过显式进程与回调服务连接外部 ACP Agent。
-`workspace`、`composer`、`timeline` 等 UI 包消费这些契约。产品组装位于 [products/maka](../../products/maka/README.zh-CN.md)。
+`agent-screen`、`composer`、`timeline` 等 UI 包消费这些契约。产品组装位于 [products/maka](../../products/maka/README.zh-CN.md)。
diff --git a/plugins/agent/agent-screen/README.md b/plugins/agent/agent-screen/README.md
new file mode 100644
index 0000000000..c616d2caf6
--- /dev/null
+++ b/plugins/agent/agent-screen/README.md
@@ -0,0 +1,36 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+
+# Agent screen
+
+`AgentScreen` is the Agent's main UI: session navigation, conversation, composer and product pages. A session's `workspace` still means its Host working directory. The component can own a foreground surface or mount inside WorkHub.
+
+| Module | Responsibility |
+| --- | --- |
+| `index.ts` | Component construction, initial state and lifecycle wiring |
+| `state.ts` | The single component state, request types and navigation bookmark |
+| `actions.ts` | Available commands and explicit service intents |
+| `results.ts` | Query results, mutation receipts and live deliveries |
+| `messages.ts` | Bounded live text and timeline display projections |
+| `events.ts` | Input routing within one candidate-frame transaction |
+| `frame.ts` | Retained child composition, conversation/composer, dialogs and subscriptions |
+| `navigation-view.ts`, `header-view.ts`, `pages-view.ts` | Navigation, header and retained product pages |
+| `tui.ts`, `desktop.ts`, `package.ts` | Target adapters and package declaration |
+
+Modules share the existing `State`; they do not own separate stores or dispatch loops. View helpers return native scenes. Publication of a candidate frame and its intents still happens once at the component boundary. The default footer is a bound component, documented in the [runtime UI guide](../../../skills/maka-plugin-authoring/references/runtime-ui.md).
diff --git a/plugins/agent/agent-screen/README.zh-CN.md b/plugins/agent/agent-screen/README.zh-CN.md
new file mode 100644
index 0000000000..2c6abcb31b
--- /dev/null
+++ b/plugins/agent/agent-screen/README.zh-CN.md
@@ -0,0 +1,36 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+
+# Agent 主界面
+
+`AgentScreen` 负责会话导航、对话、composer 和产品页面。会话的 `workspace` 仍指 Host 工作目录。组件既能拥有前台 surface,也能嵌入 WorkHub。
+
+| 模块 | 职责 |
+| --- | --- |
+| `index.ts` | 组件构造、初始状态和生命周期接线 |
+| `state.ts` | 唯一的组件状态、请求类型和导航书签 |
+| `actions.ts` | 可用命令与明确的服务调用意图 |
+| `results.ts` | 查询结果、修改回执及实时事件 |
+| `messages.ts` | 有界实时文本和时间线展示投影 |
+| `events.ts` | 同一候选帧事务内的输入路由 |
+| `frame.ts` | 子组件组合、对话/composer、对话框及订阅 |
+| `navigation-view.ts`、`header-view.ts`、`pages-view.ts` | 导航、标题栏和保留状态的产品页面 |
+| `tui.ts`、`desktop.ts`、`package.ts` | 展示目标适配和插件声明 |
+
+各模块共用原有 `State`,没有独立 store 或额外事件循环。视图函数返回原生场景;候选帧和调用意图仍在组件边界一次发布。自定义 footer 是绑定的组件,详见[运行时 UI 指南](../../../skills/maka-plugin-authoring/references/runtime-ui.md)。
diff --git a/plugins/agent/agent-screen/actions.ts b/plugins/agent/agent-screen/actions.ts
new file mode 100644
index 0000000000..4ad5640ef7
--- /dev/null
+++ b/plugins/agent/agent-screen/actions.ts
@@ -0,0 +1,1007 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { tuiText } from '../../shared/tui-locale.js';
+import type {
+ Action,
+ InteractionAnswer,
+ AgentQueueMessage,
+ SourcePosition,
+ HistoryPosition,
+ AgentSession,
+ AgentList,
+ TerminalPaneCommand,
+} from '../../../packages/plugin-sdk/src/index.js';
+import type { PaletteCommand, QuestionDraft } from '../../shared/contracts.js';
+import { sameValue } from '../../shared/value.js';
+import { copy } from '../../shared/locale.js';
+import { compactLabel } from '../../shared/view.js';
+
+import {
+ attachmentFiles,
+ attachmentCapacity,
+ sameFile,
+ selectedFile,
+} from '../../shared/attachments.js';
+
+import { draftReady } from '../../shared/drafts.js';
+
+import { type Backend, type Request, type State, type RootCommand } from './state.js';
+import { messages } from './messages.js';
+
+export function intent(
+ state: State,
+ actions: Action[],
+ request: Request,
+ binding: string,
+ method: string,
+ input: unknown,
+ resources: Action['resources'] = {},
+ continuations: Action['continuations'] = [],
+) {
+ const key = 'request-' + ++state.serial;
+ state.pending[key] = request;
+ actions.push({
+ key,
+ binding,
+ member: 0,
+ method,
+ input,
+ resources,
+ continuations,
+ ...(request.kind === 'send'
+ ? {
+ recovery: {
+ format: 'maka.agent.draft-send@1',
+ sessionId: request.sessionId,
+ editor: { owner: request.draft.snapshot.text.owner, revision: request.draft.revision },
+ attachments: request.attachments,
+ },
+ }
+ : {}),
+ });
+}
+
+export function canExecute(state: State) {
+ return !!state.executor || (!!state.model && !state.modelRefreshing);
+}
+
+export function selectedBackend(state: State): Backend {
+ return state.executor
+ ? {
+ kind: 'executor',
+ selection: { ...state.executor, settings: { ...state.executor.settings } },
+ }
+ : {
+ kind: 'model',
+ model: { ...state.model!.choice.selection },
+ thinkingLevel: state.model!.thinkingLevel,
+ };
+}
+
+function createSession(state: State, actions: Action[], locale: string) {
+ if (mutationPending(state) || catalogPending(state)) return;
+ if (!canExecute(state)) {
+ openModelSetup(state);
+ return;
+ }
+ const title = copy(locale).new;
+ if (state.selected?.archived) state.dialog = 'directories';
+ else if (state.selected)
+ intent(state, actions, { kind: 'create' }, 'agent', 'create', {
+ title,
+ labels: [],
+ workspace: { kind: 'session', sessionId: state.selected.id },
+ });
+ else
+ intent(state, actions, { kind: 'createDirectory', title }, 'agent', 'directories', {
+ kind: 'roots',
+ });
+}
+
+function openModelSetup(state: State) {
+ if (state.modelSetup === 'connect' && !state.hasBackends) {
+ state.pages = ['connections'];
+ } else state.modelOpenRequest = ++state.serial;
+}
+
+export function catalogPending(state: State): boolean {
+ return state.catalogWatch?.loading ?? false;
+}
+
+export function catalog(state: State, position?: AgentList['position']) {
+ state.catalogFailed = false;
+ const anchor = state.catalog?.sessions[0]?.id;
+ state.catalogWatch = {
+ key: `catalog-${++state.catalogGeneration}`,
+ input: {
+ filter: state.catalogFilter,
+ position: position ?? (anchor ? { kind: 'at', cursor: anchor } : { kind: 'start' }),
+ },
+ loading: true,
+ };
+}
+
+export function observe(state: State, actions: Action[]) {
+ if (state.selected)
+ intent(state, actions, { kind: 'observe', epoch: state.epoch }, 'agent', 'observe', {
+ sessionId: state.selected.id,
+ });
+}
+
+export function history(
+ state: State,
+ actions: Action[],
+ position: HistoryPosition,
+ through: number,
+ latest: boolean,
+ implicit = false,
+ extend = false,
+) {
+ if (!state.selected) return;
+ if (!implicit && !extend) state.loading = true;
+ const navigation = ++state.navigation;
+ intent(
+ state,
+ actions,
+ {
+ kind: 'history',
+ epoch: state.epoch,
+ navigation,
+ through,
+ latest,
+ implicit,
+ position,
+ extend,
+ },
+ 'agent',
+ 'history',
+ { sessionId: state.selected.id, through, position, includeThinking: true },
+ {},
+ implicit ? [{ child: 'timeline', method: 'tail', input: null }] : [],
+ );
+}
+
+export function follow(state: State, actions: Action[]) {
+ const receipt = state.tailReceipt;
+ if (
+ receipt?.landing === state.landing.sequence &&
+ !state.source &&
+ !state.searchOpen &&
+ state.browse === null &&
+ !state.image &&
+ !state.loading &&
+ state.window?.latest &&
+ (state.latestKnown > state.window.page.through ||
+ state.live.some(
+ (fragment) => fragment.closedAt !== null && fragment.closedAt <= state.window!.page.through,
+ )) &&
+ !Object.values(state.pending).some(
+ (request) =>
+ request.kind === 'history' &&
+ request.epoch === state.epoch &&
+ request.navigation === state.navigation,
+ ) &&
+ !(
+ state.followAttempt?.through === state.latestKnown &&
+ state.followAttempt.landing === receipt.landing &&
+ state.followAttempt.readingRevision === receipt.readingRevision
+ )
+ ) {
+ state.followAttempt = { ...receipt, through: state.latestKnown };
+ history(state, actions, { kind: 'tail' }, state.latestKnown, true, true);
+ }
+}
+
+export function select(state: State, actions: Action[], session: AgentSession | null) {
+ state.searchOpen = false;
+ state.browse = null;
+ state.recallAnchor = null;
+ state.selected = session;
+ state.epoch++;
+ state.window = null;
+ state.source = null;
+ state.image = null;
+ state.tailReceipt = null;
+ state.fileContext = null;
+ state.followAttempt = null;
+ state.navigation++;
+ state.parts = [];
+ state.run = null;
+ state.queue = null;
+ state.interactions = null;
+ state.watch = null;
+ state.runThrough = 0;
+ state.live = [];
+ state.latestKnown = 0;
+ state.loading = session !== null;
+ state.notice = null;
+ if (session) observe(state, actions);
+}
+
+export function readHistorySource(state: State, actions: Action[], position: SourcePosition) {
+ const source = state.source;
+ if (!source || !state.selected) return;
+ source.navigation = ++state.navigation;
+ source.loading = true;
+ source.failed = false;
+ source.position = position;
+ intent(
+ state,
+ actions,
+ {
+ kind: 'source',
+ epoch: state.epoch,
+ navigation: source.navigation,
+ through: source.through,
+ sequence: source.chunk.sequence,
+ field: source.field,
+ position,
+ },
+ 'agent',
+ 'source',
+ {
+ sessionId: state.selected.id,
+ through: source.through,
+ source: { kind: 'sequence', sequence: source.chunk.sequence },
+ field: source.field,
+ position,
+ },
+ );
+}
+
+export function mutationPending(state: State) {
+ return (
+ state.restoring ||
+ state.uncertain !== null ||
+ Object.values(state.pending).some((request) =>
+ [
+ 'read',
+ 'queueRead',
+ 'questionRead',
+ 'respond',
+ 'markRead',
+ 'uploadAttachment',
+ 'create',
+ 'createDirectory',
+ 'send',
+ 'stop',
+ 'queue',
+ 'receipt',
+ ].includes(request.kind),
+ )
+ );
+}
+
+export function uploadPastedFile(state: State, actions: Action[]) {
+ const paste = state.attachments.paste,
+ observed = paste?.observed,
+ file = observed?.selection;
+ if (
+ !paste ||
+ !observed ||
+ !file ||
+ file.phase !== 'ready' ||
+ !file.file ||
+ paste.command ||
+ mutationPending(state) ||
+ sameFile(paste.attempted, file)
+ )
+ return;
+ if (
+ state.selected?.id === observed.context &&
+ sameValue(state.selected.dataSource, observed.dataSource) &&
+ state.selected.archived
+ )
+ return;
+ const dataSource = observed.dataSource;
+ if (state.selected && !sameValue(state.selected.dataSource, dataSource)) return;
+ if (!dataSource || !draftReady(state.drafts, { id: observed.context, dataSource })) return;
+ if (!attachmentCapacity(state.attachments, observed.context, dataSource)) {
+ state.notice = 'attachmentLimit';
+ return;
+ }
+ paste.attempted = selectedFile(file);
+ state.notice = null;
+ intent(
+ state,
+ actions,
+ {
+ kind: 'uploadAttachment',
+ dataSource,
+ sessionId: observed.context,
+ attempt: ++state.serial,
+ pasted: paste.attempted,
+ },
+ 'agent',
+ 'uploadAttachment',
+ { sessionId: observed.context, ...file.file },
+ {
+ file: { child: 'composer', method: 'file', input: paste.attempted },
+ },
+ );
+}
+
+export function executionPending(state: State) {
+ return Object.values(state.pending).some(
+ (request) => request.kind === 'observe' && request.epoch === state.epoch,
+ );
+}
+
+export function conversationRoute(state: State) {
+ return state.pages.length === 0;
+}
+
+export function reviewTarget(state: State) {
+ const retained = state.interaction,
+ original = state.uncertain?.request;
+ // Keep an unresolved answer attached to its original review even after the
+ // canonical pending list advances. Opening never repeats that answer.
+ if (
+ retained?.record &&
+ retained.sessionId === state.selected?.id &&
+ original?.kind === 'respond' &&
+ original.sessionId === retained.sessionId &&
+ original.attempt === retained.attempt &&
+ original.requestId === retained.requestId
+ )
+ return {
+ requestId: retained.requestId,
+ kind: retained.record.request.kind,
+ retained: true,
+ };
+ const entry = state.interactions?.entries.find(
+ (entry) =>
+ ['question', 'form', 'service_approval', 'permissions'].includes(entry.kind) &&
+ !(
+ retained?.sessionId === state.selected?.id &&
+ retained?.requestId === entry.requestId &&
+ retained.record?.outcome
+ ),
+ );
+ return entry ? { ...entry, retained: false } : null;
+}
+
+export function openInteraction(state: State, actions: Action[], requestId: string, kind: string) {
+ if (!state.selected) return;
+ if (
+ state.interaction?.requestId !== requestId ||
+ state.interaction.sessionId !== state.selected.id
+ )
+ state.interaction = {
+ attempt: ++state.serial,
+ sessionId: state.selected.id,
+ requestId,
+ record: null,
+ notice: null,
+ };
+ state.dialog = kind === 'question' || kind === 'form' ? 'question' : 'approval';
+ readInteraction(state, actions);
+}
+
+export function rootCommands(
+ state: State,
+ locale: string,
+ target: 'tui' | 'desktop',
+): (PaletteCommand & { kind: RootCommand })[] {
+ const text = copy(locale);
+ const shellRoute = state.dialog === null || state.dialog === 'palette';
+ const route = conversationRoute(state) && shellRoute;
+ const review = reviewTarget(state);
+ const file = state.fileContext;
+ const filePath =
+ file &&
+ file.sessionId === state.selected?.id &&
+ file.through === state.window?.page.through &&
+ file.landing === state.landing.sequence &&
+ !state.loading &&
+ !state.source &&
+ !state.image &&
+ !state.searchOpen
+ ? file.path
+ : null;
+ const copied = state.messageCopy;
+ const readyCopy =
+ copied &&
+ state.dialog === 'palette' &&
+ copied.request === state.copyRequest &&
+ copied.sessionId === state.selected?.id &&
+ copied.through === state.window?.page.through &&
+ copied.landing === state.landing.sequence &&
+ !state.loading &&
+ !state.source &&
+ !state.image &&
+ !state.searchOpen
+ ? copied
+ : null;
+ const commands: (PaletteCommand & { kind: RootCommand })[] = [
+ ...(target === 'tui'
+ ? [
+ {
+ kind: 'browse' as const,
+ id: 'browse',
+ label: text.browseMessages,
+ hint: '',
+ disabled:
+ !route ||
+ !state.selected ||
+ state.loading ||
+ !!state.source ||
+ !!state.image ||
+ !messages(state).length,
+ },
+ {
+ kind: 'search-open' as const,
+ id: 'search-open',
+ label: text.findConversation,
+ hint: 'Ctrl F',
+ disabled: !route || !state.selected || state.loading || !!state.source || !!state.image,
+ },
+ ...(['copy-message', 'copy-source'] as const).map((kind) => {
+ const value = kind === 'copy-message' ? readyCopy?.message : readyCopy?.source;
+ const result = readyCopy?.field === 'result',
+ args = readyCopy?.field === 'arguments';
+ return {
+ kind,
+ id: kind,
+ label:
+ kind === 'copy-message'
+ ? text.copyMessage
+ : result
+ ? text.copyToolResultSource
+ : args
+ ? text.copyToolArgumentsSource
+ : text.copySource,
+ hint: value
+ ? ''
+ : readyCopy?.status === 'loading'
+ ? text.copyLoading
+ : readyCopy?.status === 'too_large'
+ ? text.copyTooLarge
+ : readyCopy?.status === 'empty'
+ ? text.copyEmpty
+ : readyCopy?.status === 'streaming'
+ ? text.copyStreaming
+ : text.copyUnavailable,
+ disabled: !route || !value,
+ ...(value ? { copyText: value } : {}),
+ };
+ }),
+ {
+ kind: 'copy-file-path' as const,
+ id: 'copy-file-path',
+ label: text.toolCopyPath,
+ hint: filePath
+ ? compactLabel(filePath.split(/[/\\]/u).filter(Boolean).at(-1) || filePath, 32)
+ : '',
+ disabled: !route || !filePath,
+ ...(filePath ? { copyText: filePath } : {}),
+ },
+ ]
+ : []),
+ {
+ kind: 'help-open',
+ id: 'help-open',
+ label: text.helpTitle,
+ hint: 'F1',
+ disabled: state.dialog !== null && state.dialog !== 'palette',
+ },
+ {
+ kind: 'interaction-open',
+ id: 'interaction-open',
+ label: text.reviewRequest,
+ hint: 'Ctrl/⌘ G',
+ disabled:
+ !route ||
+ !state.selected ||
+ state.selected.archived ||
+ state.loading ||
+ !review ||
+ (mutationPending(state) && !review.retained),
+ },
+ {
+ kind: 'model-setup',
+ id: 'model-setup',
+ label: state.hasBackends
+ ? text.executionBackend
+ : state.modelSetup === 'connect'
+ ? text.configureModels
+ : text.chooseModel,
+ hint: '',
+ disabled: !route || mutationPending(state) || executionPending(state),
+ },
+ {
+ kind: 'create',
+ id: 'create',
+ label: text.new,
+ hint: 'Ctrl/⌘ N',
+ disabled: !shellRoute || mutationPending(state) || catalogPending(state),
+ },
+ {
+ kind: 'create-workspace',
+ id: 'create-workspace',
+ label: text.newWorkspace,
+ hint: '',
+ disabled: !shellRoute || mutationPending(state) || catalogPending(state),
+ },
+ ...(state.skills
+ ? [
+ {
+ kind: 'skills-open' as const,
+ id: 'skills-open',
+ label: text.skillsTitle,
+ hint: '',
+ disabled: !route,
+ },
+ ]
+ : []),
+ {
+ kind: 'terminal-open',
+ id: 'terminal-open',
+ label: text.terminalTitle,
+ hint: '',
+ disabled: !route || !state.selected || state.loading,
+ },
+ ...(target === 'tui'
+ ? [
+ {
+ kind: 'usage-open' as const,
+ id: 'usage-open',
+ label: tuiText(locale, 'insights-this-session'),
+ hint: '',
+ disabled: !route || !state.selected,
+ },
+ ]
+ : []),
+ {
+ kind: 'details-open',
+ id: 'details-open',
+ label: text.detailsTitle,
+ hint: '',
+ disabled:
+ !route ||
+ !state.selected ||
+ state.loading ||
+ executionPending(state) ||
+ mutationPending(state) ||
+ catalogPending(state),
+ },
+ {
+ kind: 'navigation-toggle',
+ id: 'navigation-toggle',
+ label: sidebarLabel(state, text),
+ hint: 'Ctrl/⌘ B',
+ disabled: !shellRoute,
+ },
+ ...(target === 'tui'
+ ? [
+ {
+ kind: 'appearance-open' as const,
+ id: 'appearance-open',
+ label: tuiText(locale, 'settings-appearance'),
+ hint: '',
+ disabled: !shellRoute,
+ },
+ ]
+ : []),
+ ...(state.embedding.workhub && !state.embedding.fixed
+ ? [
+ {
+ kind: 'workhub-open' as const,
+ id: 'workhub-open',
+ label: 'WorkHub',
+ hint: '',
+ disabled: !shellRoute,
+ },
+ ]
+ : []),
+ {
+ kind: 'apps-open',
+ id: 'apps-open',
+ label: text.appsTitle,
+ hint: '',
+ disabled: !shellRoute || !!state.embedding.fixed,
+ },
+ {
+ kind: 'scheduler-open',
+ id: 'scheduler-open',
+ label: text.scheduleTitle,
+ hint: '',
+ disabled: !shellRoute || !!state.embedding.fixed || !state.scheduledTasks,
+ },
+ {
+ kind: 'recall-open',
+ id: 'recall-open',
+ label: text.recallTitle,
+ hint: '',
+ disabled: !shellRoute || !!state.embedding.fixed,
+ },
+ {
+ kind: 'imports-open',
+ id: 'imports-open',
+ label: text.importsTitle,
+ hint: '',
+ disabled: !shellRoute || !!state.embedding.fixed,
+ },
+ {
+ kind: 'projects-open',
+ id: 'projects-open',
+ label: text.projectsTitle,
+ hint: '',
+ disabled: !shellRoute || !!state.embedding.fixed,
+ },
+ {
+ kind: 'settings',
+ id: 'settings',
+ label: target === 'tui' ? tuiText(locale, 'route-settings') : text.settings,
+ hint: '',
+ disabled: !shellRoute,
+ },
+ {
+ kind: 'deployment-open',
+ id: 'deployment-open',
+ label: text.deploymentTitle,
+ hint: '',
+ disabled: !shellRoute,
+ },
+ {
+ kind: 'reload',
+ id: `reload:${state.epoch}`,
+ label: text.refreshSessions,
+ hint: '',
+ disabled: !shellRoute || state.loading,
+ },
+ ];
+ if (state.run)
+ commands.push({
+ kind: 'stop',
+ id: `stop:${state.epoch}:${state.run.invocationId}`,
+ label: text.stop,
+ hint: '',
+ disabled: !route || executionPending(state) || mutationPending(state) || !state.selected,
+ });
+ if (state.uncertain)
+ commands.push({
+ kind: 'check',
+ id: `check:${state.uncertain.reference.operation}:${state.uncertain.reference.fingerprint}`,
+ label: text.check,
+ hint: '',
+ disabled:
+ !route || Object.values(state.pending).some((request) => request.kind === 'receipt'),
+ });
+ return state.embedding.fixed
+ ? commands.filter(
+ (command) => !['create', 'create-workspace', 'navigation-toggle'].includes(command.kind),
+ )
+ : commands;
+}
+
+export function openUsage(state: State, session: AgentSession) {
+ state.insightsScope = { sequence: ++state.serial, sessionId: session.id, label: session.title };
+ state.settingsCategory = 'insights';
+ state.pages = ['settings'];
+ controlSidebar(state, 'dismiss_transient');
+}
+
+export function sidebarLabel(state: State, text: ReturnType<typeof copy>) {
+ return 'open' in state.sidebar
+ ? state.sidebar.open
+ ? text.hideSidebar
+ : text.showSidebar
+ : text.toggleSidebar;
+}
+
+export function controlSidebar(state: State, op: TerminalPaneCommand['op']) {
+ if ('open' in state.sidebar) {
+ if (op === 'toggle') state.sidebar.open = !state.sidebar.open;
+ } else {
+ state.sidebar.command = { sequence: (state.sidebar.command?.sequence ?? 0) + 1, op };
+ }
+}
+
+export function rootCommand(
+ state: State,
+ actions: Action[],
+ locale: string,
+ kind: RootCommand,
+ target: 'tui' | 'desktop',
+) {
+ if (
+ !rootCommands(state, locale, target).some(
+ (command) => command.kind === kind && !command.disabled,
+ )
+ )
+ return false;
+ if (kind === 'browse') {
+ state.searchOpen = false;
+ state.browse = ++state.serial;
+ state.tailReceipt = null;
+ } else if (kind === 'search-open') {
+ state.searchOpen = true;
+ state.browse = null;
+ } else if (kind === 'help-open') state.dialog = 'help';
+ else if (kind === 'interaction-open') {
+ const review = reviewTarget(state)!;
+ openInteraction(state, actions, review.requestId, review.kind);
+ } else if (kind === 'create') {
+ state.pages = [];
+ createSession(state, actions, locale);
+ } else if (kind === 'appearance-open') state.preferences.open = true;
+ else if (kind === 'model-setup') openModelSetup(state);
+ else if (kind === 'create-workspace') {
+ state.pages = [];
+ if (!canExecute(state)) openModelSetup(state);
+ else state.dialog = 'directories';
+ if (target === 'tui') controlSidebar(state, 'dismiss_transient');
+ } else if (kind === 'terminal-open') state.dialog = 'terminal';
+ else if (kind === 'skills-open') state.dialog = 'skills';
+ else if (kind === 'details-open') state.dialog = 'details';
+ else if (kind === 'usage-open' && state.selected) openUsage(state, state.selected);
+ else if (kind === 'navigation-toggle') controlSidebar(state, 'toggle');
+ else if (kind === 'settings') {
+ state.insightsScope = { sequence: ++state.serial, sessionId: null, label: '' };
+ state.pages = [target === 'tui' ? 'settings' : 'connections'];
+ if (target === 'tui') controlSidebar(state, 'dismiss_transient');
+ } else if (kind === 'workhub-open') {
+ if (target === 'tui') controlSidebar(state, 'dismiss_transient');
+ } else if (kind === 'apps-open') {
+ state.pages = ['apps'];
+ if (target === 'tui') controlSidebar(state, 'dismiss_transient');
+ } else if (kind === 'scheduler-open') {
+ state.pages = ['scheduler'];
+ if (!state.scheduleWatch) state.scheduleWatch = 'schedule-alerts-' + ++state.serial;
+ if (target === 'tui') controlSidebar(state, 'dismiss_transient');
+ } else if (kind === 'recall-open') {
+ state.pages = ['recall'];
+ if (target === 'tui') controlSidebar(state, 'dismiss_transient');
+ } else if (kind === 'imports-open') {
+ state.pages = ['imports'];
+ if (target === 'tui') controlSidebar(state, 'dismiss_transient');
+ } else if (kind === 'projects-open') {
+ state.pages = ['projects'];
+ if (target === 'tui') controlSidebar(state, 'dismiss_transient');
+ } else if (kind === 'deployment-open') {
+ state.pages = ['deployment'];
+ if (target === 'tui') controlSidebar(state, 'dismiss_transient');
+ } else if (kind === 'stop' && state.selected && state.run)
+ intent(state, actions, { kind: 'stop', sessionId: state.selected.id }, 'agent', 'stop', {
+ sessionId: state.selected.id,
+ invocationId: state.run.invocationId,
+ });
+ else if (kind === 'check' && state.uncertain)
+ intent(state, actions, { kind: 'receipt' }, 'receipts', 'read', state.uncertain.reference);
+ else if (kind === 'reload') {
+ if (state.restoring)
+ intent(state, actions, { kind: 'restorePage' }, 'history', 'page', {
+ after: state.restoreAfter,
+ limit: 1,
+ });
+ else if (!state.selected && state.embedding.session) state.externalSelection = null;
+ else if (!state.selected) catalog(state);
+ else {
+ const window = state.window;
+ select(state, actions, state.selected);
+ state.window = window;
+ }
+ }
+ return true;
+}
+
+export function loadQueueEdit(state: State, actions: Action[], target: AgentQueueMessage) {
+ const attempt = ++state.serial;
+ state.queueEdit = {
+ attempt,
+ target,
+ phase: 'loading',
+ hasAttachments: false,
+ body: null,
+ command: null,
+ };
+ state.dialog = 'queue';
+ state.notice = null;
+ intent(state, actions, { kind: 'queueBody', attempt }, 'agent', 'queueMessage', target);
+}
+
+export function initializeQueueEdit(state: State) {
+ const edit = state.queueEdit,
+ draft = state.queueDraft;
+ if (!edit || edit.body === null || !draft || edit.command) return;
+ edit.command = {
+ owner: draft.snapshot.text.owner,
+ sequence: ++state.serial,
+ basedOn: draft.revision,
+ edit: { kind: 'reset', text: edit.body },
+ };
+ edit.body = null;
+}
+
+export function questionChild(state: State) {
+ return `question-form-${state.interaction?.attempt ?? 0}`;
+}
+
+export function readInteraction(state: State, actions: Action[]) {
+ const question = state.interaction;
+ if (
+ !question ||
+ Object.values(state.pending).some(
+ (r) => r.kind === 'interaction' && r.attempt === question.attempt,
+ )
+ )
+ return;
+ intent(
+ state,
+ actions,
+ {
+ kind: 'interaction',
+ attempt: question.attempt,
+ sessionId: question.sessionId,
+ requestId: question.requestId,
+ },
+ 'agent',
+ 'interaction',
+ { sessionId: question.sessionId, requestId: question.requestId },
+ );
+}
+
+export function respondQuestion(state: State, actions: Action[]) {
+ const submission = state.questionSubmission,
+ question = state.interaction;
+ if (!submission || submission.waiting.length) return;
+ state.questionSubmission = null;
+ if (
+ !question ||
+ submission.attempt !== question.attempt ||
+ state.dialog !== 'question' ||
+ state.selected?.id !== question.sessionId ||
+ state.selected.archived ||
+ !question.record ||
+ question.record.outcome
+ )
+ return;
+ respondInteraction(state, actions, { kind: 'question', answers: submission.answers });
+}
+
+export function respondInteraction(state: State, actions: Action[], answer: InteractionAnswer) {
+ const interaction = state.interaction;
+ if (
+ !interaction?.record ||
+ interaction.record.outcome ||
+ interaction.record.request.kind !== answer.kind ||
+ state.selected?.id !== interaction.sessionId ||
+ state.selected.archived ||
+ mutationPending(state)
+ )
+ return;
+ intent(
+ state,
+ actions,
+ {
+ kind: 'respond',
+ attempt: interaction.attempt,
+ sessionId: interaction.sessionId,
+ requestId: interaction.requestId,
+ },
+ 'agent',
+ 'respond',
+ {
+ sessionId: interaction.sessionId,
+ requestId: interaction.requestId,
+ answer,
+ },
+ );
+}
+
+export function submitQuestion(
+ state: State,
+ actions: Action[],
+ event: { requestId: string; answers: QuestionDraft[] },
+) {
+ const question = state.interaction;
+ if (
+ !question ||
+ !question.record ||
+ question.record.request.kind !== 'question' ||
+ question.record.outcome ||
+ state.dialog !== 'question' ||
+ state.selected?.id !== question.sessionId ||
+ state.selected.archived ||
+ event.requestId !== question.requestId ||
+ mutationPending(state)
+ )
+ return;
+ const questions = question.record.request.questions;
+ if (event.answers.length !== questions.length) return;
+ const answers: (string | null)[] = [],
+ waiting: number[] = [];
+ for (const [index, answer] of event.answers.entries()) {
+ if (answer.kind === 'skip') answers.push(null);
+ else if (answer.kind === 'option' && questions[index].options[answer.index])
+ answers.push(questions[index].options[answer.index].label);
+ else if (
+ answer.kind === 'text' &&
+ !answer.observation.snapshot.marked &&
+ answer.observation.snapshot.text.bytes > 0 &&
+ answer.observation.snapshot.text.bytes <= 2048
+ ) {
+ answers.push(null);
+ waiting.push(index);
+ } else {
+ question.notice = 'questionInvalid';
+ return;
+ }
+ }
+ question.notice = null;
+ state.questionSubmission = { attempt: question.attempt, answers, waiting };
+ for (const index of waiting) {
+ const answer = event.answers[index];
+ if (answer.kind !== 'text') continue;
+ const draft = answer.observation;
+ intent(
+ state,
+ actions,
+ { kind: 'questionRead', attempt: question.attempt, index, draft },
+ 'reader',
+ 'read',
+ { start: 0, end: draft.snapshot.text.bytes },
+ {
+ text: {
+ child: questionChild(state),
+ method: 'snapshot',
+ input: {
+ requestId: question.requestId,
+ index,
+ snapshot: {
+ kind: 'at_revision',
+ owner: draft.snapshot.text.owner,
+ revision: draft.snapshot.text.revision,
+ },
+ },
+ },
+ },
+ );
+ }
+ respondQuestion(state, actions);
+}
+
+export function checkInteraction(state: State, actions: Action[]) {
+ if (state.uncertain && !Object.values(state.pending).some((r) => r.kind === 'receipt'))
+ intent(state, actions, { kind: 'receipt' }, 'receipts', 'read', state.uncertain.reference);
+ readInteraction(state, actions);
+}
+
+export function focusAttachments(state: State) {
+ const selection = state.attachments.selection;
+ if (!selection || selection.browse) return;
+ const files = attachmentFiles(state.attachments, selection.sessionId, selection.dataSource);
+ const index = files.findIndex((file) => file.id === selection.selected);
+ selection.focus = {
+ sequence: ++state.serial,
+ target: {
+ kind: 'focus_item',
+ key: index < 0 ? 'attachment-browse' : 'attachment-select-' + index,
+ },
+ };
+}
+
+export function closeDialog(state: State) {
+ // Cancel follows the original queue sheet: discard this edit, not an accepted operation.
+ if (state.dialog === 'queue') state.queueEdit = null;
+ if (state.dialog === 'question') state.questionSubmission = null;
+ state.dialog = null;
+}
diff --git a/plugins/agent/workspace/desktop.ts b/plugins/agent/agent-screen/desktop.ts
similarity index 82%
rename from plugins/agent/workspace/desktop.ts
rename to plugins/agent/agent-screen/desktop.ts
index faa3519fdf..60e9161ca9 100644
--- a/plugins/agent/workspace/desktop.ts
+++ b/plugins/agent/agent-screen/desktop.ts
@@ -18,9 +18,9 @@
*/
import { presentations } from '../../shared/view.js';
-import { workspace } from './index.js';
-import { workspaceContract } from '../../shared/contracts.js';
-export let component: ReturnType<typeof workspace>;
+import { agentScreen } from './index.js';
+import { agentScreenContract } from '../../shared/contracts.js';
+export let component: ReturnType<typeof agentScreen>;
export function activate(configuration: {
locale: string;
terminals?: boolean;
@@ -30,7 +30,7 @@ export function activate(configuration: {
web?: boolean;
jev?: boolean;
}) {
- component = workspace(
+ component = agentScreen(
presentations.desktop,
configuration.locale,
configuration.terminals === true,
@@ -41,6 +41,6 @@ export function activate(configuration: {
configuration.jev === true,
);
return {
- Workspace: { kind: 'component', contract: workspaceContract, component: ['component'] },
+ AgentScreen: { kind: 'component', contract: agentScreenContract, component: ['component'] },
};
}
diff --git a/plugins/agent/agent-screen/events.ts b/plugins/agent/agent-screen/events.ts
new file mode 100644
index 0000000000..c18585fd98
--- /dev/null
+++ b/plugins/agent/agent-screen/events.ts
@@ -0,0 +1,1181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { effectiveLocale, appearanceEvent } from '../../shared/appearance-editor.js';
+import { settingsEvent } from '../../shared/settings.js';
+import type {
+ Action,
+ ActionOutcome,
+ Emission,
+ AgentExecutorSelection,
+ AgentDeliveryPage,
+ InteractionAnswer,
+ SourceField,
+ SourcePosition,
+ AgentSession,
+ AgentSessionPage,
+ AgentDirectorySelection,
+ AgentProjectRef,
+ AgentScheduleAlerts,
+ FileObservation,
+ AgentQueueAction,
+ RowActivated,
+ ComponentInput,
+ ComponentValues,
+ EditorObservation,
+} from '../../../packages/plugin-sdk/src/index.js';
+import type {
+ AgentScreenProps,
+ PageChrome,
+ Theme,
+ QuestionDraft,
+ SelectedModel,
+ ModelSetup,
+ TimelineFileContext,
+ MessageCopyObservation,
+} from '../../shared/contracts.js';
+import { sameValue } from '../../shared/value.js';
+import { observeBookmark, retryBookmark } from '../../shared/bookmark.js';
+import { copy, resolveLocale } from '../../shared/locale.js';
+import { type Presentation, type Node } from '../../shared/view.js';
+
+import {
+ attachmentFiles,
+ attachmentCapacity,
+ attachmentChild,
+ pastedFiles,
+ type DraftFilePaste,
+ selectedFile,
+} from '../../shared/attachments.js';
+
+import { observeDraft, draftReady, retryDraft, type DraftCheckpoint } from '../../shared/drafts.js';
+
+import {
+ type Page,
+ type Navigation,
+ navigationBookmark,
+ navigationKey,
+ type State,
+} from './state.js';
+import { utf8Bytes } from './messages.js';
+import {
+ intent,
+ canExecute,
+ selectedBackend,
+ catalogPending,
+ catalog,
+ observe,
+ history,
+ follow,
+ select,
+ readHistorySource,
+ mutationPending,
+ executionPending,
+ conversationRoute,
+ openInteraction,
+ rootCommands,
+ openUsage,
+ controlSidebar,
+ rootCommand,
+ loadQueueEdit,
+ initializeQueueEdit,
+ questionChild,
+ respondInteraction,
+ submitQuestion,
+ checkInteraction,
+ focusAttachments,
+ closeDialog,
+} from './actions.js';
+import { receipt, queryFailed, querySucceeded, delivery } from './results.js';
+import { frame } from './frame.js';
+
+export function handleEvent<N extends Node>(
+ presentation: Presentation<N>,
+ configuredLocale: string,
+ previous: State,
+ input: ComponentInput,
+ props: AgentScreenProps,
+ values: ComponentValues,
+) {
+ const target = presentation.target;
+
+ // Callbacks receive detached committed state. Mutations here belong only to
+ // the candidate frame and publish with its intents in one UI transaction.
+ const state = previous;
+ state.embedding = props;
+ if (target === 'tui') {
+ if (input.kind === 'raw' && input.key === 'navigation-checkpoint-retry')
+ retryBookmark(state.bookmark);
+ const restored = observeBookmark(
+ state.bookmark,
+ input,
+ navigationKey(state),
+ navigationBookmark(state),
+ (value) => {
+ const item = value as Navigation | null;
+ return item &&
+ Array.isArray(item.pages) &&
+ item.pages.length <= 3 &&
+ item.pages.every((page) =>
+ [
+ 'settings',
+ 'connections',
+ 'deployment',
+ 'projects',
+ 'imports',
+ 'apps',
+ 'recall',
+ 'scheduler',
+ ].includes(page),
+ ) &&
+ [
+ 'appearance',
+ 'interface',
+ 'models',
+ 'sessions',
+ 'host',
+ 'plugins',
+ 'assistant',
+ 'insights',
+ ...(state.web ? ['web'] : []),
+ ...(state.jev ? ['jev'] : []),
+ ...(props.workhub ? ['subagents'] : []),
+ ].includes(item.category)
+ ? item
+ : null;
+ },
+ );
+ if (restored) {
+ state.pages = restored.pages;
+ state.settingsCategory = restored.category;
+ }
+ }
+ if (input.kind === 'component' && input.child === 'attachment-list' && input.name === 'input')
+ input = input.event as ComponentInput;
+ if (input.kind === 'component' && input.child === 'composer' && input.name === 'attachments')
+ input = { kind: 'raw', key: 'attachments-open', event: {} };
+ if (input.kind === 'raw' && input.key === 'attachments-summary')
+ input = { ...input, key: 'attachments-open' };
+ if (input.kind === 'raw' && input.key === 'attachment-browser-close')
+ input = { ...input, key: 'attachment-close' };
+ state.revision++;
+ const actions: Action[] = [];
+ const events: Emission[] = [];
+ const locale =
+ target === 'tui'
+ ? effectiveLocale(state.preferences, configuredLocale, values.locale?.[0])
+ : resolveLocale(configuredLocale, values.locale?.[0]);
+ if (
+ target === 'tui' &&
+ appearanceEvent(
+ state.preferences,
+ input,
+ values.appearance?.[0] as Theme,
+ configuredLocale,
+ actions,
+ )
+ )
+ return frame(presentation, configuredLocale, state, values, actions);
+ if (input.kind === 'action') {
+ const outcome = input.event as ActionOutcome;
+ if (state.scheduleWatch === input.key) {
+ if (outcome.status === 'updated') state.scheduleAlerts = outcome.value as AgentScheduleAlerts;
+ else if (outcome.status === 'failed' || outcome.status === 'not_accepted')
+ state.scheduleWatch = null;
+ } else if (state.catalogWatch?.key === input.key) {
+ if (outcome.status === 'updated') {
+ state.catalogWatch.loading = false;
+ state.catalogFailed = false;
+ state.catalog = outcome.value as AgentSessionPage;
+ if (
+ !state.selected &&
+ !state.embedding.session &&
+ !state.embedding.fixed &&
+ state.catalog.sessions.length
+ )
+ select(state, actions, state.catalog.sessions[0]);
+ else if (state.selected) {
+ const selected = state.catalog.sessions.find(
+ (session) => session.id === state.selected!.id,
+ );
+ if (selected && selected.revision >= state.selected.revision) state.selected = selected;
+ }
+ } else if (outcome.status === 'failed' || outcome.status === 'not_accepted') {
+ state.catalogWatch = null;
+ state.catalogFailed = true;
+ }
+ } else if (state.watch?.key === input.key) {
+ if (outcome.status === 'updated')
+ delivery(state, outcome.value as AgentDeliveryPage, actions);
+ else if (outcome.status === 'failed' || outcome.status === 'not_accepted') {
+ state.watch = null;
+ state.notice = 'unavailable';
+ }
+ } else {
+ const request = state.pending[input.key];
+ if (request) {
+ delete state.pending[input.key];
+ if (
+ outcome.status === 'operation' &&
+ (request.kind === 'create' ||
+ request.kind === 'send' ||
+ request.kind === 'stop' ||
+ request.kind === 'queue' ||
+ request.kind === 'respond' ||
+ request.kind === 'markRead' ||
+ request.kind === 'uploadAttachment')
+ )
+ receipt(state, actions, request, outcome.reply);
+ else if (outcome.status === 'succeeded')
+ querySucceeded(state, actions, request, outcome.value);
+ else queryFailed(state, request);
+ // A newer original paint/intent may have arrived while this read
+ // was pending. The last attempted basis prevents self-retry.
+ if (request.kind === 'history' && request.implicit) follow(state, actions);
+ }
+ }
+ } else if (input.kind === 'component') {
+ if (input.name === 'chrome' && state.openedPages.includes(input.child as Page)) {
+ state.pageChrome[input.child as Page] = input.event as PageChrome;
+ return frame(presentation, configuredLocale, state, values, actions);
+ }
+
+ if (input.child === 'composer' && input.name === 'checkpoint') {
+ state.drafts = observeDraft(state.drafts, input.event as DraftCheckpoint, state.selected);
+ return frame(presentation, configuredLocale, state, values, actions);
+ }
+ if (input.child === 'composer' && input.name === 'draftCheck') {
+ retryDraft(state.drafts, (input.event as { owner: string }).owner);
+ return frame(presentation, configuredLocale, state, values, actions);
+ }
+ if (input.child === 'composer' && input.name === 'files' && target === 'tui') {
+ pastedFiles(state.attachments, input.event as DraftFilePaste);
+ return frame(presentation, configuredLocale, state, values, actions);
+ }
+ if (input.child === attachmentChild(state.attachments) && input.name === 'changed') {
+ if (state.dialog === 'attachments' && state.attachments.selection && !mutationPending(state))
+ state.attachments.selection.file = input.event as FileObservation;
+ return frame(presentation, configuredLocale, state, values, actions);
+ }
+ if (input.child === `permission-approval-${state.interaction?.attempt}`) {
+ if (state.dialog === 'approval') {
+ if (input.name === 'closed') closeDialog(state);
+ else if (input.name === 'check') checkInteraction(state, actions);
+ else if (input.name === 'submit') {
+ const event = input.event as {
+ requestId: string;
+ decision: 'deny' | 'allow';
+ scope: 'once' | 'turn' | 'session';
+ };
+ const request = state.interaction?.record?.request;
+ if (event.requestId === state.interaction?.requestId && request?.kind === 'permissions')
+ respondInteraction(state, actions, {
+ kind: 'permissions',
+ decision:
+ event.decision === 'deny'
+ ? { decision: 'deny' }
+ : {
+ decision: 'allow',
+ scope: event.scope,
+ permissions: request.request.permissions,
+ },
+ });
+ }
+ }
+ return frame(presentation, configuredLocale, state, values, actions);
+ }
+ if (input.child === `service-approval-${state.interaction?.attempt}`) {
+ if (state.dialog === 'approval') {
+ if (input.name === 'closed') closeDialog(state);
+ else if (input.name === 'check') checkInteraction(state, actions);
+ else if (input.name === 'submit') {
+ const event = input.event as { requestId: string; decision: 'allow' | 'deny' };
+ if (event.requestId === state.interaction?.requestId)
+ respondInteraction(state, actions, {
+ kind: 'service_approval',
+ decision: event.decision,
+ });
+ }
+ }
+ return frame(presentation, configuredLocale, state, values, actions);
+ }
+ if (input.child === questionChild(state)) {
+ if (state.dialog === 'question') {
+ if (input.name === 'closed') closeDialog(state);
+ else if (input.name === 'check') checkInteraction(state, actions);
+ else if (input.name === 'submit' && state.interaction?.record?.request.kind === 'form') {
+ const event = input.event as { requestId: string; answer: InteractionAnswer };
+ if (event.requestId === state.interaction.requestId && event.answer.kind === 'form')
+ respondInteraction(state, actions, event.answer);
+ } else if (input.name === 'submit')
+ submitQuestion(
+ state,
+ actions,
+ input.event as { requestId: string; answers: QuestionDraft[] },
+ );
+ }
+ return frame(presentation, configuredLocale, state, values, actions);
+ }
+ if (input.child === 'queue-editor') {
+ const edit = state.queueEdit;
+ if (input.name === 'changed') {
+ const draft = input.event as EditorObservation;
+ state.queueDraft = draft;
+ if (
+ edit?.command &&
+ draft.receipt?.owner === edit.command.owner &&
+ draft.receipt.sequence === edit.command.sequence
+ ) {
+ edit.phase = draft.receipt.outcome.status === 'applied' ? 'ready' : 'failed';
+ edit.command = null;
+ if (edit.phase === 'failed') state.notice = 'draftKept';
+ }
+ initializeQueueEdit(state);
+ } else if (state.dialog === 'queue' && edit) {
+ if (input.name === 'closed') closeDialog(state);
+ else if (
+ input.name === 'check' &&
+ state.uncertain &&
+ !Object.values(state.pending).some((request) => request.kind === 'receipt')
+ )
+ intent(
+ state,
+ actions,
+ { kind: 'receipt' },
+ 'receipts',
+ 'read',
+ state.uncertain.reference,
+ );
+ else if (!mutationPending(state)) {
+ if (
+ input.name === 'reload' &&
+ state.selected?.id === edit.target.sessionId &&
+ state.queue
+ ) {
+ loadQueueEdit(state, actions, {
+ ...edit.target,
+ expectedRevision: state.queue.revision,
+ });
+ } else if (input.name === 'save' && edit.phase === 'ready' && !edit.command) {
+ const draft = input.event as EditorObservation;
+ if (!sameValue(draft, state.queueDraft) || draft.snapshot.marked)
+ state.notice = 'draftKept';
+ else if (draft.snapshot.text.bytes > 65536) state.notice = 'tooLarge';
+ else
+ intent(
+ state,
+ actions,
+ { kind: 'queueRead', attempt: edit.attempt, draft },
+ 'reader',
+ 'read',
+ { start: 0, end: draft.snapshot.text.bytes },
+ {
+ text: {
+ child: 'queue-editor',
+ method: 'snapshot',
+ input: {
+ kind: 'at_revision',
+ owner: draft.snapshot.text.owner,
+ revision: draft.snapshot.text.revision,
+ },
+ },
+ },
+ );
+ }
+ }
+ }
+ return frame(presentation, configuredLocale, state, values, actions);
+ }
+ if (input.child === 'skill-library') {
+ if (input.name === 'closed' && state.dialog === 'skills') state.dialog = null;
+ return frame(presentation, configuredLocale, state, values, actions);
+ }
+ if (input.child === 'help') {
+ if (input.name === 'closed' && state.dialog === 'help') state.dialog = null;
+ return frame(presentation, configuredLocale, state, values, actions);
+ }
+ if (input.child === 'terminal-panel') {
+ if (input.name === 'closed' && state.dialog === 'terminal') state.dialog = null;
+ return frame(presentation, configuredLocale, state, values, actions);
+ }
+ if (input.child === 'session-details') {
+ if (input.name === 'closed' && state.dialog === 'details') state.dialog = null;
+ else if (input.name === 'changed') {
+ if (state.selected?.id === (input.event as { sessionId: string }).sessionId)
+ observe(state, actions);
+ }
+ return frame(presentation, configuredLocale, state, values, actions);
+ }
+ if (
+ [
+ 'assistant-preferences',
+ 'session-defaults',
+ 'network-settings',
+ 'insights',
+ 'web-settings',
+ 'jev-settings',
+ 'subagent-settings',
+ ].includes(input.child) &&
+ input.name === 'closed' &&
+ state.pages.at(-1) === 'settings'
+ )
+ state.pages.pop();
+ if (input.child === 'apps') {
+ if (input.name === 'closed' && state.pages.at(-1) === 'apps') state.pages.pop();
+ if (input.name === 'manage' && state.pages.at(-1) === 'apps') state.pages.push('deployment');
+ }
+ if (input.child === 'scheduler') {
+ if (input.name === 'closed' && state.pages.at(-1) === 'scheduler') state.pages.pop();
+ else if (input.name === 'connections' && state.pages.at(-1) === 'scheduler')
+ state.pages.push('connections');
+ else if (
+ input.name === 'selected' &&
+ state.pages.at(-1) === 'scheduler' &&
+ !state.embedding.fixed &&
+ !mutationPending(state) &&
+ !catalogPending(state)
+ ) {
+ select(state, actions, input.event as AgentSession);
+ state.pages = [];
+ catalog(state);
+ }
+ return frame(presentation, configuredLocale, state, values, actions);
+ }
+ if (input.child === 'recall') {
+ if (input.name === 'closed' && state.pages.at(-1) === 'recall') state.pages.pop();
+ else if (
+ input.name === 'selected' &&
+ state.pages.at(-1) === 'recall' &&
+ !state.embedding.fixed &&
+ !mutationPending(state) &&
+ !catalogPending(state)
+ ) {
+ const selected = input.event as { session: AgentSession; anchor: number | null };
+ select(state, actions, selected.session);
+ state.recallAnchor = selected.anchor;
+ state.pages = [];
+ }
+ return frame(presentation, configuredLocale, state, values, actions);
+ }
+ if (input.child === 'imports') {
+ if (input.name === 'closed' && state.pages.at(-1) === 'imports') state.pages.pop();
+ else if (input.name === 'changed') catalog(state);
+ else if (
+ input.name === 'selected' &&
+ state.pages.at(-1) === 'imports' &&
+ !state.embedding.fixed &&
+ !mutationPending(state) &&
+ !catalogPending(state)
+ ) {
+ select(state, actions, (input.event as { session: AgentSession }).session);
+ state.pages = [];
+ catalog(state);
+ }
+ return frame(presentation, configuredLocale, state, values, actions);
+ }
+ if (input.child === 'projects') {
+ if (input.name === 'closed' && state.pages.at(-1) === 'projects') state.pages.pop();
+ else if (input.name === 'changed') {
+ catalog(state);
+ if (state.selected) observe(state, actions);
+ } else if (
+ input.name === 'selected' &&
+ state.pages.at(-1) === 'projects' &&
+ !state.embedding.fixed &&
+ !mutationPending(state) &&
+ !catalogPending(state)
+ ) {
+ intent(state, actions, { kind: 'create' }, 'agent', 'create', {
+ title: copy(locale).new,
+ labels: [],
+ workspace: {
+ kind: 'project',
+ ...(input.event as {
+ project: AgentProjectRef;
+ selection: AgentDirectorySelection;
+ }),
+ },
+ });
+ state.pages = [];
+ }
+ return frame(presentation, configuredLocale, state, values, actions);
+ }
+ if (input.child === 'directory-picker') {
+ if (state.dialog === 'directories' && conversationRoute(state)) {
+ if (input.name === 'closed') state.dialog = null;
+ else if (
+ input.name === 'selected' &&
+ !state.embedding.fixed &&
+ !mutationPending(state) &&
+ !catalogPending(state)
+ ) {
+ intent(state, actions, { kind: 'create' }, 'agent', 'create', {
+ title: copy(locale).new,
+ labels: [],
+ workspace: { kind: 'directory', selection: input.event as AgentDirectorySelection },
+ });
+ state.dialog = null;
+ }
+ }
+ return frame(presentation, configuredLocale, state, values, actions);
+ }
+ if (input.child === 'command-palette') {
+ if (state.dialog === 'palette') {
+ if (input.name === 'closed') state.dialog = null;
+ else if (input.name === 'invoked') {
+ const command = rootCommands(state, locale, target).find(
+ (command) => command.id === (input.event as { id: string }).id && !command.disabled,
+ );
+ if (
+ command &&
+ rootCommand(state, actions, locale, command.kind, target) &&
+ state.dialog === 'palette'
+ ) {
+ state.dialog = null;
+ if (command.kind === 'workhub-open') events.push({ name: 'workhub', value: {} });
+ }
+ } else if (
+ input.name === 'selected' &&
+ !state.embedding.fixed &&
+ !mutationPending(state) &&
+ !catalogPending(state)
+ ) {
+ select(state, actions, input.event as AgentSession);
+ state.pages = [];
+ state.dialog = null;
+ }
+ }
+ return frame(presentation, configuredLocale, state, values, actions, events);
+ }
+
+ if (target === 'tui' && input.child === 'queue' && input.name === 'activated') {
+ const event = input.event as RowActivated;
+ const queue = state.queue;
+ const row = queue?.entries.find((row) => row.messageId === event.row);
+ if (
+ conversationRoute(state) &&
+ !state.dialog &&
+ !mutationPending(state) &&
+ !executionPending(state) &&
+ state.selected &&
+ queue &&
+ event.revision === queue.revision &&
+ row &&
+ row.kind !== 'in_flight'
+ ) {
+ let edit: AgentQueueAction | null = null;
+ if (event.action === 'edit')
+ loadQueueEdit(state, actions, {
+ sessionId: state.selected.id,
+ messageId: row.messageId,
+ expectedRevision: event.revision,
+ });
+ else if (event.action === 'retract') edit = { kind: 'retract', messageId: row.messageId };
+ else if (event.action === 'promote' && row.kind === 'followup' && state.run)
+ edit = { kind: 'promote', messageId: row.messageId };
+ else if (event.action === 'up' || event.action === 'down') {
+ const ids = queue.entries
+ .filter((entry) => entry.kind === row.kind)
+ .map((entry) => entry.messageId);
+ const index = ids.indexOf(row.messageId),
+ next = index + (event.action === 'up' ? -1 : 1);
+ if (next >= 0 && next < ids.length) {
+ [ids[index], ids[next]] = [ids[next], ids[index]];
+ edit = { kind: 'reorder', messageIds: [ids[0], ...ids.slice(1)] };
+ }
+ }
+ if (edit)
+ intent(
+ state,
+ actions,
+ { kind: 'queue', sessionId: state.selected.id },
+ 'agent',
+ 'editQueue',
+ { sessionId: state.selected.id, expectedRevision: event.revision, edit },
+ );
+ }
+ } else if (input.child === 'timeline' && input.name === 'messageCopy') {
+ const copied = input.event as MessageCopyObservation | null;
+ if (
+ !copied ||
+ (state.dialog === 'palette' &&
+ copied.request === state.copyRequest &&
+ copied.sessionId === state.selected?.id &&
+ copied.through === state.window?.page.through &&
+ copied.landing === state.landing.sequence)
+ )
+ state.messageCopy = copied;
+ } else if (input.child === 'timeline' && input.name === 'fileContext') {
+ const context = input.event as TimelineFileContext | null;
+ if (
+ !context ||
+ (context.sessionId === state.selected?.id &&
+ context.through === state.window?.page.through &&
+ context.landing === state.landing.sequence)
+ )
+ state.fileContext = context;
+ } else if (input.child === 'timeline' && input.name === 'browseClosed') {
+ if ((input.event as { request: number }).request === state.browse) state.browse = null;
+ } else if (input.child === 'timeline' && input.name === 'searchClosed') {
+ state.searchOpen = false;
+ } else if (input.child === 'timeline' && input.name === 'tailReady') {
+ const receipt = input.event as NonNullable<State['tailReceipt']>;
+ if (receipt.landing === state.landing.sequence) {
+ state.tailReceipt = receipt;
+ follow(state, actions);
+ }
+ } else if (input.name === 'closed' && input.child === state.pages.at(-1)) state.pages.pop();
+ else if (input.child === 'connections' && input.name === 'changed') {
+ state.catalogEpoch++;
+ state.modelSetup = 'loading';
+ state.modelRefreshing = !!state.model;
+ } else if (input.child === 'composer' && input.name === 'changed') {
+ const observation = input.event as EditorObservation;
+ if (
+ state.command &&
+ observation.receipt?.owner === state.command.owner &&
+ observation.receipt.sequence === state.command.sequence
+ ) {
+ if (observation.receipt.outcome.status === 'conflict') state.notice = 'draftKept';
+ state.command = null;
+ }
+ } else if (
+ input.child === 'composer' &&
+ input.name === 'send' &&
+ draftReady(state.drafts, state.selected) &&
+ !state.attachments.paste &&
+ conversationRoute(state) &&
+ !state.dialog &&
+ !state.image &&
+ !state.source &&
+ !executionPending(state) &&
+ state.notice !== 'unavailable' &&
+ !mutationPending(state) &&
+ state.selected &&
+ !state.selected.archived &&
+ canExecute(state) &&
+ !state.loading &&
+ (!state.run || (target === 'tui' && state.queue !== null)) &&
+ !state.command
+ ) {
+ const draft = input.event as EditorObservation;
+ if (draft.snapshot.marked) state.notice = 'draftKept';
+ else if (draft.snapshot.text.bytes > 65536) state.notice = 'tooLarge';
+ else
+ intent(
+ state,
+ actions,
+ {
+ kind: 'read',
+ dataSource: state.selected.dataSource,
+ attachments: attachmentFiles(
+ state.attachments,
+ state.selected.id,
+ state.selected.dataSource,
+ ).map((file) => file.id),
+ draft,
+ sessionId: state.selected.id,
+ backend: selectedBackend(state),
+ },
+ 'reader',
+ 'read',
+ { start: 0, end: draft.snapshot.text.bytes },
+ {
+ text: {
+ child: 'composer',
+ method: 'snapshot',
+ input: {
+ kind: 'at_revision',
+ owner: draft.snapshot.text.owner,
+ revision: draft.revision,
+ },
+ },
+ },
+ );
+ } else if (input.child === 'composer' && input.name === 'stop') {
+ rootCommand(state, actions, locale, 'stop', target);
+ } else if (input.child === 'models' && input.name === 'backends') {
+ state.hasBackends = (input.event as { available: boolean }).available;
+ } else if (
+ input.child === 'models' &&
+ input.name === 'executor' &&
+ !mutationPending(state) &&
+ !executionPending(state)
+ ) {
+ state.executor = input.event as AgentExecutorSelection | null;
+ } else if (input.child === 'models' && input.name === 'setup') {
+ const setup = input.event as ModelSetup;
+ if (setup.catalogEpoch === state.catalogEpoch) state.modelSetup = setup.kind;
+ } else if (
+ input.child === 'models' &&
+ input.name === 'connections' &&
+ conversationRoute(state) &&
+ !state.dialog
+ ) {
+ state.pages = ['connections'];
+ } else if (
+ input.child === 'models' &&
+ input.name === 'selected' &&
+ !state.executor &&
+ !mutationPending(state) &&
+ !executionPending(state)
+ ) {
+ state.model = input.event as SelectedModel;
+ state.modelRefreshing = false;
+ } else if (
+ input.child === 'navigation' &&
+ !state.embedding.fixed &&
+ input.name === 'select' &&
+ state.dialog === null &&
+ !mutationPending(state) &&
+ !catalogPending(state)
+ ) {
+ const { sessionId, details, usage } = input.event as {
+ sessionId: string;
+ details?: boolean;
+ usage?: boolean;
+ };
+ const session = state.catalog?.sessions.find((item) => item.id === sessionId);
+ if (session && usage && target === 'tui') openUsage(state, session);
+ else if (session) {
+ state.pages = [];
+ if (state.selected?.id !== session.id) select(state, actions, session);
+ if (details && !executionPending(state)) state.dialog = 'details';
+ if (target === 'tui') controlSidebar(state, 'dismiss_transient');
+ }
+ } else if (input.child === 'image-viewer' && input.name === 'closed') {
+ state.image = null;
+ state.followAttempt = null;
+ follow(state, actions);
+ } else if (
+ input.child === 'timeline' &&
+ input.name === 'viewImage' &&
+ state.selected &&
+ state.window &&
+ !state.loading
+ ) {
+ const sequence = (input.event as { sequence: number }).sequence;
+ const chunk = state.window.page.chunks.find(
+ (c) => c.sequence === sequence && c.tool?.result?.image,
+ );
+ if (chunk)
+ state.image = {
+ sessionId: state.selected.id,
+ through: state.window.page.through,
+ sequence: chunk.tool!.result!.sequence,
+ };
+ } else if (input.child === 'timeline' && state.source) {
+ const source = state.source;
+ if (input.name === 'sourceBack') state.source = null;
+ else if (!source.loading && input.name === 'sourceField') {
+ const field = (input.event as { field: SourceField }).field;
+ if (field !== source.field && source.fields.includes(field)) {
+ source.field = field;
+ source.ready = false;
+ source.format = null;
+ source.chunk = { ...source.chunk, offset: 0, totalBytes: 0, text: '' };
+ readHistorySource(state, actions, { kind: 'start' });
+ }
+ } else if (!source.loading) {
+ const chunk = source.chunk;
+ const end = chunk.offset + utf8Bytes(chunk.text);
+ const position: SourcePosition | null =
+ input.name === 'sourceRetry' && source.failed
+ ? source.position
+ : input.name === 'sourceFirst' && chunk.offset > 0
+ ? { kind: 'start' }
+ : input.name === 'sourceLast' && end < chunk.totalBytes
+ ? { kind: 'tail' }
+ : input.name === 'sourcePrevious' && chunk.offset > 0
+ ? { kind: 'before', offset: chunk.offset }
+ : input.name === 'sourceNext' && end < chunk.totalBytes
+ ? { kind: 'after', offset: end }
+ : null;
+ if (position) readHistorySource(state, actions, position);
+ }
+ } else if (input.child === 'timeline' && !state.loading && state.window) {
+ const page = state.window.page;
+ if (
+ input.name === 'markRead' &&
+ state.selected?.hasUnread &&
+ !state.selected.archived &&
+ !mutationPending(state) &&
+ conversationRoute(state) &&
+ state.dialog === null &&
+ !state.image &&
+ !state.searchOpen
+ ) {
+ const event = input.event as {
+ sessionId: string;
+ sequence: number;
+ field: 'body' | 'thinking';
+ };
+ const chunk =
+ event.sessionId === state.selected.id &&
+ page.chunks.find(
+ (chunk) =>
+ chunk.sequence === event.sequence &&
+ (chunk.field ?? 'body') === event.field &&
+ (chunk.role === 'user' || chunk.role === 'assistant'),
+ );
+ if (chunk)
+ intent(
+ state,
+ actions,
+ { kind: 'markRead', sessionId: state.selected.id },
+ 'agent',
+ 'markRead',
+ {
+ sessionId: state.selected.id,
+ messageId: chunk.messageId,
+ },
+ );
+ } else if (input.name === 'source') {
+ const chunk = page.chunks.find(
+ (chunk) =>
+ chunk.sequence === (input.event as { sequence: number }).sequence &&
+ (chunk.field ?? 'body') === ((input.event as { field?: string }).field ?? 'body'),
+ );
+ if (chunk)
+ state.source = {
+ through: page.through,
+ chunk: {
+ sequence: chunk.sequence,
+ offset: chunk.offset,
+ totalBytes: chunk.totalBytes,
+ text: chunk.text,
+ },
+ navigation: ++state.navigation,
+ landing: { sequence: state.navigation, edge: 'start' },
+ loading: false,
+ failed: false,
+ field: chunk.field ?? 'body',
+ fields: [chunk.field ?? 'body'],
+ format: 'text',
+ ready: true,
+ position: { kind: 'after', offset: chunk.offset },
+ };
+ } else if (input.name === 'toolSource') {
+ const event = input.event as { sequence: number; field: 'arguments' | 'result' };
+ const chunk = page.chunks.find((chunk) => chunk.sequence === event.sequence);
+ const reference =
+ event.field === 'arguments'
+ ? chunk?.tool?.call
+ : event.field === 'result'
+ ? chunk?.tool?.result
+ : null;
+ if (reference) {
+ state.source = {
+ through: page.through,
+ chunk: { sequence: reference.sequence, offset: 0, totalBytes: 0, text: '' },
+ navigation: state.navigation,
+ landing: { sequence: state.navigation, edge: 'start' },
+ loading: false,
+ failed: false,
+ field: event.field,
+ fields: [],
+ format: null,
+ ready: false,
+ position: { kind: 'start' },
+ };
+ readHistorySource(state, actions, { kind: 'start' });
+ }
+ } else if (
+ input.name === 'older' &&
+ page.before &&
+ !Object.values(state.pending).some(
+ (request) => request.kind === 'history' && request.epoch === state.epoch,
+ )
+ )
+ history(
+ state,
+ actions,
+ { kind: 'before', cursor: page.before },
+ page.through,
+ false,
+ false,
+ target === 'tui',
+ );
+ else if (
+ input.name === 'newer' &&
+ page.after &&
+ !Object.values(state.pending).some(
+ (request) => request.kind === 'history' && request.epoch === state.epoch,
+ )
+ )
+ history(
+ state,
+ actions,
+ { kind: 'after', cursor: page.after },
+ page.through,
+ false,
+ false,
+ target === 'tui',
+ );
+ else if (input.name === 'latest')
+ history(state, actions, { kind: 'tail' }, state.latestKnown, true);
+ }
+ } else if (input.kind === 'raw') {
+ if (
+ input.key === 'schedule-reminder' &&
+ !state.dialog &&
+ !state.embedding.fixed &&
+ state.scheduleAlerts.reminders[0]
+ ) {
+ state.scheduleRequested = {
+ sequence: ++state.serial,
+ taskId: state.scheduleAlerts.reminders[0].taskId,
+ };
+ rootCommand(state, actions, locale, 'scheduler-open', target);
+ }
+ if (input.key === 'catalog-search') input = { ...input, key: 'palette-open' };
+ if (input.key === 'home-projects') rootCommand(state, actions, locale, 'projects-open', target);
+ else if (input.key === 'home-imports')
+ rootCommand(state, actions, locale, 'imports-open', target);
+ else if (input.key === 'home-new') rootCommand(state, actions, locale, 'create', target);
+ if (
+ input.key.startsWith('file-paste-') &&
+ state.attachments.paste &&
+ !mutationPending(state) &&
+ !state.attachments.paste.command
+ ) {
+ const paste = state.attachments.paste,
+ file = paste.observed.selection;
+ if (input.key === 'file-paste-cancel')
+ paste.command = { selection: selectedFile(file), action: 'cancel' };
+ else if (input.key === 'file-paste-skip' && ['ready', 'failed'].includes(file.phase))
+ paste.command = { selection: selectedFile(file), action: 'next' };
+ else if (input.key === 'file-paste-retry') {
+ if (file.phase === 'failed' && paste.observed.retryable)
+ paste.command = { selection: selectedFile(file), action: 'retry' };
+ else if (file.phase === 'ready') paste.attempted = null;
+ }
+ }
+ if (
+ input.key === 'attachments-open' &&
+ draftReady(state.drafts, state.selected) &&
+ !state.attachments.paste &&
+ conversationRoute(state) &&
+ !state.dialog &&
+ state.selected &&
+ !state.loading &&
+ !state.source &&
+ !state.image &&
+ !mutationPending(state)
+ ) {
+ const files = attachmentFiles(
+ state.attachments,
+ state.selected.id,
+ state.selected.dataSource,
+ );
+ state.attachments.selection = {
+ attempt: ++state.serial,
+ sessionId: state.selected.id,
+ dataSource: state.selected.dataSource,
+ file: null,
+ browse: files.length === 0,
+ selected: files[0]?.id ?? null,
+ focus: null,
+ };
+ focusAttachments(state);
+ state.dialog = 'attachments';
+ } else if (input.key === 'attachment-close' && state.dialog === 'attachments') {
+ closeDialog(state);
+ } else if (
+ (input.key === 'attachment-check' || input.key === 'attachment-browser-check') &&
+ state.dialog === 'attachments'
+ ) {
+ if (
+ state.uncertain &&
+ !Object.values(state.pending).some((request) => request.kind === 'receipt')
+ )
+ intent(state, actions, { kind: 'receipt' }, 'receipts', 'read', state.uncertain.reference);
+ } else if (
+ state.dialog === 'attachments' &&
+ state.attachments.selection &&
+ draftReady(state.drafts, state.selected) &&
+ state.selected?.id === state.attachments.selection.sessionId &&
+ sameValue(state.selected.dataSource, state.attachments.selection.dataSource) &&
+ !mutationPending(state)
+ ) {
+ const selection = state.attachments.selection;
+ const files = attachmentFiles(state.attachments, selection.sessionId, selection.dataSource);
+ if (input.key === 'attachment-list-back') {
+ selection.browse = false;
+ focusAttachments(state);
+ } else if (/^attachment-select-[0-7]$/.test(input.key)) {
+ selection.selected =
+ files[Number(input.key.slice('attachment-select-'.length))]?.id ?? selection.selected;
+ } else if (
+ input.key === 'attachment-browse' &&
+ !state.selected.archived &&
+ attachmentCapacity(state.attachments, selection.sessionId, selection.dataSource)
+ ) {
+ if (
+ ['activate', 'submit'].includes((input.event as { kind?: string } | null)?.kind ?? '')
+ ) {
+ selection.selected = null;
+ selection.browse = true;
+ selection.focus = null;
+ }
+ } else if (input.key.startsWith('attachment-remove-')) {
+ const id = input.key.slice('attachment-remove-'.length);
+ state.attachments.files = state.attachments.files.filter(
+ (file) =>
+ file.sessionId !== selection.sessionId ||
+ !sameValue(file.dataSource, selection.dataSource) ||
+ file.id !== id,
+ );
+ const remaining = attachmentFiles(
+ state.attachments,
+ selection.sessionId,
+ selection.dataSource,
+ );
+ const removed = files.findIndex((file) => file.id === id);
+ if (removed >= 0) {
+ selection.selected = remaining[Math.min(removed, remaining.length - 1)]?.id ?? null;
+ focusAttachments(state);
+ }
+ } else if (
+ input.key === 'attachment-upload' &&
+ !state.selected.archived &&
+ attachmentCapacity(state.attachments, selection.sessionId, selection.dataSource) &&
+ selection.file?.phase === 'ready' &&
+ selection.file.file
+ ) {
+ const file = selection.file;
+ state.notice = null;
+ intent(
+ state,
+ actions,
+ {
+ kind: 'uploadAttachment',
+ dataSource: selection.dataSource,
+ sessionId: selection.sessionId,
+ attempt: selection.attempt,
+ },
+ 'agent',
+ 'uploadAttachment',
+ { sessionId: selection.sessionId, ...file.file },
+ {
+ file: {
+ child: attachmentChild(state.attachments),
+ method: 'snapshot',
+ input: { owner: file.owner, revision: file.revision },
+ },
+ },
+ );
+ }
+ }
+
+ if (
+ (input.key.startsWith('question-open-') || input.key.startsWith('approval-open-')) &&
+ !state.dialog &&
+ !mutationPending(state) &&
+ state.selected &&
+ !state.selected.archived
+ ) {
+ const requestId = input.key.slice(14);
+ const kind = state.interactions?.entries.find((entry) => entry.requestId === requestId)?.kind;
+ if (
+ (input.key.startsWith('question-') && (kind === 'question' || kind === 'form')) ||
+ (input.key.startsWith('approval-') &&
+ (kind === 'service_approval' || kind === 'permissions'))
+ ) {
+ openInteraction(state, actions, requestId, kind);
+ }
+ } else if (
+ input.key === 'question-close' &&
+ (state.dialog === 'question' || state.dialog === 'approval')
+ )
+ closeDialog(state);
+ else if (
+ input.key === 'question-check' &&
+ (state.dialog === 'question' || state.dialog === 'approval')
+ )
+ checkInteraction(state, actions);
+ else if (input.key === 'catalog-filter-all' || input.key === 'catalog-filter-needs_you') {
+ const filter = input.key === 'catalog-filter-all' ? 'all' : 'needs_you';
+ if (!mutationPending(state) && !catalogPending(state) && state.catalogFilter !== filter) {
+ state.catalogFilter = filter;
+ state.catalog = null;
+ catalog(state);
+ }
+ } else if (['catalog-refresh', 'catalog-more', 'catalog-previous'].includes(input.key)) {
+ if (!mutationPending(state) && !catalogPending(state)) {
+ const page = state.catalog;
+ if (input.key === 'catalog-refresh') catalog(state);
+ else if (!state.catalogFailed && page) {
+ if (input.key === 'catalog-more' && page.after)
+ catalog(state, {
+ kind: 'after',
+ revision: page.revision,
+ cursor: page.after,
+ });
+ else if (input.key === 'catalog-previous' && page.before)
+ catalog(state, {
+ kind: 'before',
+ revision: page.revision,
+ cursor: page.before,
+ });
+ }
+ }
+ } else if (input.key === 'palette-open' && state.dialog === null) {
+ state.dialog = 'palette';
+ state.copyRequest = ++state.serial;
+ state.messageCopy = null;
+ } else if (input.key === 'help-open') {
+ if (state.dialog === null) rootCommand(state, actions, locale, 'help-open', target);
+ } else if (input.key === 'agent-screen-dialog') {
+ const selection = state.attachments.selection;
+ if (
+ target === 'tui' &&
+ state.dialog === 'attachments' &&
+ selection?.browse &&
+ attachmentFiles(state.attachments, selection.sessionId, selection.dataSource).length
+ ) {
+ selection.browse = false;
+ focusAttachments(state);
+ } else closeDialog(state);
+ } else if (input.key === 'page-back' && state.pages.length > 0 && !state.dialog) {
+ const page = state.pages.at(-1)!;
+ if (page === 'settings') state.pages.pop();
+ else if (state.pageChrome[page]?.backEnabled) state.pageBack[page] = ++state.serial;
+ } else if (
+ state.pages.at(-1) === 'settings' &&
+ settingsEvent(state, input, values.appearance?.[0] as Theme)
+ ) {
+ // Settings write through the existing preference checkpoint.
+ } else if (input.key === 'navigation-back' && state.dialog === null) {
+ if ('open' in state.sidebar) state.sidebar.open = false;
+ else controlSidebar(state, 'toggle');
+ } else {
+ const command = rootCommands(state, locale, target).find(
+ (command) => command.kind === input.key,
+ );
+ if (
+ command &&
+ rootCommand(state, actions, locale, command.kind, target) &&
+ command.kind === 'workhub-open'
+ )
+ events.push({ name: 'workhub', value: {} });
+ }
+ }
+ return frame(presentation, configuredLocale, state, values, actions, events);
+}
diff --git a/plugins/agent/agent-screen/frame.ts b/plugins/agent/agent-screen/frame.ts
new file mode 100644
index 0000000000..343bd4ec49
--- /dev/null
+++ b/plugins/agent/agent-screen/frame.ts
@@ -0,0 +1,1223 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+ effectiveAppearance,
+ effectiveLocale,
+ appearanceEvent,
+ appearanceView,
+} from '../../shared/appearance-editor.js';
+import { tuiText } from '../../shared/tui-locale.js';
+import type {
+ Action,
+ Emission,
+ Shortcut,
+ RowsProps,
+ ComponentValues,
+} from '../../../packages/plugin-sdk/src/index.js';
+import type {
+ Theme,
+ QueueEditorProps,
+ QuestionEditorProps,
+ ServiceApprovalProps,
+} from '../../shared/contracts.js';
+import { sameValue } from '../../shared/value.js';
+import { bookmarkView } from '../../shared/bookmark.js';
+import { copy, resolveLocale } from '../../shared/locale.js';
+import {
+ compactLabel,
+ fill,
+ output,
+ pixels,
+ type Presentation,
+ type Node,
+} from '../../shared/view.js';
+
+import {
+ attachmentFiles,
+ attachmentView,
+ attachmentSummary,
+ pasteView,
+} from '../../shared/attachments.js';
+
+import {
+ draftReady,
+ retainSelectedDraft,
+ draftStatuses,
+ nextDraftStep,
+} from '../../shared/drafts.js';
+
+import { navigationBookmark, navigationKey, type State } from './state.js';
+import { utf8Bytes, messages, turnTimings } from './messages.js';
+import {
+ intent,
+ canExecute,
+ catalogPending,
+ select,
+ mutationPending,
+ uploadPastedFile,
+ executionPending,
+ conversationRoute,
+ reviewTarget,
+ rootCommands,
+ questionChild,
+ closeDialog,
+} from './actions.js';
+import { navigationView } from './navigation-view.js';
+import { headerView } from './header-view.js';
+import { pagesView } from './pages-view.js';
+
+export function frame<N extends Node>(
+ presentation: Presentation<N>,
+ locale: string,
+ state: State,
+ values: ComponentValues,
+ actions: Action[] = [],
+ events: Emission[] = [],
+) {
+ const target = presentation.target;
+ const currentPage = state.pages.at(-1);
+ if (currentPage && !state.openedPages.includes(currentPage)) state.openedPages.push(currentPage);
+ const requested = state.embedding.session;
+ if (requested && !sameValue(requested, state.externalSelection) && !mutationPending(state)) {
+ state.externalSelection = requested;
+ select(state, actions, null);
+ state.loading = true;
+ closeDialog(state);
+ intent(state, actions, { kind: 'openSession', selection: requested }, 'agent', 'observe', {
+ sessionId: requested.id,
+ });
+ }
+ if (state.dialog !== 'palette') {
+ state.copyRequest = null;
+ state.messageCopy = null;
+ }
+ retainSelectedDraft(state.drafts, state.selected);
+ if (
+ !Object.values(state.pending).some(
+ (request) => request.kind === 'draftPage' || request.kind === 'draftReceipt',
+ )
+ ) {
+ const step = nextDraftStep(state.drafts, state.attachments);
+ if (step)
+ intent(
+ state,
+ actions,
+ step,
+ step.kind === 'draftPage' ? 'history' : 'receipts',
+ step.kind === 'draftPage' ? 'page' : 'read',
+ step.kind === 'draftPage' ? { after: step.after, limit: 1 } : step.original.reference,
+ );
+ }
+ uploadPastedFile(state, actions);
+ if (!conversationRoute(state) || state.source || state.image || !state.selected) {
+ state.searchOpen = false;
+ state.browse = null;
+ }
+ const baseTheme = values.appearance?.[0] as Theme;
+ if (target === 'tui' && state.preferences.open && !state.preferences.draft)
+ appearanceEvent(
+ state.preferences,
+ { kind: 'raw', key: 'appearance-open', event: {} },
+ baseTheme,
+ locale,
+ actions,
+ );
+ locale =
+ target === 'tui'
+ ? effectiveLocale(state.preferences, locale, values.locale?.[0])
+ : resolveLocale(locale, values.locale?.[0]);
+ const theme = target === 'tui' ? effectiveAppearance(state.preferences, baseTheme) : baseTheme;
+ const text = copy(locale),
+ ui = presentation.paint(theme),
+ busy = mutationPending(state);
+ const catalogBusy = busy || catalogPending(state);
+ const commands = rootCommands(state, locale, target);
+ const navigation = navigationView(presentation, state, theme, locale);
+ const timelineMessages = messages(state);
+ const timeline = presentation.slot(
+ 'timeline',
+ 'timeline',
+ {
+ theme,
+ locale,
+ messages: timelineMessages,
+ timings: turnTimings(state, timelineMessages),
+ copyRequest: target === 'tui' && !state.image ? state.copyRequest : null,
+ browse: state.browse,
+ markReadEnabled:
+ !!state.selected?.hasUnread &&
+ !state.selected.archived &&
+ !busy &&
+ conversationRoute(state) &&
+ state.dialog === null &&
+ !state.source &&
+ !state.image,
+ search:
+ target === 'tui' && state.searchOpen && state.selected
+ ? { sessionId: state.selected.id, epoch: state.epoch, through: state.latestKnown }
+ : null,
+ source: state.source
+ ? {
+ kind: state.source.field === 'body' ? 'message' : state.source.field,
+ fields: state.source.fields,
+ format: state.source.format ?? 'text',
+ ready: state.source.ready,
+ key: `${state.source.field}-${state.source.through}-${state.source.chunk.sequence}-${state.source.chunk.offset}`,
+ text: state.source.chunk.text,
+ atStart: state.source.chunk.offset === 0,
+ atEnd:
+ state.source.chunk.offset + utf8Bytes(state.source.chunk.text) ===
+ state.source.chunk.totalBytes,
+ loading: state.source.loading,
+ failed: state.source.failed,
+ navigation: state.source.landing.sequence,
+ landing: state.source.landing.edge,
+ }
+ : null,
+ loading: state.loading,
+ older: !!state.window?.page.before,
+ newer: !!state.window?.page.after,
+ latest:
+ !!state.window &&
+ (!state.window.latest ||
+ state.latestKnown > state.window.page.through ||
+ state.live.some(
+ (fragment) =>
+ fragment.closedAt !== null && fragment.closedAt <= state.window!.page.through,
+ )),
+ landing: state.landing,
+ toolContext:
+ state.selected && state.window && state.window.page.through > 0
+ ? { sessionId: state.selected.id, through: state.window.page.through }
+ : null,
+ liveTools:
+ state.window?.latest && state.run
+ ? [
+ ...new Set(
+ state.window.page.chunks.flatMap((chunk) =>
+ chunk.turnId === state.run!.turnId && chunk.tool ? [chunk.tool.toolUseId] : [],
+ ),
+ ),
+ ]
+ : [],
+ waitingTools: state.window?.latest
+ ? (state.interactions?.entries ?? [])
+ .filter(
+ (entry) =>
+ !(
+ state.interaction?.record?.requestId === entry.requestId &&
+ state.interaction.record.outcome
+ ),
+ )
+ .flatMap((entry) => (entry.toolUseId === null ? [] : [entry.toolUseId]))
+ : [],
+ live: !!state.run && !!state.window?.latest,
+ },
+ {
+ older: 'older',
+ newer: 'newer',
+ latest: 'latest',
+ markRead: 'mark-read',
+ source: 'source',
+ toolSource: 'tool-source',
+ fileContext: 'fileContext',
+ messageCopy: 'messageCopy',
+ viewImage: 'view-image',
+ sourceFirst: 'source-first',
+ sourcePrevious: 'source-previous',
+ sourceNext: 'source-next',
+ sourceLast: 'source-last',
+ sourceBack: 'source-back',
+ sourceRetry: 'source-retry',
+ sourceField: 'source-field',
+ tailReady: 'tail-ready',
+ searchClosed: 'search-closed',
+ browseClosed: 'browse-closed',
+ },
+ );
+ const imageViewer = presentation.slot(
+ 'image-viewer',
+ 'imageViewer',
+ {
+ theme,
+ locale,
+ image: state.image,
+ active: !!state.image && conversationRoute(state),
+ },
+ { closed: 'image-closed' },
+ );
+ const attached = attachmentFiles(
+ state.attachments,
+ state.selected?.id,
+ state.selected?.dataSource,
+ );
+ const attachmentsDisabled =
+ !state.selected ||
+ !draftReady(state.drafts, state.selected) ||
+ busy ||
+ state.loading ||
+ !!state.attachments.paste;
+ const attachmentsButton = ui.button(
+ 'attachments-open',
+ `${text.attachmentTitle}${attached.length ? ` (${attached.length})` : ''}`,
+ attachmentsDisabled,
+ );
+ const composer = presentation.slot(
+ 'composer',
+ 'composer',
+ {
+ context: state.selected?.id ?? null,
+ dataSource: state.selected?.dataSource ?? null,
+ attachments: state.attachments.files,
+ drafts: draftStatuses(state.drafts),
+ theme,
+ locale,
+ busy:
+ busy ||
+ !!state.attachments.paste ||
+ executionPending(state) ||
+ (target === 'tui' && !!state.run && !state.queue),
+ filePaste:
+ target === 'tui' &&
+ state.selected &&
+ !state.selected.archived &&
+ conversationRoute(state) &&
+ !state.dialog &&
+ !state.source &&
+ !state.image &&
+ !state.loading &&
+ (!busy || !!state.attachments.paste) &&
+ (!state.attachments.paste ||
+ (state.attachments.paste.observed.context === state.selected.id &&
+ sameValue(state.attachments.paste.observed.dataSource, state.selected.dataSource)))
+ ? {
+ context: state.selected.id,
+ maxFiles: Math.min(8 - attached.length, 64 - state.attachments.files.length),
+ }
+ : null,
+ fileCommand: state.attachments.paste?.command ?? null,
+ running: !!state.run,
+ enabled:
+ !!state.selected &&
+ !state.selected.archived &&
+ canExecute(state) &&
+ !state.loading &&
+ state.notice !== 'unavailable',
+ command: state.command,
+ autoFocus: !state.searchOpen && state.browse === null,
+ hasAttachments: attached.length > 0,
+ },
+ {
+ send: 'send',
+ stop: 'stop',
+ attachments: 'attachments',
+ changed: 'draft',
+ files: 'files',
+ checkpoint: 'checkpoint',
+ draftCheck: 'draft-check',
+ },
+ );
+ const queueProps: RowsProps = {
+ terminal: theme.terminal,
+ entry: state.queue?.entries.at(-1)?.messageId ?? null,
+ revision: state.queue?.revision ?? 0,
+ rows: (state.queue?.entries ?? []).map((entry): RowsProps['rows'][number] => {
+ const lane = state.queue!.entries.filter((row) => row.kind === entry.kind);
+ const index = lane.findIndex((row) => row.messageId === entry.messageId);
+ const enabled = !busy && !executionPending(state);
+ const up = {
+ key: 'up',
+ label: theme.ascii ? '^' : '↑',
+ enabled: enabled && index > 0,
+ };
+ const down = {
+ key: 'down',
+ label: theme.ascii ? 'v' : '↓',
+ enabled: enabled && index + 1 < lane.length,
+ };
+ const edit = { key: 'edit', label: theme.ascii ? 'e' : '✎', enabled };
+ const retract = {
+ key: 'retract',
+ label: theme.ascii ? 'x' : '×',
+ enabled,
+ };
+ return {
+ key: entry.messageId,
+ text: ` ${entry.kind === 'steering' ? (theme.ascii ? '^' : '↗') : entry.kind === 'in_flight' ? (theme.ascii ? '...' : '…') : theme.ascii ? '+' : '↳'} ${entry.preview}`,
+ actions:
+ entry.kind === 'in_flight'
+ ? []
+ : entry.kind === 'followup'
+ ? [
+ {
+ key: 'promote',
+ label: theme.ascii ? '^' : '↗',
+ enabled: enabled && !!state.run,
+ },
+ up,
+ down,
+ edit,
+ retract,
+ ]
+ : [up, down, edit, retract],
+ };
+ }),
+ shortcuts: [
+ { key: 'enter', modifiers: [], input: 'edit', enabled: true },
+ { key: 'e', modifiers: [], input: 'edit', enabled: true },
+ { key: 's', modifiers: [], input: 'promote', enabled: true },
+ { key: 'delete', modifiers: [], input: 'retract', enabled: true },
+ { key: 'x', modifiers: [], input: 'retract', enabled: true },
+ { key: 'up', modifiers: ['alt'], input: 'up', enabled: true },
+ { key: 'down', modifiers: ['alt'], input: 'down', enabled: true },
+ ],
+ maxRows: 3,
+ compactBelow: { height: 12, maxRows: 1 },
+ tabIndex: 2,
+ foreground: theme.text,
+ muted: theme.muted,
+ selected: theme.selected,
+ hovered: theme.surface,
+ scrollbar: {
+ thumb: theme.ascii ? '#' : '┃',
+ track: theme.ascii ? '|' : '│',
+ thumbColor: theme.text,
+ trackColor: theme.border,
+ },
+ };
+ const status = state.notice
+ ? text[state.notice]
+ : state.run
+ ? state.run.state === 'waiting_for_user'
+ ? text.waiting
+ : text.working
+ : state.selected?.archived
+ ? text.archivedConversation
+ : '';
+ const title = ui.text(
+ 'title',
+ (currentPage && state.pageChrome[currentPage]?.title) ||
+ (state.pages.at(-1) === 'scheduler'
+ ? text.scheduleTitle
+ : state.pages.at(-1) === 'recall'
+ ? text.recallTitle
+ : state.pages.at(-1) === 'apps'
+ ? text.appsTitle
+ : state.pages.at(-1) === 'imports'
+ ? text.importsTitle
+ : state.pages.at(-1) === 'projects'
+ ? text.projectsTitle
+ : state.pages.at(-1) === 'settings'
+ ? tuiText(locale, 'route-settings')
+ : state.pages.at(-1) === 'connections'
+ ? text.settings
+ : state.pages.at(-1) === 'deployment'
+ ? text.deploymentTitle
+ : (state.selected?.title ?? tuiText(locale, 'route-agent-screen'))),
+ 17,
+ theme.text,
+ 650,
+ {
+ align: target === 'tui' ? 'start' : 'center',
+ width: pixels(0),
+ grow: 1,
+ minWidth: 0,
+ truncate: true,
+ },
+ );
+ const models = presentation.slot(
+ 'models',
+ 'models',
+ {
+ theme,
+ locale,
+ selection: state.model,
+ executor: state.executor,
+ catalogEpoch: state.catalogEpoch,
+ openRequest: state.modelOpenRequest,
+ busy: busy || executionPending(state) || (target === 'tui' && !!state.run && !state.queue),
+ },
+ {
+ selected: 'model',
+ setup: 'model-setup',
+ connections: 'model-connections',
+ executor: 'executor',
+ backends: 'backends',
+ },
+ );
+
+ const header = headerView(presentation, state, theme, locale, title, models, status);
+ const welcome =
+ !!state.selected &&
+ !state.selected.archived &&
+ !state.loading &&
+ !!state.window &&
+ !state.window.page.chunks.length &&
+ !state.live.length &&
+ !state.run &&
+ !state.source &&
+ !state.image &&
+ !state.searchOpen;
+ const main = ui.box(
+ 'main',
+ [
+ ...(target === 'desktop' ? [header] : []),
+ ...(target === 'tui'
+ ? [
+ ui.box(
+ 'home',
+ [
+ ui.box('home-top', [], { terminal: { grow: 1 } }),
+ ui.box(
+ 'home-center',
+ [
+ ui.box('home-left', [], { terminal: { grow: 1 } }),
+ ui.box(
+ 'home-content',
+ [
+ ui.text('home-title', 'Maka', 24, theme.text, 650, {
+ align: 'center',
+ }),
+ ...(state.place
+ ? [
+ ui.text('home-place', state.place, 14, theme.subtle, 400, {
+ align: 'center',
+ truncate: true,
+ }),
+ ]
+ : []),
+ ui.box('home-gap', [], { terminal: { basis: 1 } }),
+ ...(!canExecute(state)
+ ? [
+ ui.text(
+ 'home-setup',
+ state.hasBackends
+ ? text.executionBackend
+ : tuiText(
+ locale,
+ state.modelSetup === 'loading'
+ ? 'connections-loading'
+ : state.modelSetup === 'unavailable'
+ ? 'home-setup-failed'
+ : state.modelSetup === 'connect'
+ ? 'home-needs-connection'
+ : 'home-needs-default',
+ ),
+ 14,
+ state.modelSetup === 'unavailable' ? theme.warning : theme.subtle,
+ 400,
+ { align: 'center' },
+ ),
+ ]
+ : []),
+ ui.button(
+ 'home-new',
+ canExecute(state)
+ ? '+ ' + tuiText(locale, 'sidebar-new-session') + ' Ctrl+N'
+ : state.hasBackends
+ ? text.executionBackend
+ : tuiText(
+ locale,
+ state.modelSetup === 'connect'
+ ? 'route-connections'
+ : 'default-model-title',
+ ) + ' Ctrl+N',
+ catalogBusy ||
+ (!canExecute(state) &&
+ !state.hasBackends &&
+ state.modelSetup === 'loading'),
+ true,
+ { background: 0 },
+ ),
+ ui.button('home-projects', text.projectsTitle, false, false, {
+ background: 0,
+ }),
+ ui.button('home-imports', text.importsTitle, false, false, {
+ background: 0,
+ }),
+ ],
+ { gap: 0, terminal: { basis: 52, shrink: true } },
+ ),
+ ui.box('home-right', [], { terminal: { grow: 1 } }),
+ ],
+ { row: true, gap: 0 },
+ ),
+ ui.box('home-bottom', [], { terminal: { grow: 1 } }),
+ ],
+ {
+ gap: 0,
+ visible: !state.selected && !state.embedding.fixed,
+ viewport: true,
+ terminal: { grow: 1 },
+ },
+ ),
+ ]
+ : []),
+ ...(state.selected?.archived && attached.length && !state.source && !state.image
+ ? [ui.box('archived-attachments', [attachmentsButton], { row: true })]
+ : []),
+ ...(target === 'desktop' && state.attachments.paste
+ ? [pasteView(presentation, theme, locale, state.attachments.paste, busy)]
+ : []),
+ ...(target === 'desktop'
+ ? [
+ ui.box(
+ 'conversation',
+ [
+ ui.box('retained-timeline', [timeline], {
+ visible: !state.image,
+ style: { width: fill, height: fill },
+ terminal: { grow: 1 },
+ }),
+ imageViewer,
+ ],
+ {
+ style: {
+ width: fill,
+ height: pixels(0),
+ grow: 1,
+ shrink: 1,
+ minHeight: 0,
+ },
+ },
+ ),
+ ui.box(
+ 'input-area',
+ [...(!state.selected?.archived ? [attachmentsButton] : []), composer],
+ {
+ visible: !state.image && !state.selected?.archived,
+ padding: 24,
+ style: { width: fill, maxWidth: 808, shrink: 0 },
+ },
+ ),
+ ]
+ : [
+ ui.box(
+ 'session-body',
+ [
+ ui.box(
+ 'conversation',
+ [
+ ui.box(
+ 'welcome-mark',
+ [
+ presentation.slot(
+ 'welcome',
+ 'welcome',
+ {
+ background: theme.terminal?.background ?? theme.background >>> 8,
+ foreground: theme.terminal?.foreground ?? theme.text >>> 8,
+ motion: theme.motion,
+ ascii: theme.ascii,
+ },
+ {},
+ ),
+ ],
+ { visible: welcome, terminal: { grow: 1 } },
+ ),
+ ui.box('retained-timeline', [timeline], {
+ visible: !state.image && !welcome,
+ style: { width: fill, height: fill },
+ terminal: { grow: 1 },
+ }),
+ imageViewer,
+ ],
+ { terminal: { basis: 1, grow: 1 } },
+ ),
+ ...(state.attachments.paste
+ ? [pasteView(presentation, theme, locale, state.attachments.paste, busy)]
+ : []),
+ ui.box(
+ 'retained-composer',
+ [
+ ...(reviewTarget(state)
+ ? [
+ ui.button(
+ 'interaction-open',
+ `Ctrl G · ${text.reviewRequest}`,
+ !rootCommands(state, locale, target).some(
+ (command) => command.kind === 'interaction-open' && !command.disabled,
+ ),
+ ),
+ ]
+ : []),
+ presentation.slot('queue', 'rows', queueProps, {
+ activated: 'queue-action',
+ }),
+ ...(queueProps.rows.length && ui.focusButton
+ ? [
+ ui.box(
+ 'queue-status',
+ [
+ ui.box('queue-status-space', [], {
+ terminal: { basis: 0, grow: 1 },
+ }),
+ ui.focusButton(
+ 'queue-focus',
+ `${theme.ascii ? '=' : '≡'} ${queueProps.rows.length}`,
+ 'queue',
+ ),
+ ],
+ { row: true, gap: 0 },
+ ),
+ ]
+ : []),
+ ...(attached.length
+ ? [
+ attachmentSummary(
+ presentation,
+ theme,
+ locale,
+ attached,
+ attachmentsDisabled,
+ ),
+ ]
+ : []),
+ ui.box(
+ 'composer-navigation',
+ [
+ composer,
+ ui.box(
+ 'composer-footer',
+ [
+ presentation.slot(
+ 'footer',
+ 'footer',
+ {
+ sessionId: state.selected?.id ?? null,
+ dataSource: state.selected?.dataSource ?? null,
+ running: !!state.run,
+ theme,
+ locale,
+ },
+ {},
+ ),
+ ],
+ { terminal: { basis: 1, shrink: false }, scroll: 'none' },
+ ),
+ ],
+ {
+ gap: 0,
+ terminal: { shrink: true },
+ focusShortcuts: [
+ {
+ key: 'up',
+ modifiers: [],
+ child: 'queue',
+ enabled: queueProps.rows.length > 0,
+ },
+ ],
+ },
+ ),
+ ],
+ {
+ gap: 0,
+ visible: !state.source && !state.image && !state.selected?.archived,
+ terminal: { shrink: true },
+ },
+ ),
+ ],
+ {
+ gap: 0,
+ visible: !!state.selected,
+ viewport: true,
+ terminal: { grow: 1 },
+ },
+ ),
+ ui.box(
+ 'session-metadata',
+ [
+ ui.box('model-metadata', [models], {
+ terminal: { grow: 1, shrink: true },
+ }),
+ ...(status
+ ? [
+ ui.text(
+ 'status',
+ status,
+ 12,
+ state.run?.state === 'waiting_for_user' ? theme.warning : theme.muted,
+ ),
+ ]
+ : []),
+ ],
+ { row: true, gap: 1 },
+ ),
+ ]),
+ ],
+ {
+ visible: conversationRoute(state),
+ gap: target === 'tui' ? 0 : undefined,
+ terminal: { grow: 1 },
+ style: {
+ width: target === 'desktop' ? pixels(0) : fill,
+ grow: 1,
+ height: fill,
+ align: 'center',
+ minWidth: 0,
+ },
+ },
+ );
+ const pageContent = pagesView(presentation, state, theme, locale, values, main);
+ const conversation = ui.box(
+ 'agent-screen',
+ state.embedding.fixed
+ ? [pageContent]
+ : target === 'desktop'
+ ? [
+ ui.box('sidebar', [navigation], {
+ visible: 'open' in state.sidebar && state.sidebar.open,
+ style: { width: pixels(240), height: fill, shrink: 0 },
+ }),
+ pageContent,
+ ]
+ : [
+ ui.adaptivePane!(
+ 'navigation-pane',
+ text.conversations,
+ ui.box(
+ 'page',
+ [
+ ui.box('page-inset-start', [], { terminal: { basis: 1 } }),
+ pageContent,
+ ui.box('page-inset-end', [], { terminal: { basis: 1 } }),
+ ],
+ { row: true, gap: 0, terminal: { grow: 1 } },
+ ),
+ ui.box('navigation-content', [navigation], {
+ gap: 0,
+ terminal: { grow: 1 },
+ }),
+ 'command' in state.sidebar ? state.sidebar.command : null,
+ ),
+ ],
+ {
+ row: target === 'desktop',
+ gap: 0,
+ terminal: { grow: 1 },
+ style: { width: fill, height: fill },
+ },
+ );
+ const background = ui.box(
+ 'product',
+ [
+ ...(target === 'tui' ? [header] : []),
+ ...(!state.embedding.fixed && state.scheduleAlerts.reminders.length
+ ? [
+ ui.button(
+ 'schedule-reminder',
+ `${text.scheduleReminder}: ${compactLabel(state.scheduleAlerts.reminders[0]!.title, 100)}`,
+ !!state.dialog,
+ false,
+ { background: theme.surface, align: 'start', width: fill },
+ ),
+ ]
+ : []),
+ conversation,
+ ...(target === 'tui' && (!state.embedding.fixed || state.embedding.session)
+ ? [
+ bookmarkView(
+ presentation,
+ state.bookmark,
+ navigationKey(state),
+ navigationKey(state),
+ navigationBookmark(state),
+ ),
+ ...(state.bookmark.observed?.state.kind === 'failed'
+ ? [ui.button('navigation-checkpoint-retry', copy(locale).sourceRetry)]
+ : []),
+ ]
+ : []),
+ ],
+ {
+ gap: 0,
+ background: theme.background,
+ style: { width: fill, height: fill, align: 'center' },
+ terminal: { grow: 1 },
+ shortcuts: [
+ ...(state.pages.at(-1) === 'settings' && state.dialog === null && !state.preferences.open
+ ? ([{ key: 'escape', modifiers: [], input: 'page-back', enabled: true }] as Shortcut[])
+ : []),
+ {
+ key: 'f',
+ modifiers: ['primary'],
+ input: 'search-open',
+ enabled: rootCommands(state, locale, target).some(
+ (command) => command.kind === 'search-open' && !command.disabled,
+ ),
+ },
+ { key: 'f1', modifiers: [], input: 'help-open', enabled: true },
+ {
+ key: 'g',
+ modifiers: ['primary'],
+ input: 'interaction-open',
+ enabled: rootCommands(state, locale, target).some(
+ (command) => command.kind === 'interaction-open' && !command.disabled,
+ ),
+ },
+ {
+ key: 'k',
+ modifiers: ['primary'],
+ input: 'palette-open',
+ enabled: true,
+ },
+ {
+ key: 'b',
+ modifiers: ['primary'],
+ input: 'navigation-toggle',
+ enabled: !state.embedding.fixed,
+ },
+ {
+ key: 'n',
+ modifiers: ['primary'],
+ input: 'create',
+ enabled: !state.embedding.fixed && !mutationPending(state) && !catalogPending(state),
+ },
+ ],
+ },
+ );
+ const out = output(
+ state,
+ ui.dialog(
+ 'agent-screen-dialog',
+ state.dialog === 'attachments'
+ ? target === 'tui' && state.attachments.selection?.browse
+ ? tuiText(locale, 'attachments-local-files')
+ : text.attachmentTitle
+ : state.dialog === 'skills'
+ ? text.skillsTitle
+ : state.dialog === 'terminal'
+ ? text.terminalTitle
+ : state.dialog === 'question'
+ ? state.interaction?.record?.request.kind === 'form'
+ ? text.formTitle
+ : text.questionsTitle
+ : state.dialog === 'approval'
+ ? state.interaction?.record?.request.kind === 'permissions'
+ ? text.permissionApprovalTitle
+ : text.approvalTitle
+ : state.dialog === 'directories'
+ ? text.directoryTitle
+ : state.dialog === 'details'
+ ? text.detailsTitle
+ : state.dialog === 'queue'
+ ? text.queueEdit
+ : state.dialog === 'help'
+ ? text.helpTitle
+ : text.paletteTitle,
+ background,
+ ui.box(
+ 'dialog-content',
+ [
+ ...(state.dialog === 'attachments' && state.attachments.selection
+ ? [
+ attachmentView(
+ presentation,
+ theme,
+ locale,
+ state.attachments,
+ busy,
+ state.selected?.id === state.attachments.selection.sessionId &&
+ sameValue(state.selected.dataSource, state.attachments.selection.dataSource) &&
+ !state.selected.archived,
+ state.notice
+ ? text[state.notice]
+ : state.selected?.archived
+ ? text.archivedConversation
+ : null,
+ !!state.uncertain,
+ Object.values(state.pending).some((request) => request.kind === 'receipt'),
+ ),
+ ]
+ : []),
+ ...(state.interaction?.record?.request.kind === 'service_approval' ||
+ state.interaction?.record?.request.kind === 'permissions'
+ ? [
+ presentation.slot(
+ `${state.interaction.record.request.kind === 'permissions' ? 'permission' : 'service'}-approval-${state.interaction.attempt}`,
+ state.interaction.record.request.kind === 'permissions'
+ ? 'permissionApproval'
+ : 'serviceApproval',
+ {
+ theme,
+ locale,
+ open: state.dialog === 'approval',
+ record: state.interaction.record,
+ busy:
+ mutationPending(state) ||
+ state.selected?.id !== state.interaction.sessionId ||
+ !!state.selected?.archived,
+ checking: Object.values(state.pending).some(
+ (r) =>
+ r.kind === 'interaction' || r.kind === 'receipt' || r.kind === 'respond',
+ ),
+ status: state.uncertain
+ ? text.interactionUnknown
+ : state.interaction.notice
+ ? text[state.interaction.notice]
+ : state.selected?.id !== state.interaction.sessionId
+ ? text.interactionChanged
+ : '',
+ } satisfies ServiceApprovalProps,
+ { closed: 'closed', submit: 'submit', check: 'check' },
+ ),
+ ]
+ : state.interaction?.record
+ ? [
+ presentation.slot(
+ questionChild(state),
+ state.interaction.record.request.kind === 'form'
+ ? 'formEditor'
+ : 'questionEditor',
+ {
+ theme,
+ locale,
+ open: state.dialog === 'question',
+ record: state.interaction.record,
+ busy:
+ mutationPending(state) ||
+ state.selected?.id !== state.interaction.sessionId ||
+ !!state.selected?.archived,
+ checking: Object.values(state.pending).some(
+ (r) =>
+ r.kind === 'interaction' || r.kind === 'receipt' || r.kind === 'respond',
+ ),
+ status: state.uncertain
+ ? text.interactionUnknown
+ : state.interaction.notice
+ ? text[state.interaction.notice]
+ : state.selected?.id !== state.interaction.sessionId
+ ? text.interactionChanged
+ : '',
+ } satisfies QuestionEditorProps,
+ { closed: 'closed', submit: 'submit', check: 'check' },
+ ),
+ ]
+ : [
+ ui.box(
+ 'question-loading',
+ [
+ ui.button('question-close', text.closeModels),
+ ui.text(
+ 'question-loading-text',
+ state.interaction?.notice
+ ? text[state.interaction.notice]
+ : text.questionLoading,
+ ),
+ ui.button(
+ 'question-check',
+ text.check,
+ Object.values(state.pending).some((r) => r.kind === 'interaction'),
+ ),
+ ],
+ {
+ visible: state.dialog === 'question' || state.dialog === 'approval',
+ terminal: { grow: 1 },
+ },
+ ),
+ ]),
+ presentation.slot(
+ 'queue-editor',
+ 'queueEditor',
+ {
+ theme,
+ locale,
+ open: state.dialog === 'queue',
+ ready: state.queueEdit?.phase === 'ready',
+ hasAttachments: state.queueEdit?.hasAttachments ?? false,
+ busy: mutationPending(state),
+ uncertain: !!state.uncertain,
+ checking: Object.values(state.pending).some((request) => request.kind === 'receipt'),
+ command: state.queueEdit?.command ?? null,
+ status: state.notice
+ ? text[state.notice]
+ : state.queueEdit?.phase === 'loading'
+ ? text.queueLoading
+ : '',
+ } satisfies QueueEditorProps,
+ {
+ changed: 'changed',
+ save: 'save',
+ closed: 'closed',
+ reload: 'reload',
+ check: 'check',
+ },
+ ),
+ ...(state.skills
+ ? [
+ presentation.slot(
+ 'skill-library',
+ 'skills',
+ {
+ theme,
+ locale,
+ open: state.dialog === 'skills',
+ workspace: state.selected?.workspace ?? null,
+ },
+ { closed: 'closed' },
+ ),
+ ]
+ : []),
+ presentation.slot(
+ 'help',
+ 'help',
+ { theme, locale, open: state.dialog === 'help' },
+ { closed: 'closed' },
+ ),
+ presentation.slot(
+ 'terminal-panel',
+ 'terminals',
+ {
+ theme,
+ locale,
+ open: state.dialog === 'terminal',
+ enabled: state.terminals,
+ busy: mutationPending(state),
+ session: state.selected,
+ },
+ { closed: 'closed' },
+ ),
+ presentation.slot(
+ 'session-details',
+ 'details',
+ {
+ theme,
+ locale,
+ open: state.dialog === 'details',
+ session: state.selected,
+ busy: mutationPending(state),
+ },
+ { changed: 'changed', closed: 'closed' },
+ ),
+ presentation.slot(
+ 'directory-picker',
+ 'directories',
+ {
+ theme,
+ locale,
+ open: state.dialog === 'directories',
+ busy: mutationPending(state) || catalogPending(state),
+ },
+ { selected: 'selected', closed: 'closed' },
+ ),
+ presentation.slot(
+ 'command-palette',
+ 'palette',
+ {
+ theme,
+ locale,
+ open: state.dialog === 'palette',
+ busy: false,
+ selectionDisabled:
+ !!state.embedding.fixed || mutationPending(state) || catalogPending(state),
+ commands: rootCommands(state, locale, target).map(
+ ({ kind: _kind, ...command }) => command,
+ ),
+ },
+ { invoked: 'invoked', selected: 'selected', closed: 'closed' },
+ ),
+ ],
+ {
+ gap: 0,
+ terminal: { grow: state.dialog === 'queue' ? 0 : 1 },
+ style: { width: fill, minWidth: 0 },
+ },
+ ),
+ state.dialog !== null,
+ state.dialog === 'terminal' ? { width: pixels(900) } : {},
+ state.dialog === 'attachments'
+ ? {
+ width: 64,
+ height: state.attachments.selection?.browse
+ ? 20
+ : Math.min(20, attached.length * 3 + 7),
+ }
+ : {},
+ ),
+ );
+ if (target === 'tui')
+ out.frame.view = appearanceView(
+ presentation,
+ state.preferences,
+ theme,
+ locale,
+ out.frame.view,
+ resolveLocale('auto', values.locale?.[0]),
+ );
+ if (!state.embedding.embedded)
+ out.frame.surface =
+ target === 'desktop'
+ ? { kind: 'window', binding: 'surface', title: 'maka', width: 1100, height: 760 }
+ : { kind: 'terminal', binding: 'surface' };
+ if (!state.embedding.embedded)
+ out.frame.view = presentation.paint(theme).box(
+ 'agent-screen-with-authoring',
+ [
+ out.frame.view,
+ presentation
+ .paint(theme)
+ .box('authoring-background', [presentation.slot('authoring', 'authoring', {}, {})], {
+ visible: false,
+ }),
+ ],
+ { terminal: { grow: 1 }, style: { width: fill, height: fill } },
+ );
+ out.actions = actions;
+ out.events = events;
+ out.subscriptions = [
+ ...(state.scheduleWatch && !state.embedding.fixed
+ ? [
+ {
+ key: state.scheduleWatch,
+ binding: 'agent',
+ member: 0,
+ method: 'scheduleAlerts',
+ input: {},
+ resources: {},
+ },
+ ]
+ : []),
+ ...(state.catalogWatch
+ ? [
+ {
+ key: state.catalogWatch.key,
+ binding: 'agent',
+ member: 0,
+ method: 'catalog',
+ input: state.catalogWatch.input,
+ resources: {},
+ },
+ ]
+ : []),
+ ...(state.watch
+ ? [
+ {
+ key: state.watch.key,
+ binding: 'agent',
+ member: 0,
+ method: 'watch',
+ input: state.watch.input,
+ resources: {},
+ },
+ ]
+ : []),
+ ];
+ return out;
+}
diff --git a/plugins/agent/agent-screen/header-view.ts b/plugins/agent/agent-screen/header-view.ts
new file mode 100644
index 0000000000..726dcff9b2
--- /dev/null
+++ b/plugins/agent/agent-screen/header-view.ts
@@ -0,0 +1,250 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import type { Theme } from '../../shared/contracts.js';
+import { copy } from '../../shared/locale.js';
+import { fill, pixels, type Presentation, type Node } from '../../shared/view.js';
+
+import { type State } from './state.js';
+import {
+ canExecute,
+ mutationPending,
+ conversationRoute,
+ rootCommands,
+ sidebarLabel,
+} from './actions.js';
+
+export function headerView<N extends Node>(
+ presentation: Presentation<N>,
+ state: State,
+ theme: Theme,
+ locale: string,
+ title: N,
+ models: N,
+ status: string | null,
+): N {
+ const target = presentation.target;
+ const ui = presentation.paint(theme),
+ text = copy(locale),
+ busy = mutationPending(state);
+ const currentPage = state.pages.at(-1);
+ const header = ui.box(
+ 'header',
+ [
+ ...(target === 'desktop'
+ ? [
+ ui.box(
+ 'context',
+ [
+ title,
+ ...(!state.embedding.fixed
+ ? [ui.button('navigation-toggle', sidebarLabel(state, text))]
+ : []),
+ ],
+ {
+ row: true,
+ gap: 8,
+ style: { width: fill, align: 'center' },
+ },
+ ),
+ ui.box(
+ 'toolbar',
+ [
+ ui.box('model-control', [models], {
+ style: { width: pixels(0), grow: 1, minWidth: 0 },
+ }),
+ ui.button('palette-open', text.paletteTitle),
+ ui.button(
+ 'details-open',
+ text.detailsOpen,
+ !rootCommands(state, locale, target).some(
+ (command) => command.kind === 'details-open' && !command.disabled,
+ ),
+ ),
+ ui.button('terminal-open', text.terminalTitle, !state.selected || state.loading),
+ ...(!state.embedding.fixed
+ ? [
+ ui.button('projects-open', text.projectsTitle),
+ ...(state.embedding.workhub ? [ui.button('workhub-open', 'WorkHub')] : []),
+ ui.button('apps-open', text.appsTitle),
+ ui.button('imports-open', text.importsTitle),
+ ...(state.scheduledTasks
+ ? [ui.button('scheduler-open', text.scheduleTitle)]
+ : []),
+ ]
+ : []),
+ ui.button('settings', text.settings),
+ ui.button('deployment-open', text.deploymentTitle),
+ ],
+ { row: true, gap: 8, style: { width: fill, align: 'center' } },
+ ),
+ ]
+ : [
+ ui.box(
+ 'header-left',
+ [
+ ...(!state.embedding.fixed
+ ? [
+ ui.box(
+ 'navigation-cell',
+ [
+ ui.button(
+ 'navigation-toggle',
+ theme.ascii ? '=' : '≡',
+ false,
+ false,
+ { background: 0 },
+ undefined,
+ text.toggleSidebar + ' · Ctrl+B',
+ ),
+ ],
+ { terminal: { basis: 3 } },
+ ),
+ ]
+ : []),
+ ...(currentPage
+ ? [
+ ui.box(
+ 'back-cell',
+ [
+ ui.button(
+ 'page-back',
+ theme.ascii ? '<' : '‹',
+ currentPage !== 'settings' &&
+ state.pageChrome[currentPage]?.backEnabled !== true,
+ false,
+ { background: 0 },
+ undefined,
+ text.back,
+ ),
+ ],
+ { terminal: { basis: 3 } },
+ ),
+ ]
+ : []),
+ ],
+ {
+ visible: !state.embedding.fixed || !conversationRoute(state),
+ row: true,
+ gap: 0,
+ terminal: { basis: (state.embedding.fixed ? 0 : 3) + (currentPage ? 3 : 0) },
+ },
+ ),
+ ui.box(
+ 'header-task',
+ [
+ ...(state.selected && conversationRoute(state)
+ ? [
+ state.run?.state === 'waiting_for_user'
+ ? ui.text('task-face', '-_-', 14, theme.warning)
+ : ui.activity('task-face', state.run ? 'spring' : 'familiar', theme.accent),
+ ]
+ : []),
+ ui.box('header-title', [title], { terminal: { shrink: true } }),
+ ui.box('header-title-trailing', [], { terminal: { grow: 1 } }),
+ ],
+ { row: true, gap: 1, terminal: { grow: 1, shrink: true } },
+ ),
+ ui.box(
+ 'toolbar',
+ [
+ ...(['details-open', 'terminal-open', 'palette-open'] as const).map((key, at) =>
+ ui.box(
+ `${key}-cell`,
+ [
+ ui.button(
+ key,
+ [theme.ascii ? 'i' : 'ⓘ', '>_', theme.ascii ? ':' : '⌘'][at]!,
+ key !== 'palette-open' && (!conversationRoute(state) || !state.selected),
+ false,
+ { background: 0 },
+ undefined,
+ [text.detailsOpen, text.terminalTitle, text.paletteTitle + ' · Ctrl+K'][at],
+ ),
+ ],
+ { terminal: { basis: 3 } },
+ ),
+ ),
+ ],
+ { row: true, gap: 0, terminal: { basis: 9 } },
+ ),
+ ]),
+ ...((target === 'desktop' ? state.interactions?.entries : [])
+ ?.filter((entry) =>
+ ['question', 'form', 'service_approval', 'permissions'].includes(entry.kind),
+ )
+ .map((entry, index) =>
+ ui.button(
+ `${entry.kind === 'question' || entry.kind === 'form' ? 'question' : 'approval'}-open-${entry.requestId}`,
+ `${entry.kind === 'form' ? text.formTitle : entry.kind === 'question' ? text.questionsTitle : entry.kind === 'permissions' ? text.permissionApprovalTitle : text.approvalTitle} ${index + 1}`,
+ !state.selected || state.selected.archived || mutationPending(state),
+ ),
+ ) ?? []),
+ ...(target === 'desktop' && !canExecute(state)
+ ? [
+ ui.text(
+ 'model-setup-hint',
+ state.hasBackends
+ ? text.executionBackend
+ : state.modelSetup === 'loading'
+ ? text.loadingModels
+ : state.modelSetup === 'unavailable'
+ ? text.modelsUnavailable
+ : state.modelSetup === 'connect'
+ ? text.setupConnectionHint
+ : text.setupModelHint,
+ 13,
+ theme.muted,
+ ),
+ ...(state.modelSetup === 'connect'
+ ? [
+ ui.button(
+ 'model-setup',
+ state.hasBackends ? text.executionBackend : text.configureModels,
+ busy,
+ ),
+ ]
+ : []),
+ ]
+ : []),
+ ...(target === 'desktop' && status ? [ui.text('status', status, 13, theme.muted)] : []),
+ ...(state.uncertain
+ ? [
+ ui.button(
+ 'check',
+ text.check,
+ Object.values(state.pending).some((request) => request.kind === 'receipt'),
+ ),
+ ]
+ : []),
+ ...(state.notice === 'unavailable' ? [ui.button('reload', text.loading, state.loading)] : []),
+ ],
+ {
+ row: target === 'tui',
+ gap: target === 'desktop' ? 8 : 0,
+ style: {
+ width: fill,
+ shrink: 0,
+ padding: { top: 16, right: 24, bottom: 16, left: 24 },
+ },
+ },
+ );
+
+ return header;
+}
diff --git a/plugins/agent/agent-screen/index.ts b/plugins/agent/agent-screen/index.ts
new file mode 100644
index 0000000000..84ce2f1427
--- /dev/null
+++ b/plugins/agent/agent-screen/index.ts
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { initialAppearance } from '../../shared/appearance-editor.js';
+import type { Action, Component } from '../../../packages/plugin-sdk/src/index.js';
+import type { AgentScreenProps } from '../../shared/contracts.js';
+import { bookmark } from '../../shared/bookmark.js';
+import { type Presentation, type Node } from '../../shared/view.js';
+
+import { type State } from './state.js';
+import { intent, catalog } from './actions.js';
+import { frame } from './frame.js';
+import { handleEvent } from './events.js';
+
+export function agentScreen<N extends Node>(
+ presentation: Presentation<N>,
+ locale: string,
+ terminals = false,
+ place = '',
+ skills = false,
+ scheduledTasks = false,
+ web = false,
+ jev = false,
+): Component<State, AgentScreenProps, N> {
+ const target = presentation.target;
+ const configuredLocale = locale;
+ return {
+ mount(props, values) {
+ const state: State = {
+ embedding: props,
+ externalSelection: null,
+ place,
+ preferences: initialAppearance(),
+ bookmark: bookmark({ pages: [], category: 'appearance' }),
+ revision: 0,
+ attachments: { files: [], selection: null, paste: null },
+ drafts: [],
+ serial: 0,
+ epoch: 0,
+ catalog: null,
+ catalogGeneration: 0,
+ catalogWatch: null,
+ catalogFilter: 'all',
+ catalogFailed: false,
+ selected: null,
+ model: null,
+ executor: null,
+ hasBackends: false,
+ modelSetup: 'loading',
+ modelOpenRequest: null,
+ catalogEpoch: 0,
+ modelRefreshing: false,
+ pages: [],
+ openedPages: [],
+ pageChrome: {},
+ pageBack: {},
+ recallAnchor: null,
+ scheduledTasks,
+ web,
+ jev,
+ scheduleAlerts: { pending: 0, reminders: [] },
+ scheduleWatch: scheduledTasks ? 'schedule-alerts-initial' : null,
+ scheduleRequested: null,
+ settingsCategory: 'appearance',
+ insightsScope: null,
+ searchOpen: false,
+ browse: null,
+ sidebar: target === 'desktop' ? { open: true } : { command: null },
+ terminals,
+ skills,
+ dialog: null,
+ image: null,
+ window: null,
+ source: null,
+ navigation: 0,
+ landing: { sequence: 0, edge: 'end' },
+ tailReceipt: null,
+ fileContext: null,
+ copyRequest: null,
+ messageCopy: null,
+ followAttempt: null,
+ parts: [],
+ run: null,
+ queue: null,
+ queueEdit: null,
+ queueDraft: null,
+ interactions: null,
+ interaction: null,
+ questionSubmission: null,
+ runThrough: 0,
+ loading: false,
+ live: [],
+ latestKnown: 0,
+ watch: null,
+ pending: {},
+ uncertain: null,
+ restoring: true,
+ restoreAfter: 0,
+ command: null,
+ commandSequence: 0,
+ notice: null,
+ };
+ const actions: Action[] = [];
+ catalog(state);
+ intent(state, actions, { kind: 'restorePage' }, 'history', 'page', { after: 0, limit: 1 });
+ return frame(presentation, configuredLocale, state, values, actions);
+ },
+ update(state, props, values) {
+ if (state.embedding.fixed && state.embedding.session?.id !== props.session?.id) {
+ state.bookmark = bookmark({ pages: [], category: 'appearance' });
+ state.pages = [];
+ state.settingsCategory = 'appearance';
+ }
+ return frame(
+ presentation,
+ configuredLocale,
+ { ...state, embedding: props, revision: state.revision + 1 },
+ values,
+ );
+ },
+ event(previous, input, props, values) {
+ return handleEvent(presentation, configuredLocale, previous, input, props, values);
+ },
+ };
+}
diff --git a/plugins/agent/agent-screen/messages.ts b/plugins/agent/agent-screen/messages.ts
new file mode 100644
index 0000000000..b724967a31
--- /dev/null
+++ b/plugins/agent/agent-screen/messages.ts
@@ -0,0 +1,163 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import type { Message, TimelineProps } from '../../shared/contracts.js';
+
+import { type DisplayPart, type State } from './state.js';
+
+export function utf8Bytes(text: string): number {
+ let bytes = 0;
+ for (const character of text) {
+ const point = character.codePointAt(0)!;
+ bytes += point <= 0x7f ? 1 : point <= 0x7ff ? 2 : point <= 0xffff ? 3 : 4;
+ }
+ return bytes;
+}
+// Raw fences can advance while model text is still open. Only an actual step
+// closure covers its live prefix for both atomic and item-accepting providers.
+
+export function appendLive(
+ state: State,
+ sequence: number,
+ part: DisplayPart,
+ invocationId: string,
+ text: string,
+) {
+ const messageId = part.messageId;
+ const field = part.textKind === 'thinking' ? 'thinking' : 'body';
+ if (
+ state.window &&
+ sequence <= state.window.page.through &&
+ state.window.page.chunks.some(
+ (chunk) => chunk.messageId === messageId && (chunk.field ?? 'body') === field,
+ )
+ )
+ return;
+ const ids = new Set(state.live.map((fragment) => fragment.messageId));
+ let remaining = 16000 - state.live.reduce((total, fragment) => total + fragment.text.length, 0);
+ // Thinking shares the existing prefix budget but leaves room for an answer
+ // before atomic providers publish either field. Never evict a displayed prefix.
+ if (field === 'thinking') {
+ const thinking = state.live.filter((fragment) => fragment.field === 'thinking');
+ const thinkingIds = new Set(thinking.map((fragment) => fragment.messageId));
+ if (thinking.length >= 32 || (!thinkingIds.has(messageId) && thinkingIds.size >= 8)) return;
+ remaining = Math.min(
+ remaining,
+ 8000 - thinking.reduce((total, fragment) => total + fragment.text.length, 0),
+ );
+ }
+ // A truncated surrogate can leave one unused code unit; keep the prefix closed.
+ if (remaining <= 1 || state.live.length >= 64 || (!ids.has(messageId) && ids.size >= 16)) return;
+ text = text.slice(0, remaining);
+ const last = text.charCodeAt(text.length - 1);
+ if (last >= 0xd800 && last <= 0xdbff) text = text.slice(0, -1);
+ if (text)
+ state.live.push({
+ sequence,
+ messageId,
+ invocationId,
+ stepId: part.stepId,
+ text,
+ closedAt: null,
+ field,
+ });
+}
+
+export function closeLive(state: State, sequence: number, invocationId: string, stepId?: string) {
+ for (const fragment of state.live)
+ if (
+ fragment.closedAt === null &&
+ fragment.invocationId === invocationId &&
+ (stepId === undefined || fragment.stepId === stepId)
+ )
+ fragment.closedAt = sequence;
+}
+
+export function messages(state: State): Message[] {
+ const canonical: Message[] = (state.window?.page.chunks ?? []).map((chunk) => ({
+ id: chunk.field === 'thinking' ? 'thinking-' + chunk.messageId : chunk.messageId,
+ contentKey: `canonical-${chunk.sequence}-${chunk.offset}${chunk.field === 'thinking' ? '-thinking' : ''}`,
+ role: chunk.field === 'thinking' ? 'thinking' : chunk.role,
+ timestamp: chunk.timestamp,
+ text: chunk.text,
+ tool: chunk.tool,
+ continued: chunk.offset !== 0 || utf8Bytes(chunk.text) !== chunk.totalBytes,
+ source: chunk.sequence,
+ image: chunk.tool?.result?.image ?? false,
+ }));
+ if (!state.window?.latest) return canonical;
+ const live = new Map<string, Message>();
+ for (const fragment of state.live) {
+ if (
+ fragment.sequence <= state.window.page.through &&
+ state.window.page.chunks.some(
+ (chunk) =>
+ chunk.messageId === fragment.messageId && (chunk.field ?? 'body') === fragment.field,
+ )
+ )
+ continue;
+ const key = fragment.messageId + (fragment.field === 'thinking' ? '-thinking' : '');
+ let message = live.get(key);
+ if (!message) {
+ message = {
+ id: 'live-' + key,
+ contentKey: 'live-' + key,
+ role: fragment.field === 'thinking' ? 'thinking' : 'assistant',
+ timestamp: null,
+ text: '',
+ tool: null,
+ continued: true,
+ source: null,
+ image: false,
+ };
+ live.set(key, message);
+ }
+ message.text += fragment.text;
+ }
+ return [...canonical, ...live.values()];
+}
+
+export function turnTimings(state: State, displayed: Message[]): TimelineProps['timings'] {
+ const turns = new Map<string, TimelineProps['timings'][number]>();
+ for (const chunk of state.window?.page.chunks ?? []) {
+ if (!chunk.timing) continue;
+ if (!chunk.timing.finished && (!state.window?.latest || state.run?.turnId !== chunk.turnId))
+ continue;
+ turns.set(chunk.turnId, {
+ ...chunk.timing,
+ turnId: chunk.turnId,
+ afterMessageId: chunk.field === 'thinking' ? 'thinking-' + chunk.messageId : chunk.messageId,
+ });
+ }
+ if (state.window?.latest && state.run) {
+ const timing = turns.get(state.run.turnId);
+ if (timing && !timing.finished) {
+ for (const fragment of state.live) {
+ const id =
+ 'live-' + fragment.messageId + (fragment.field === 'thinking' ? '-thinking' : '');
+ if (
+ fragment.invocationId === state.run.invocationId &&
+ displayed.some((message) => message.id === id)
+ )
+ timing.afterMessageId = id;
+ }
+ }
+ }
+ return [...turns.values()];
+}
diff --git a/plugins/agent/agent-screen/navigation-view.ts b/plugins/agent/agent-screen/navigation-view.ts
new file mode 100644
index 0000000000..ef941d0fc1
--- /dev/null
+++ b/plugins/agent/agent-screen/navigation-view.ts
@@ -0,0 +1,280 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { tuiText } from '../../shared/tui-locale.js';
+import type { Theme } from '../../shared/contracts.js';
+import { copy } from '../../shared/locale.js';
+import { fill, pixels, type Presentation, type Node } from '../../shared/view.js';
+
+import { type State } from './state.js';
+import { catalogPending, mutationPending, rootCommands } from './actions.js';
+
+export function navigationView<N extends Node>(
+ presentation: Presentation<N>,
+ state: State,
+ theme: Theme,
+ locale: string,
+): N {
+ const target = presentation.target;
+ const ui = presentation.paint(theme),
+ text = copy(locale),
+ busy = mutationPending(state);
+ const catalogBusy = busy || catalogPending(state);
+ const commands = rootCommands(state, locale, target);
+ const createButton = ui.button(
+ 'create',
+ target === 'tui' ? '+ ' + tuiText(locale, 'sidebar-new-session') : text.new,
+ !commands.some((command) => command.kind === 'create' && !command.disabled),
+ target === 'tui',
+ target === 'tui' ? { background: 0, align: 'start' } : {},
+ );
+ const createEntry = ui.contextMenu
+ ? ui.contextMenu('navigation-create-menu', text.new, createButton, [
+ ui.button(
+ 'create-workspace',
+ text.newWorkspace,
+ !commands.some((command) => command.kind === 'create-workspace' && !command.disabled),
+ ),
+ ])
+ : createButton;
+ const navigationLink = (
+ key: string,
+ label: string,
+ glyph: string,
+ ascii: string,
+ current = false,
+ ) =>
+ ui.button(key, `${theme.ascii ? ascii : glyph} ${label}`, false, false, {
+ background: current ? theme.selected : 0,
+ align: 'start',
+ });
+ const navigationApps =
+ target === 'tui' && !state.embedding.fixed
+ ? [
+ ui.box('navigation-apps-gap', [], { terminal: { basis: 1 } }),
+ navigationLink('apps-open', text.appsTitle, '', '', state.pages.at(-1) === 'apps'),
+ ...(state.embedding.workhub ? [navigationLink('workhub-open', 'WorkHub', '◈', '*')] : []),
+ ...(state.scheduledTasks
+ ? [
+ navigationLink(
+ 'scheduler-open',
+ text.scheduleTitle +
+ (state.scheduleAlerts.pending ? ` (${state.scheduleAlerts.pending})` : ''),
+ '⏲',
+ 'T',
+ state.pages.at(-1) === 'scheduler',
+ ),
+ ]
+ : []),
+ navigationLink(
+ 'recall-open',
+ text.recallTitle,
+ '⌕',
+ '/',
+ state.pages.at(-1) === 'recall',
+ ),
+ navigationLink(
+ 'imports-open',
+ text.importsTitle,
+ '⇥',
+ '>',
+ state.pages.at(-1) === 'imports',
+ ),
+ ]
+ : [];
+ const navigation = ui.box(
+ 'navigation-panel',
+ [
+ target === 'tui'
+ ? ui.box(
+ 'navigation-heading',
+ [
+ ui.box('navigation-create-cell', [createEntry], { terminal: { grow: 1 } }),
+ ui.button(
+ 'navigation-back',
+ theme.ascii ? 'x' : '×',
+ false,
+ false,
+ { background: 0, align: 'start' },
+ undefined,
+ text.hideSidebar,
+ ),
+ ],
+ { row: true, gap: 0 },
+ )
+ : ui.text('navigation-brand', 'Maka', 24, theme.text, 650),
+ ...(target === 'desktop' ? [createEntry] : []),
+ ...(target === 'desktop'
+ ? [ui.text('navigation-heading', text.conversations, 12, theme.muted, 550)]
+ : []),
+ ui.box(
+ 'catalog-filters',
+ [
+ ...(['all', 'needs_you'] as const).map((filter) => {
+ const label =
+ filter === 'all'
+ ? target === 'tui'
+ ? tuiText(locale, 'sidebar-all')
+ : text.allSessions
+ : target === 'tui'
+ ? tuiText(locale, 'sidebar-needs-you') +
+ (state.catalog?.needsYouCount ? ` · ${state.catalog.needsYouCount}` : '')
+ : state.catalog
+ ? `${text.needsYou} (${state.catalog.needsYouCount})`
+ : text.needsYou;
+ const selected = state.catalogFilter === filter;
+ return ui.item
+ ? ui.item(
+ `catalog-filter-${filter}`,
+ ui.text(`catalog-filter-label-${filter}`, label, 12, theme.text),
+ selected,
+ catalogBusy,
+ )
+ : ui.button(
+ `catalog-filter-${filter}`,
+ label,
+ catalogBusy,
+ false,
+ selected ? { background: theme.selected } : {},
+ );
+ }),
+ ...(target === 'tui'
+ ? [
+ ui.button(
+ 'catalog-search',
+ `${theme.ascii ? '/' : '⌕'} ${tuiText(locale, 'sidebar-search')}`,
+ false,
+ false,
+ { background: 0, align: 'start' },
+ undefined,
+ tuiText(locale, 'sidebar-search-sessions'),
+ ),
+ ]
+ : []),
+ ],
+ { row: true, focusGroup: target === 'tui', gap: target === 'tui' ? 2 : 1 },
+ ),
+ ...(target === 'tui' ? [ui.box('catalog-gap', [], { terminal: { basis: 1 } })] : []),
+ ui.box(
+ 'catalog-body',
+ [
+ ...(!state.catalogFailed && !catalogPending(state) && state.catalog?.sessions.length === 0
+ ? [
+ ui.box(
+ 'catalog-empty-inset',
+ [
+ ui.text(
+ 'catalog-empty',
+ (target === 'tui' ? ' ' : '') +
+ (state.catalogFilter === 'needs_you'
+ ? text.noneNeedsYou
+ : tuiText(locale, 'sidebar-empty')),
+ 12,
+ theme.muted,
+ ),
+ ],
+ { padding: target === 'desktop' ? 1 : 0, terminal: { shrink: true } },
+ ),
+ ]
+ : []),
+ ...(state.catalogFailed
+ ? [ui.text('catalog-error', text.sessionsUnavailable, 12, theme.muted)]
+ : []),
+ ui.box(
+ 'catalog-controls',
+ [
+ ...(state.catalog?.before
+ ? [
+ ui.button(
+ 'catalog-previous',
+ text.previousSessions,
+ catalogBusy || state.catalogFailed,
+ ),
+ ]
+ : []),
+ ...(state.catalog?.after
+ ? [ui.button('catalog-more', text.moreSessions, catalogBusy || state.catalogFailed)]
+ : []),
+ ...(target === 'desktop' || state.catalogFailed
+ ? [ui.button('catalog-refresh', text.refreshSessions, catalogBusy)]
+ : []),
+ ],
+ { row: true, style: { wrap: true }, terminal: { wrap: true } },
+ ),
+ presentation.slot(
+ 'navigation',
+ 'navigation',
+ {
+ theme,
+ locale,
+ sessions: state.catalog?.sessions ?? [],
+ selected: state.selected?.id ?? null,
+ busy: catalogBusy,
+ },
+ { select: 'select' },
+ ),
+ ...navigationApps,
+ ],
+ {
+ gap: 0,
+ scroll: target === 'tui' ? 'manual' : 'none',
+ terminal: { grow: 1 },
+ style: { height: pixels(0), grow: 1, minHeight: 0 },
+ },
+ ),
+ ...(target === 'tui'
+ ? [
+ ui.box(
+ 'navigation-footer',
+ [
+ ...(!state.embedding.fixed
+ ? [
+ navigationLink(
+ 'projects-open',
+ text.projectsTitle,
+ '▦',
+ 'P',
+ state.pages.at(-1) === 'projects',
+ ),
+ ]
+ : []),
+ navigationLink(
+ 'settings',
+ tuiText(locale, 'route-settings'),
+ '⛭',
+ 'S',
+ state.pages.at(-1) === 'settings',
+ ),
+ ],
+ { gap: 0 },
+ ),
+ ]
+ : []),
+ ],
+ {
+ surface: target === 'desktop',
+ gap: target === 'tui' ? 0 : undefined,
+ padding: target === 'desktop' ? 20 : 1,
+ style: { height: fill, width: fill },
+ terminal: { grow: 1 },
+ },
+ );
+
+ return navigation;
+}
diff --git a/plugins/agent/workspace/package.ts b/plugins/agent/agent-screen/package.ts
similarity index 96%
rename from plugins/agent/workspace/package.ts
rename to plugins/agent/agent-screen/package.ts
index 5394a4ac7f..55a557d8d8 100644
--- a/plugins/agent/workspace/package.ts
+++ b/plugins/agent/agent-screen/package.ts
@@ -22,8 +22,8 @@ import * as own from '../../shared/contracts.js';
import { one, uiEntries } from '../../shared/package.js';
const entries = uiEntries({
- contract: own.workspaceContract,
- export: 'Workspace',
+ contract: own.agentScreenContract,
+ export: 'AgentScreen',
imports: {
agent: one(sdk.contracts.agent),
reading: one(sdk.contracts.readingArea),
@@ -98,7 +98,7 @@ for (const target of ['desktop', 'tui'] as const)
);
export default {
schemaVersion: 2,
- id: 'maka.agent.workspace',
- displayName: 'workspace',
+ id: 'maka.agent.agent-screen',
+ displayName: 'Agent screen',
application: { entries },
};
diff --git a/plugins/agent/agent-screen/pages-view.ts b/plugins/agent/agent-screen/pages-view.ts
new file mode 100644
index 0000000000..6676d701f2
--- /dev/null
+++ b/plugins/agent/agent-screen/pages-view.ts
@@ -0,0 +1,284 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { settingsView } from '../../shared/settings.js';
+import type { ComponentValues } from '../../../packages/plugin-sdk/src/index.js';
+import type { Theme } from '../../shared/contracts.js';
+import { bookmarkReady } from '../../shared/bookmark.js';
+import { copy } from '../../shared/locale.js';
+import { fill, type Presentation, type Node } from '../../shared/view.js';
+
+import { type Page, navigationBookmark, navigationKey, type State } from './state.js';
+import { catalogPending, mutationPending } from './actions.js';
+
+export function pagesView<N extends Node>(
+ presentation: Presentation<N>,
+ state: State,
+ theme: Theme,
+ locale: string,
+ values: ComponentValues,
+ main: N,
+): N {
+ const target = presentation.target;
+ const currentPage = state.pages.at(-1);
+ const ui = presentation.paint(theme),
+ text = copy(locale),
+ busy = mutationPending(state);
+ const pageShell = (page: Page) =>
+ target === 'tui' ? { shell: { back: state.pageBack[page] ?? 0 } } : {};
+ const pageContent = ui.box(
+ 'page-content',
+ [
+ main,
+ ...(state.openedPages.includes('settings')
+ ? [
+ ui.box(
+ 'retained-settings',
+ [
+ settingsView(
+ presentation,
+ state.preferences,
+ theme,
+ locale,
+ state.settingsCategory,
+ currentPage === 'settings',
+ state.insightsScope,
+ state.web,
+ state.jev,
+ state.embedding.workhub === true,
+ ),
+ ],
+ {
+ visible: currentPage === 'settings',
+ terminal: { grow: 1 },
+ style: { width: fill, height: fill },
+ },
+ ),
+ ]
+ : []),
+ ...(state.openedPages.includes('deployment')
+ ? [
+ ui.box(
+ 'retained-deployment',
+ [
+ presentation.slot(
+ 'deployment',
+ 'deployment',
+ {
+ theme,
+ locale,
+ ...pageShell('deployment'),
+ ...(target === 'tui'
+ ? {
+ bookmarkKey: navigationKey(state) + '/deployment',
+ navigationReady: bookmarkReady(state.bookmark, navigationBookmark(state)),
+ }
+ : {}),
+ },
+ { chrome: 'page-chrome', closed: 'deployment-closed' },
+ ),
+ ],
+ {
+ visible: currentPage === 'deployment',
+ gap: 0,
+ terminal: { grow: 1 },
+ style: { width: fill, height: fill },
+ },
+ ),
+ ]
+ : []),
+ ...(state.openedPages.includes('apps')
+ ? [
+ ui.box(
+ 'retained-apps',
+ [
+ presentation.slot(
+ 'apps',
+ 'apps',
+ {
+ theme,
+ locale,
+ ...pageShell('apps'),
+ open: state.pages.at(-1) === 'apps',
+ busy: mutationPending(state) || catalogPending(state),
+ },
+ { chrome: 'page-chrome', closed: 'apps-closed', manage: 'apps-manage' },
+ ),
+ ],
+ {
+ visible: currentPage === 'apps',
+ gap: 0,
+ terminal: { grow: 1 },
+ style: { width: fill, height: fill },
+ },
+ ),
+ ]
+ : []),
+ ...(state.openedPages.includes('scheduler')
+ ? [
+ ui.box(
+ 'retained-scheduler',
+ [
+ presentation.slot(
+ 'scheduler',
+ 'scheduler',
+ {
+ theme,
+ locale,
+ ...pageShell('scheduler'),
+ open: currentPage === 'scheduler',
+ busy: mutationPending(state) || catalogPending(state),
+ catalogEpoch: state.catalogEpoch,
+ requested: state.scheduleRequested,
+ },
+ {
+ chrome: 'page-chrome',
+ closed: 'scheduler-closed',
+ selected: 'scheduler-selected',
+ connections: 'scheduler-connections',
+ },
+ ),
+ ],
+ {
+ visible: currentPage === 'scheduler',
+ gap: 0,
+ terminal: { grow: 1 },
+ style: { width: fill, height: fill },
+ },
+ ),
+ ]
+ : []),
+ ...(state.openedPages.includes('recall')
+ ? [
+ ui.box(
+ 'retained-recall',
+ [
+ presentation.slot(
+ 'recall',
+ 'recall',
+ {
+ theme,
+ locale,
+ ...pageShell('recall'),
+ open: state.pages.at(-1) === 'recall',
+ busy: mutationPending(state) || catalogPending(state),
+ },
+ { chrome: 'page-chrome', closed: 'recall-closed', selected: 'recall-selected' },
+ ),
+ ],
+ {
+ visible: currentPage === 'recall',
+ gap: 0,
+ terminal: { grow: 1 },
+ style: { width: fill, height: fill },
+ },
+ ),
+ ]
+ : []),
+ ...(state.openedPages.includes('imports')
+ ? [
+ ui.box(
+ 'retained-imports',
+ [
+ presentation.slot(
+ 'imports',
+ 'imports',
+ {
+ theme,
+ locale,
+ ...pageShell('imports'),
+ open: state.pages.at(-1) === 'imports',
+ busy: mutationPending(state) || catalogPending(state),
+ },
+ {
+ chrome: 'page-chrome',
+ closed: 'imports-closed',
+ selected: 'import-selected',
+ changed: 'imports-changed',
+ },
+ ),
+ ],
+ {
+ visible: currentPage === 'imports',
+ gap: 0,
+ terminal: { grow: 1 },
+ style: { width: fill, height: fill },
+ },
+ ),
+ ]
+ : []),
+ ...(state.openedPages.includes('projects')
+ ? [
+ ui.box(
+ 'retained-projects',
+ [
+ presentation.slot(
+ 'projects',
+ 'projects',
+ {
+ theme,
+ locale,
+ ...pageShell('projects'),
+ open: state.pages.at(-1) === 'projects',
+ busy: mutationPending(state) || catalogPending(state),
+ },
+ {
+ chrome: 'page-chrome',
+ closed: 'projects-closed',
+ selected: 'project-selected',
+ changed: 'projects-changed',
+ },
+ ),
+ ],
+ {
+ visible: currentPage === 'projects',
+ gap: 0,
+ terminal: { grow: 1 },
+ style: { width: fill, height: fill },
+ },
+ ),
+ ]
+ : []),
+ ...(state.openedPages.includes('connections')
+ ? [
+ ui.box(
+ 'retained-connections',
+ [
+ presentation.slot(
+ 'connections',
+ 'connections',
+ { theme, locale, ...pageShell('connections') },
+ { chrome: 'page-chrome', closed: 'settings-closed', changed: 'settings-changed' },
+ ),
+ ],
+ {
+ visible: currentPage === 'connections',
+ gap: 0,
+ terminal: { grow: 1 },
+ style: { width: fill, height: fill },
+ },
+ ),
+ ]
+ : []),
+ ],
+ { gap: 0, terminal: { grow: 1 }, style: { width: fill, height: fill, minWidth: 0 } },
+ );
+
+ return pageContent;
+}
diff --git a/plugins/agent/agent-screen/results.ts b/plugins/agent/agent-screen/results.ts
new file mode 100644
index 0000000000..900e46b06f
--- /dev/null
+++ b/plugins/agent/agent-screen/results.ts
@@ -0,0 +1,825 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import type {
+ Action,
+ AgentDeliveryPage,
+ InteractionRecord,
+ AgentInteractionResult,
+ AgentQueueMessageResult,
+ AgentHistoryPage,
+ AgentHistorySourcePage,
+ AgentObservation,
+ AgentCreated,
+ AgentMarkReadResult,
+ AgentDirectoryPage,
+ AgentSendResult,
+ AgentAttachmentUploadResult,
+ AgentQueueMutationResult,
+ CallerIntent,
+ OperationReply,
+ TextChunk,
+} from '../../../packages/plugin-sdk/src/index.js';
+import { sameValue } from '../../shared/value.js';
+
+import { sameFile } from '../../shared/attachments.js';
+
+import { draftPage, draftReceipt, draftFailed } from '../../shared/drafts.js';
+
+import { type Request, type Mutation, type DisplayPart, type State } from './state.js';
+import { utf8Bytes, appendLive, closeLive } from './messages.js';
+import {
+ intent,
+ catalog,
+ observe,
+ history,
+ follow,
+ select,
+ initializeQueueEdit,
+ readInteraction,
+ respondQuestion,
+ closeDialog,
+} from './actions.js';
+
+function settled(state: State, actions: Action[], request: Mutation, value: unknown) {
+ state.uncertain = null;
+ if (request.kind === 'restored') {
+ state.restoring = true;
+ intent(state, actions, { kind: 'restorePage' }, 'history', 'page', {
+ after: state.restoreAfter,
+ limit: 1,
+ });
+ catalog(state);
+ } else if (request.kind === 'create') {
+ const created = value as AgentCreated;
+ if (created.kind === 'rejected') {
+ if (state.notice !== 'unavailable') state.notice = 'createRejected';
+ return;
+ }
+ // The live catalog may have selected this first conversation before its
+ // original creation receipt arrived. Keep that observation and its owners.
+ if (state.selected?.id !== created.session.id) select(state, actions, created.session);
+ else if (created.session.revision >= state.selected.revision) state.selected = created.session;
+ // Explicit creation reveals its selected row; background catalog changes
+ // continue to retain the current directory location.
+ if (
+ state.catalogFilter !== 'all' ||
+ !state.catalog?.sessions.some((session) => session.id === created.session.id)
+ ) {
+ state.catalogFilter = 'all';
+ catalog(state, { kind: 'at', cursor: created.session.id });
+ }
+ } else if (request.kind === 'markRead') {
+ const result = value as AgentMarkReadResult;
+ if (result.kind === 'observed' && result.session.id === request.sessionId) {
+ if (
+ state.selected?.id === request.sessionId &&
+ result.session.revision >= state.selected.revision
+ )
+ state.selected = result.session;
+ if (state.catalog)
+ state.catalog.sessions = state.catalog.sessions.map((session) =>
+ session.id === request.sessionId && result.session.revision >= session.revision
+ ? result.session
+ : session,
+ );
+ state.notice = null;
+ } else state.notice = 'unavailable';
+ } else if (request.kind === 'queue') {
+ const result = value as AgentQueueMutationResult;
+ state.notice = result.kind === 'committed' ? null : 'queueChanged';
+ if (
+ result.kind === 'committed' &&
+ request.saved &&
+ state.queueEdit?.attempt === request.saved.attempt &&
+ state.queueDraft?.snapshot.text.owner === request.saved.draft.snapshot.text.owner &&
+ state.queueDraft.revision === request.saved.draft.revision
+ )
+ closeDialog(state);
+ } else if (request.kind === 'respond') {
+ const result = value as AgentInteractionResult;
+ const question = state.interaction;
+ if (question?.attempt === request.attempt) {
+ if (
+ result.kind === 'recorded' &&
+ result.record.sessionId === request.sessionId &&
+ result.record.requestId === request.requestId &&
+ sameValue(result.record.request, question.record?.request)
+ ) {
+ question.record = result.record;
+ question.notice = null;
+ } else
+ question.notice =
+ result.kind === 'rejected' &&
+ result.reason === 'invalid_answer' &&
+ question.record?.request.kind === 'form'
+ ? 'formInvalid'
+ : 'interactionChanged';
+ }
+ if (state.selected?.id === request.sessionId) observe(state, actions);
+ } else if (request.kind === 'uploadAttachment') {
+ const result = value as AgentAttachmentUploadResult;
+ if (result.kind === 'uploaded') {
+ if (
+ !state.attachments.files.some(
+ (file) =>
+ file.id === result.attachment.id &&
+ file.sessionId === request.sessionId &&
+ sameValue(file.dataSource, request.dataSource),
+ )
+ )
+ state.attachments.files.push({ ...result.attachment, dataSource: request.dataSource });
+ if (
+ request.pasted &&
+ state.attachments.paste &&
+ sameFile(request.pasted, state.attachments.paste.observed.selection)
+ ) {
+ state.attachments.paste.command = { selection: request.pasted, action: 'next' };
+ }
+ if (!request.pasted && state.attachments.selection?.attempt === request.attempt) {
+ if (state.dialog === 'attachments') state.dialog = null;
+ state.attachments.selection = null;
+ }
+ state.notice = null;
+ } else state.notice = 'attachmentFailed';
+ } else if (request.kind === 'send') {
+ const started = value as AgentSendResult;
+ if (started.sessionId !== request.sessionId) {
+ state.notice = 'unknown';
+ return;
+ }
+ if (started.kind === 'rejected') {
+ state.notice =
+ started.reason === 'input_too_large' ? 'skillInputTooLarge' : 'skillInputUnavailable';
+ return;
+ }
+ state.attachments.files = state.attachments.files.filter(
+ (file) =>
+ file.sessionId !== request.sessionId ||
+ !sameValue(file.dataSource, request.dataSource) ||
+ !request.attachments.includes(file.id),
+ );
+ // Clear only the exact captured draft after a durable returned admission.
+ // The native editor rejects this command if a newer draft or IME composition exists.
+ state.command = {
+ owner: request.draft.snapshot.text.owner,
+ sequence: ++state.commandSequence,
+ basedOn: request.draft.revision,
+ edit: { kind: 'reset', text: '' },
+ };
+ state.notice = null;
+ if (state.selected?.id === started.sessionId) {
+ observe(state, actions);
+ }
+ } else {
+ state.notice = 'stopping';
+ if (state.selected?.id === request.sessionId) observe(state, actions);
+ }
+}
+
+export function receipt(state: State, actions: Action[], request: Mutation, reply: OperationReply) {
+ if (reply.receipt.kind === 'recorded' && reply.receipt.outcome.kind === 'returned') {
+ const value = reply.receipt.outcome.value;
+ const returned = value as AgentAttachmentUploadResult;
+ if (
+ (request.kind === 'send' && (value as AgentSendResult).sessionId !== request.sessionId) ||
+ (request.kind === 'uploadAttachment' &&
+ returned.kind === 'uploaded' &&
+ returned.attachment.sessionId !== request.sessionId)
+ ) {
+ state.uncertain = { reference: reply.reference, request };
+ state.notice = 'unknown';
+ return;
+ }
+ settled(state, actions, request, value);
+ } else {
+ state.uncertain = { reference: reply.reference, request };
+ state.notice = 'unknown';
+ }
+}
+
+export function queryFailed(state: State, request: Request) {
+ if (request.kind === 'draftPage' || request.kind === 'draftReceipt') {
+ draftFailed(state.drafts, request);
+ return;
+ }
+ if (
+ request.kind === 'interaction' ||
+ request.kind === 'questionRead' ||
+ request.kind === 'respond'
+ ) {
+ if (state.interaction?.attempt === request.attempt)
+ state.interaction.notice =
+ request.kind === 'questionRead'
+ ? 'questionInvalid'
+ : request.kind === 'respond'
+ ? 'interactionUnknown'
+ : 'interactionUnavailable';
+ if (state.questionSubmission?.attempt === request.attempt) state.questionSubmission = null;
+ return;
+ }
+ if (request.kind === 'queueBody' || request.kind === 'queueRead') {
+ if (state.queueEdit?.attempt === request.attempt) {
+ if (request.kind === 'queueBody') state.queueEdit.phase = 'failed';
+ state.notice = 'draftKept';
+ }
+ return;
+ }
+ // Optional presentation work does not disable input or change execution facts.
+ if (request.kind === 'history' && request.implicit) return;
+ if (request.kind === 'source') {
+ if (request.epoch === state.epoch && request.navigation === state.source?.navigation) {
+ state.source.loading = false;
+ state.source.failed = true;
+ }
+ } else if (request.kind === 'history' || request.kind === 'observe') {
+ if (
+ request.epoch === state.epoch &&
+ (request.kind !== 'history' || request.navigation === state.navigation)
+ ) {
+ state.loading = false;
+ state.notice = 'unavailable';
+ }
+ } else {
+ if (request.kind === 'openSession') {
+ if (sameValue(request.selection, state.externalSelection)) {
+ state.loading = false;
+ state.notice = 'unavailable';
+ }
+ } else if (request.kind === 'createDirectory') {
+ if (state.notice !== 'unavailable') state.notice = 'createRejected';
+ } else if (request.kind === 'restoreReceipt') {
+ state.uncertain = { reference: request.reference, request: { kind: 'restored' } };
+ state.restoring = false;
+ state.notice = 'unknown';
+ } else
+ state.notice =
+ request.kind === 'read'
+ ? 'draftKept'
+ : request.kind === 'uploadAttachment'
+ ? 'attachmentFailed'
+ : 'unavailable';
+ }
+}
+
+export function querySucceeded(state: State, actions: Action[], request: Request, value: unknown) {
+ switch (request.kind) {
+ case 'openSession': {
+ if (!sameValue(request.selection, state.externalSelection)) break;
+ const observed = value as AgentObservation;
+ if (observed.session.id !== request.selection.id) {
+ state.loading = false;
+ state.notice = 'unavailable';
+ break;
+ }
+ select(state, actions, observed.session);
+ break;
+ }
+ case 'interaction': {
+ const question = state.interaction,
+ record = value as InteractionRecord;
+ if (!question || question.attempt !== request.attempt) break;
+ if (
+ record.sessionId !== request.sessionId ||
+ record.requestId !== request.requestId ||
+ !['question', 'form', 'service_approval', 'permissions'].includes(record.request.kind) ||
+ (question.record && !sameValue(question.record.request, record.request))
+ ) {
+ question.notice = 'interactionUnavailable';
+ break;
+ }
+ if (!question.record?.outcome) question.record = record;
+ question.notice = null;
+ break;
+ }
+ case 'questionRead': {
+ const submission = state.questionSubmission,
+ question = state.interaction;
+ if (
+ !submission ||
+ !question ||
+ submission.attempt !== request.attempt ||
+ question.attempt !== request.attempt
+ )
+ break;
+ const chunk = value as TextChunk;
+ if (
+ !sameValue(chunk.metadata, request.draft.snapshot.text) ||
+ utf8Bytes(chunk.text) !== request.draft.snapshot.text.bytes ||
+ !chunk.text.trim()
+ ) {
+ state.questionSubmission = null;
+ question.notice = 'questionInvalid';
+ break;
+ }
+ submission.answers[request.index] = chunk.text;
+ submission.waiting = submission.waiting.filter((index) => index !== request.index);
+ respondQuestion(state, actions);
+ break;
+ }
+ case 'createDirectory': {
+ const page = value as AgentDirectoryPage;
+ if (page.kind !== 'roots' || !page.roots.length) {
+ state.notice = 'createRejected';
+ break;
+ }
+ intent(state, actions, { kind: 'create' }, 'agent', 'create', {
+ title: request.title,
+ labels: [],
+ workspace: { kind: 'directory', selection: page.roots[0].selection },
+ });
+ break;
+ }
+ case 'observe': {
+ if (request.epoch !== state.epoch) break;
+ const observation = value as AgentObservation;
+ if (observation.session.id !== state.selected?.id) {
+ state.loading = false;
+ state.notice = 'unavailable';
+ break;
+ }
+ if (observation.session.revision >= state.selected!.revision)
+ state.selected = observation.session;
+ if (
+ observation.through >= state.runThrough &&
+ observation.session.revision >= state.selected!.revision
+ ) {
+ state.run = observation.run ?? null;
+ state.runThrough = observation.through;
+ }
+ if (!state.watch)
+ state.parts = observation.parts.map((part) => ({
+ ...part,
+ invocationId: observation.run?.invocationId ?? null,
+ }));
+ state.latestKnown = Math.max(state.latestKnown, observation.through);
+ // Watch/run facts are independent of the fixed-fence reading window.
+ if (!state.watch) {
+ const after = Math.min(
+ observation.through,
+ ...state.parts.map((part) => part.startSequence - 1),
+ );
+ state.watch = {
+ key: 'watch-' + state.epoch,
+ input: { sessionId: state.selected!.id, after },
+ last: after,
+ };
+ }
+ if (
+ !state.window &&
+ !Object.values(state.pending).some(
+ (pending) => pending.kind === 'history' && pending.epoch === state.epoch,
+ )
+ ) {
+ const anchor = state.recallAnchor;
+ state.recallAnchor = null;
+ history(
+ state,
+ actions,
+ anchor === null
+ ? { kind: 'tail' }
+ : { kind: 'after', cursor: { sequence: anchor, offset: 0, field: 'body' } },
+ observation.through,
+ anchor === null,
+ );
+ } else if (
+ !Object.values(state.pending).some(
+ (pending) =>
+ pending.kind === 'history' &&
+ pending.epoch === state.epoch &&
+ pending.navigation === state.navigation,
+ )
+ )
+ state.loading = false;
+ follow(state, actions);
+ break;
+ }
+ case 'source': {
+ const source = state.source;
+ if (
+ request.epoch !== state.epoch ||
+ !source ||
+ request.navigation !== source.navigation ||
+ request.field !== source.field
+ )
+ break;
+ const page = value as AgentHistorySourcePage;
+ if (page.through !== request.through) {
+ queryFailed(state, request);
+ break;
+ }
+ if (page.kind === 'preparing') {
+ intent(state, actions, request, 'agent', 'source', {
+ sessionId: state.selected!.id,
+ through: request.through,
+ source: { kind: 'sequence', sequence: request.sequence },
+ field: request.field,
+ position: request.position,
+ });
+ break;
+ }
+ const next = page.source;
+ const end = next.offset + utf8Bytes(next.text);
+ if (
+ next.sequence !== request.sequence ||
+ next.path != null ||
+ next.field !== request.field ||
+ !next.fields.includes(next.field) ||
+ (!next.text && next.totalBytes > 0) ||
+ end > next.totalBytes ||
+ (source.ready &&
+ (next.totalBytes !== source.chunk.totalBytes || next.format !== source.format)) ||
+ (request.position.kind === 'after' && next.offset !== request.position.offset) ||
+ (request.position.kind === 'before' && end !== request.position.offset) ||
+ (request.position.kind === 'start' && next.offset !== 0) ||
+ (request.position.kind === 'tail' && end !== next.totalBytes)
+ ) {
+ queryFailed(state, request);
+ break;
+ }
+ source.chunk = {
+ ...source.chunk,
+ sequence: next.sequence,
+ offset: next.offset,
+ totalBytes: next.totalBytes,
+ text: next.text,
+ };
+ source.fields = next.fields;
+ source.format = next.format;
+ source.ready = true;
+ source.loading = false;
+ source.failed = false;
+ source.landing = {
+ sequence: request.navigation,
+ edge:
+ request.position.kind === 'before' || request.position.kind === 'tail' ? 'end' : 'start',
+ };
+ break;
+ }
+ case 'history': {
+ if (request.epoch !== state.epoch || request.navigation !== state.navigation) break;
+ const page = value as AgentHistoryPage;
+ if (page.through !== request.through) {
+ queryFailed(state, request);
+ break;
+ }
+ if (page.kind === 'preparing') {
+ intent(
+ state,
+ actions,
+ request,
+ 'agent',
+ 'history',
+ {
+ sessionId: state.selected!.id,
+ through: request.through,
+ position: request.position,
+ includeThinking: true,
+ },
+ {},
+ request.implicit ? [{ child: 'timeline', method: 'tail', input: null }] : [],
+ );
+ break;
+ }
+ const old = state.window;
+ const splitMessage = old?.page.chunks.some((oldChunk) =>
+ page.chunks.some(
+ (chunk) => chunk.sequence === oldChunk.sequence && chunk.field === oldChunk.field,
+ ),
+ );
+ if (request.extend && old?.page.through === page.through && !splitMessage) {
+ const before = request.position.kind === 'before';
+ const chunks = before
+ ? [...page.chunks, ...old.page.chunks]
+ : [...old.page.chunks, ...page.chunks];
+ let bytes = chunks.reduce((total, chunk) => total + utf8Bytes(chunk.text), 0);
+ let first = before ? page.before : old.page.before;
+ let last = before ? old.page.after : page.after;
+ // Adjacent canonical pages share one fence. Retain the displayed edge
+ // and its native anchor. Keep two complete pages so loading an adjacent
+ // page cannot evict the message that anchors the current viewport.
+ while (chunks.length > 16 || bytes > 131072) {
+ const removed = before ? chunks.pop()! : chunks.shift()!;
+ bytes -= utf8Bytes(removed.text);
+ if (before)
+ last = { sequence: removed.sequence, offset: removed.offset, field: removed.field };
+ else {
+ const start = chunks[0]!;
+ first = { sequence: start.sequence, offset: start.offset, field: start.field };
+ }
+ }
+ state.window = {
+ page: { ...page, chunks, before: first, after: last },
+ latest: last === null,
+ };
+ } else {
+ state.window = { page, latest: request.latest };
+ state.landing = {
+ sequence: state.landing.sequence + 1,
+ edge: request.position.kind === 'before' || request.latest ? 'end' : 'start',
+ };
+ }
+ state.loading = false;
+ if (!request.implicit) state.notice = null;
+ state.live = state.live.filter(
+ (fragment) =>
+ fragment.sequence > page.through ||
+ ((fragment.closedAt === null || fragment.closedAt > page.through) &&
+ !page.chunks.some(
+ (chunk) =>
+ chunk.messageId === fragment.messageId &&
+ (chunk.field ?? 'body') === fragment.field,
+ )),
+ );
+ break;
+ }
+ case 'queueBody': {
+ const edit = state.queueEdit;
+ if (!edit || edit.attempt !== request.attempt) break;
+ const body = value as AgentQueueMessageResult;
+ if (body.kind !== 'ready' || body.revision !== edit.target.expectedRevision) {
+ edit.phase = 'failed';
+ state.notice = 'queueChanged';
+ break;
+ }
+ edit.body = body.message;
+ edit.hasAttachments = body.hasAttachments;
+ edit.phase = 'initializing';
+ initializeQueueEdit(state);
+ break;
+ }
+ case 'queueRead': {
+ const edit = state.queueEdit;
+ if (!edit || edit.attempt !== request.attempt || state.dialog !== 'queue') break;
+ const chunk = value as TextChunk,
+ captured = request.draft.snapshot.text;
+ if (!sameValue(chunk.metadata, captured) || utf8Bytes(chunk.text) !== captured.bytes) {
+ state.notice = 'draftKept';
+ break;
+ }
+ if (!chunk.text.trim() && !edit.hasAttachments) {
+ state.notice = 'empty';
+ break;
+ }
+ intent(
+ state,
+ actions,
+ {
+ kind: 'queue',
+ sessionId: edit.target.sessionId,
+ saved: { attempt: edit.attempt, draft: request.draft },
+ },
+ 'agent',
+ 'editQueue',
+ {
+ sessionId: edit.target.sessionId,
+ expectedRevision: edit.target.expectedRevision,
+ edit: { kind: 'update', messageId: edit.target.messageId, message: chunk.text },
+ },
+ );
+ break;
+ }
+ case 'read': {
+ if (state.selected?.id === request.sessionId && state.selected.archived) {
+ state.notice = 'draftKept';
+ break;
+ }
+ const chunk = value as TextChunk;
+ const metadata = chunk.metadata as { owner?: string; revision?: number; bytes?: number };
+ const captured = request.draft.snapshot.text;
+ if (
+ !metadata ||
+ metadata.owner !== captured.owner ||
+ metadata.revision !== captured.revision ||
+ metadata.bytes !== captured.bytes
+ ) {
+ state.notice = 'draftKept';
+ break;
+ }
+ if (!chunk.text.trim() && request.attachments.length === 0) {
+ state.notice = 'empty';
+ break;
+ }
+ intent(
+ state,
+ actions,
+ {
+ kind: 'send',
+ dataSource: request.dataSource,
+ attachments: request.attachments,
+ draft: request.draft,
+ sessionId: request.sessionId,
+ backend: request.backend,
+ },
+ 'agent',
+ request.backend.kind === 'executor' ? 'execute' : 'send',
+ request.backend.kind === 'executor'
+ ? {
+ sessionId: request.sessionId,
+ message: chunk.text,
+ executor: request.backend.selection,
+ attachments: request.attachments,
+ }
+ : {
+ sessionId: request.sessionId,
+ message: chunk.text,
+ attachments: request.attachments,
+ model: request.backend.model,
+ thinkingLevel: request.backend.thinkingLevel,
+ },
+ );
+ break;
+ }
+ case 'draftPage': {
+ draftPage(state.drafts, request, value as CallerIntent[]);
+ break;
+ }
+ case 'draftReceipt': {
+ draftReceipt(state.drafts, request, value as OperationReply);
+ break;
+ }
+ case 'restorePage': {
+ const page = value as CallerIntent[];
+ if (!page.length) {
+ state.restoring = false;
+ break;
+ }
+ const original = page[0];
+ state.restoreAfter = original.cursor;
+ intent(
+ state,
+ actions,
+ { kind: 'restoreReceipt', reference: original.reference, cursor: original.cursor },
+ 'receipts',
+ 'read',
+ original.reference,
+ );
+ break;
+ }
+ case 'restoreReceipt': {
+ const reply = value as OperationReply;
+ if (!sameValue(request.reference, reply.reference)) {
+ state.uncertain = { reference: request.reference, request: { kind: 'restored' } };
+ state.notice = 'unknown';
+ state.restoring = false;
+ break;
+ }
+ if (reply.receipt.kind === 'recorded' && reply.receipt.outcome.kind === 'returned')
+ intent(state, actions, { kind: 'restorePage' }, 'history', 'page', {
+ after: request.cursor,
+ limit: 1,
+ });
+ else {
+ state.uncertain = { reference: request.reference, request: { kind: 'restored' } };
+ state.notice = 'unknown';
+ state.restoring = false;
+ }
+ break;
+ }
+ case 'receipt': {
+ const original = state.uncertain;
+ const reply = value as OperationReply;
+ if (!original || !sameValue(original.reference, reply.reference)) {
+ state.notice = 'unknown';
+ break;
+ }
+ receipt(state, actions, original.request, reply);
+ break;
+ }
+ default:
+ state.notice = 'unknown';
+ }
+}
+
+export function delivery(state: State, page: AgentDeliveryPage, actions: Action[]) {
+ if (page.kind === 'interactions') {
+ if (!state.interactions || page.pending.revision > state.interactions.revision) {
+ const changed =
+ !state.interactions || !sameValue(state.interactions.entries, page.pending.entries);
+ state.interactions = page.pending;
+ if (changed) observe(state, actions);
+ const question = state.interaction;
+ if (
+ question?.record &&
+ !question.record.outcome &&
+ question.sessionId === state.selected?.id &&
+ !page.pending.entries.some((entry) => entry.requestId === question.requestId)
+ )
+ readInteraction(state, actions);
+ }
+ return;
+ }
+ if (page.kind === 'queue') {
+ if (!state.queue || page.queue.revision > state.queue.revision) state.queue = page.queue;
+ return;
+ }
+ const watch = state.watch;
+ if (!watch) return;
+ state.latestKnown = Math.max(state.latestKnown, page.through);
+ let canonicalChanged = false;
+ for (const event of page.events) {
+ if (event.sequence <= watch.last) continue;
+ const fact = event.fact;
+ if (
+ [
+ 'opened',
+ 'executor_started',
+ 'transcript_changed',
+ 'part_finished',
+ 'step_ended',
+ 'ended',
+ ].includes(fact.kind)
+ )
+ canonicalChanged = true;
+ if (fact.kind === 'opened') {
+ if (event.sequence > state.runThrough) {
+ state.run = { invocationId: event.invocationId, turnId: event.turnId, state: 'running' };
+ state.runThrough = event.sequence;
+ }
+ } else if (fact.kind === 'part_started' || fact.kind === 'executor_started') {
+ const started: DisplayPart[] =
+ fact.kind === 'executor_started'
+ ? (['text', 'thinking'] as const).map((textKind) => ({
+ startSequence: event.sequence,
+ messageId: fact.messageId,
+ stepId: event.invocationId,
+ partId: textKind,
+ textKind,
+ invocationId: event.invocationId,
+ }))
+ : [
+ {
+ startSequence: event.sequence,
+ messageId: fact.messageId,
+ stepId: fact.stepId,
+ partId: fact.partId,
+ textKind: fact.textKind,
+ invocationId: event.invocationId,
+ },
+ ];
+ state.parts = state.parts.filter(
+ (part) =>
+ !started.some(
+ (next) =>
+ part.invocationId === next.invocationId &&
+ part.stepId === next.stepId &&
+ part.partId === next.partId,
+ ),
+ );
+ if (state.parts.length + started.length > 128) {
+ state.watch = null;
+ state.notice = 'unavailable';
+ return;
+ }
+ state.parts.push(...started);
+ } else if (fact.kind === 'delta') {
+ const part = state.parts.find(
+ (part) =>
+ part.invocationId === event.invocationId &&
+ part.stepId === fact.stepId &&
+ part.partId === fact.partId,
+ );
+ if (part) {
+ appendLive(state, event.sequence, part, event.invocationId, fact.text);
+ }
+ } else if (fact.kind === 'part_finished') {
+ state.parts = state.parts.filter(
+ (part) =>
+ part.invocationId !== event.invocationId ||
+ part.stepId !== fact.stepId ||
+ part.partId !== fact.partId,
+ );
+ } else if (fact.kind === 'step_ended') {
+ closeLive(state, event.sequence, event.invocationId, fact.stepId);
+ state.parts = state.parts.filter(
+ (part) => part.invocationId !== event.invocationId || part.stepId !== fact.stepId,
+ );
+ } else if (fact.kind === 'ended') {
+ closeLive(state, event.sequence, event.invocationId);
+ state.parts = state.parts.filter((part) => part.invocationId !== event.invocationId);
+ if (event.sequence > state.runThrough && state.run?.invocationId === event.invocationId) {
+ state.run = null;
+ state.runThrough = event.sequence;
+ state.notice = fact.failed ? 'failed' : 'completed';
+ }
+ }
+ }
+ watch.last = Math.max(watch.last, page.nextAfter);
+ if (canonicalChanged) follow(state, actions);
+}
diff --git a/plugins/agent/agent-screen/state.ts b/plugins/agent/agent-screen/state.ts
new file mode 100644
index 0000000000..42b4108557
--- /dev/null
+++ b/plugins/agent/agent-screen/state.ts
@@ -0,0 +1,333 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { type AppearanceEditor } from '../../shared/appearance-editor.js';
+import { type SettingsCategory } from '../../shared/settings.js';
+import type {
+ ModelSelection,
+ AgentExecutorSelection,
+ ThinkingLevel,
+ AgentQueue,
+ AgentPendingInteractions,
+ InteractionRecord,
+ AgentQueueMessage,
+ AgentHistoryPage,
+ AgentHistoryChunk,
+ SourceField,
+ SourceFormat,
+ SourcePosition,
+ HistoryPosition,
+ AgentPart,
+ AgentRun,
+ AgentSession,
+ AgentDataSource,
+ AgentSessionPage,
+ AgentList,
+ AgentScheduleAlerts,
+ FileCapture,
+ RecoveryRef,
+ EditorCommand,
+ EditorObservation,
+ TerminalPaneCommand,
+} from '../../../packages/plugin-sdk/src/index.js';
+import type {
+ AgentScreenProps,
+ InsightsProps,
+ PageChrome,
+ SelectedModel,
+ ModelSetup,
+ TimelineFileContext,
+ MessageCopyObservation,
+} from '../../shared/contracts.js';
+import { type Bookmark } from '../../shared/bookmark.js';
+import { type Copy } from '../../shared/locale.js';
+
+import { type AttachmentDrafts } from '../../shared/attachments.js';
+
+import { type DraftRecord, type DraftStep } from '../../shared/drafts.js';
+
+type Notice = keyof Copy | null;
+
+export type Backend =
+ | { kind: 'model'; model: ModelSelection; thinkingLevel: ThinkingLevel | null }
+ | { kind: 'executor'; selection: AgentExecutorSelection };
+
+export type Request =
+ | DraftStep
+ | { kind: 'observe'; epoch: number }
+ | {
+ kind: 'history';
+ epoch: number;
+ through: number;
+ navigation: number;
+ latest: boolean;
+ implicit: boolean;
+ extend?: boolean;
+ position: HistoryPosition;
+ }
+ | {
+ kind: 'read';
+ dataSource: AgentDataSource;
+ attachments: string[];
+ draft: EditorObservation;
+ sessionId: string;
+ backend: Backend;
+ }
+ | {
+ kind: 'source';
+ epoch: number;
+ navigation: number;
+ through: number;
+ sequence: number;
+ field: SourceField;
+ position: SourcePosition;
+ }
+ | { kind: 'interaction'; attempt: number; sessionId: string; requestId: string }
+ | { kind: 'questionRead'; attempt: number; index: number; draft: EditorObservation }
+ | { kind: 'respond'; attempt: number; sessionId: string; requestId: string }
+ | { kind: 'openSession'; selection: NonNullable<AgentScreenProps['session']> }
+ | { kind: 'create' }
+ | { kind: 'createDirectory'; title: string }
+ | {
+ kind: 'send';
+ dataSource: AgentDataSource;
+ attachments: string[];
+ draft: EditorObservation;
+ sessionId: string;
+ backend: Backend;
+ }
+ | {
+ kind: 'uploadAttachment';
+ sessionId: string;
+ dataSource: AgentDataSource;
+ attempt: number;
+ pasted?: FileCapture;
+ }
+ | { kind: 'markRead'; sessionId: string }
+ | { kind: 'stop'; sessionId: string }
+ | { kind: 'queue'; sessionId: string; saved?: { attempt: number; draft: EditorObservation } }
+ | { kind: 'queueBody'; attempt: number }
+ | { kind: 'queueRead'; attempt: number; draft: EditorObservation }
+ | { kind: 'receipt' }
+ | { kind: 'restorePage' }
+ | { kind: 'restoreReceipt'; reference: RecoveryRef; cursor: number };
+
+export type Mutation =
+ | Extract<
+ Request,
+ { kind: 'create' | 'send' | 'stop' | 'queue' | 'respond' | 'markRead' | 'uploadAttachment' }
+ >
+ | { kind: 'restored' };
+
+export interface DisplayPart extends AgentPart {
+ invocationId: string | null;
+}
+
+interface Uncertain {
+ reference: RecoveryRef;
+ request: Mutation;
+}
+
+interface QueueEdit {
+ hasAttachments: boolean;
+ attempt: number;
+ target: AgentQueueMessage;
+ phase: 'loading' | 'initializing' | 'ready' | 'failed';
+ body: string | null;
+ command: EditorCommand | null;
+}
+
+export type Page =
+ | 'settings'
+ | 'connections'
+ | 'deployment'
+ | 'projects'
+ | 'imports'
+ | 'apps'
+ | 'recall'
+ | 'scheduler';
+
+export type Navigation = { pages: State['pages']; category: SettingsCategory };
+
+export function navigationBookmark(state: State): Navigation {
+ return { pages: state.pages, category: state.settingsCategory };
+}
+
+export function navigationKey(state: State): string {
+ return (
+ 'navigation/' +
+ (state.embedding.fixed ? (state.embedding.session?.id ?? 'pending') : 'agent-screen')
+ );
+}
+
+export interface State {
+ bookmark: Bookmark<Navigation>;
+ embedding: AgentScreenProps;
+ externalSelection: AgentScreenProps['session'] | null;
+ preferences: AppearanceEditor;
+ drafts: DraftRecord[];
+ attachments: AttachmentDrafts;
+ revision: number;
+ serial: number;
+ epoch: number;
+ catalog: AgentSessionPage | null;
+ catalogGeneration: number;
+ catalogWatch: { key: string; input: AgentList; loading: boolean } | null;
+ catalogFilter: AgentList['filter'];
+ catalogFailed: boolean;
+ selected: AgentSession | null;
+ model: SelectedModel | null;
+ executor: AgentExecutorSelection | null;
+ hasBackends: boolean;
+ modelSetup: ModelSetup['kind'];
+ modelOpenRequest: number | null;
+ catalogEpoch: number;
+ modelRefreshing: boolean;
+ searchOpen: boolean;
+ browse: number | null;
+ pages: Page[];
+ openedPages: Page[];
+ pageChrome: Partial<Record<Page, PageChrome>>;
+ pageBack: Partial<Record<Page, number>>;
+ recallAnchor: number | null;
+ scheduledTasks: boolean;
+ web: boolean;
+ jev: boolean;
+ scheduleAlerts: AgentScheduleAlerts;
+ scheduleWatch: string | null;
+ scheduleRequested: { sequence: number; taskId: string } | null;
+ settingsCategory: SettingsCategory;
+ insightsScope: InsightsProps['scope'];
+ sidebar: { open: boolean } | { command: TerminalPaneCommand | null };
+ terminals: boolean;
+ skills: boolean;
+ place: string;
+ dialog:
+ | 'attachments'
+ | 'terminal'
+ | 'skills'
+ | 'palette'
+ | 'directories'
+ | 'details'
+ | 'help'
+ | 'queue'
+ | 'question'
+ | 'approval'
+ | null;
+ window: {
+ page: Extract<AgentHistoryPage, { kind: 'ready' }>;
+ latest: boolean;
+ } | null;
+ navigation: number;
+ image: { sessionId: string; through: number; sequence: number } | null;
+ source: {
+ through: number;
+ chunk: Pick<AgentHistoryChunk, 'sequence' | 'offset' | 'totalBytes' | 'text'>;
+ navigation: number;
+ landing: { sequence: number; edge: 'start' | 'end' };
+ loading: boolean;
+ failed: boolean;
+ field: SourceField;
+ fields: SourceField[];
+ format: SourceFormat | null;
+ ready: boolean;
+ position: SourcePosition;
+ } | null;
+ landing: { sequence: number; edge: 'start' | 'end' };
+ tailReceipt: { landing: number; readingRevision: number } | null;
+ fileContext: TimelineFileContext | null;
+ copyRequest: number | null;
+ messageCopy: MessageCopyObservation | null;
+ followAttempt: {
+ through: number;
+ landing: number;
+ readingRevision: number;
+ } | null;
+ parts: DisplayPart[];
+ run: AgentRun | null;
+ queue: AgentQueue | null;
+ queueEdit: QueueEdit | null;
+ queueDraft: EditorObservation | null;
+ interactions: AgentPendingInteractions | null;
+ interaction: {
+ attempt: number;
+ sessionId: string;
+ requestId: string;
+ record: InteractionRecord | null;
+ notice: Notice;
+ } | null;
+ questionSubmission: {
+ attempt: number;
+ answers: (string | null)[];
+ waiting: number[];
+ } | null;
+ runThrough: number;
+ loading: boolean;
+ live: {
+ sequence: number;
+ messageId: string;
+ invocationId: string;
+ stepId: string;
+ text: string;
+ closedAt: number | null;
+ field: 'body' | 'thinking';
+ }[];
+ latestKnown: number;
+ watch: {
+ key: string;
+ input: { sessionId: string; after: number };
+ last: number;
+ } | null;
+ pending: Record<string, Request>;
+ uncertain: Uncertain | null;
+ restoring: boolean;
+ restoreAfter: number;
+ command: EditorCommand | null;
+ commandSequence: number;
+ notice: Notice;
+}
+
+export type RootCommand =
+ | 'browse'
+ | 'copy-file-path'
+ | 'copy-message'
+ | 'copy-source'
+ | 'search-open'
+ | 'help-open'
+ | 'interaction-open'
+ | 'create'
+ | 'appearance-open'
+ | 'model-setup'
+ | 'terminal-open'
+ | 'skills-open'
+ | 'details-open'
+ | 'usage-open'
+ | 'create-workspace'
+ | 'navigation-toggle'
+ | 'settings'
+ | 'projects-open'
+ | 'imports-open'
+ | 'recall-open'
+ | 'scheduler-open'
+ | 'apps-open'
+ | 'workhub-open'
+ | 'deployment-open'
+ | 'reload'
+ | 'stop'
+ | 'check';
diff --git a/plugins/agent/workspace/tui.ts b/plugins/agent/agent-screen/tui.ts
similarity index 82%
rename from plugins/agent/workspace/tui.ts
rename to plugins/agent/agent-screen/tui.ts
index 7f502ac345..5fd9a220ba 100644
--- a/plugins/agent/workspace/tui.ts
+++ b/plugins/agent/agent-screen/tui.ts
@@ -18,9 +18,9 @@
*/
import { presentations } from '../../shared/view.js';
-import { workspace } from './index.js';
-import { workspaceContract } from '../../shared/contracts.js';
-export let component: ReturnType<typeof workspace>;
+import { agentScreen } from './index.js';
+import { agentScreenContract } from '../../shared/contracts.js';
+export let component: ReturnType<typeof agentScreen>;
export function activate(configuration: {
locale: string;
terminals?: boolean;
@@ -30,7 +30,7 @@ export function activate(configuration: {
web?: boolean;
jev?: boolean;
}) {
- component = workspace(
+ component = agentScreen(
presentations.tui,
configuration.locale,
configuration.terminals === true,
@@ -41,6 +41,6 @@ export function activate(configuration: {
configuration.jev === true,
);
return {
- Workspace: { kind: 'component', contract: workspaceContract, component: ['component'] },
+ AgentScreen: { kind: 'component', contract: agentScreenContract, component: ['component'] },
};
}
diff --git a/plugins/agent/workspace/index.ts b/plugins/agent/workspace/index.ts
deleted file mode 100644
index e9826631a2..0000000000
--- a/plugins/agent/workspace/index.ts
+++ /dev/null
@@ -1,5248 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import {
- initialAppearance,
- effectiveAppearance,
- effectiveLocale,
- appearanceEvent,
- appearanceView,
- type AppearanceEditor,
-} from '../../shared/appearance-editor.js';
-import { settingsView, settingsEvent, type SettingsCategory } from '../../shared/settings.js';
-import { tuiText } from '../../shared/tui-locale.js';
-import type {
- Action,
- ActionOutcome,
- Emission,
- Shortcut,
- ModelSelection,
- AgentExecutorSelection,
- ThinkingLevel,
- AgentDeliveryPage,
- AgentQueue,
- AgentPendingInteractions,
- InteractionRecord,
- InteractionAnswer,
- AgentInteractionResult,
- AgentQueueMessage,
- AgentQueueMessageResult,
- AgentHistoryPage,
- AgentHistoryChunk,
- AgentHistorySourcePage,
- SourceField,
- SourceFormat,
- SourcePosition,
- HistoryPosition,
- AgentObservation,
- AgentPart,
- AgentRun,
- AgentSession,
- AgentDataSource,
- AgentSessionPage,
- AgentCreated,
- AgentMarkReadResult,
- AgentDirectoryPage,
- AgentDirectorySelection,
- AgentProjectRef,
- AgentList,
- AgentScheduleAlerts,
- AgentSendResult,
- AgentAttachmentUploadResult,
- FileObservation,
- FileCapture,
- AgentQueueMutationResult,
- AgentQueueAction,
- RowsProps,
- RowActivated,
- CallerIntent,
- Component,
- ComponentInput,
- ComponentValues,
- OperationReply,
- RecoveryRef,
- EditorCommand,
- EditorObservation,
- TextChunk,
- TerminalPaneCommand,
-} from '../../../packages/plugin-sdk/src/index.js';
-import type {
- WorkspaceProps,
- InsightsProps,
- PageChrome,
- PaletteCommand,
- Message,
- Theme,
- QueueEditorProps,
- QuestionDraft,
- QuestionEditorProps,
- ServiceApprovalProps,
- SelectedModel,
- ModelSetup,
- TimelineProps,
- TimelineFileContext,
- MessageCopyObservation,
-} from '../../shared/contracts.js';
-import { sameValue } from '../../shared/value.js';
-import {
- bookmark,
- bookmarkReady,
- bookmarkView,
- observeBookmark,
- retryBookmark,
- type Bookmark,
-} from '../../shared/bookmark.js';
-import { copy, resolveLocale, type Copy } from '../../shared/locale.js';
-import {
- compactLabel,
- fill,
- output,
- pixels,
- type Presentation,
- type Node,
-} from '../../shared/view.js';
-
-import {
- attachmentFiles,
- attachmentCapacity,
- attachmentChild,
- attachmentView,
- attachmentSummary,
- pastedFiles,
- type DraftFilePaste,
- pasteView,
- sameFile,
- selectedFile,
- type AttachmentDrafts,
-} from '../../shared/attachments.js';
-
-import {
- observeDraft,
- draftReady,
- retainSelectedDraft,
- draftStatuses,
- nextDraftStep,
- draftPage,
- draftReceipt,
- draftFailed,
- retryDraft,
- type DraftRecord,
- type DraftStep,
- type DraftCheckpoint,
-} from '../../shared/drafts.js';
-
-type Notice = keyof Copy | null;
-type Backend =
- | { kind: 'model'; model: ModelSelection; thinkingLevel: ThinkingLevel | null }
- | { kind: 'executor'; selection: AgentExecutorSelection };
-type Request =
- | DraftStep
- | { kind: 'observe'; epoch: number }
- | {
- kind: 'history';
- epoch: number;
- through: number;
- navigation: number;
- latest: boolean;
- implicit: boolean;
- extend?: boolean;
- position: HistoryPosition;
- }
- | {
- kind: 'read';
- dataSource: AgentDataSource;
- attachments: string[];
- draft: EditorObservation;
- sessionId: string;
- backend: Backend;
- }
- | {
- kind: 'source';
- epoch: number;
- navigation: number;
- through: number;
- sequence: number;
- field: SourceField;
- position: SourcePosition;
- }
- | { kind: 'interaction'; attempt: number; sessionId: string; requestId: string }
- | { kind: 'questionRead'; attempt: number; index: number; draft: EditorObservation }
- | { kind: 'respond'; attempt: number; sessionId: string; requestId: string }
- | { kind: 'openSession'; selection: NonNullable<WorkspaceProps['session']> }
- | { kind: 'create' }
- | { kind: 'createDirectory'; title: string }
- | {
- kind: 'send';
- dataSource: AgentDataSource;
- attachments: string[];
- draft: EditorObservation;
- sessionId: string;
- backend: Backend;
- }
- | {
- kind: 'uploadAttachment';
- sessionId: string;
- dataSource: AgentDataSource;
- attempt: number;
- pasted?: FileCapture;
- }
- | { kind: 'markRead'; sessionId: string }
- | { kind: 'stop'; sessionId: string }
- | { kind: 'queue'; sessionId: string; saved?: { attempt: number; draft: EditorObservation } }
- | { kind: 'queueBody'; attempt: number }
- | { kind: 'queueRead'; attempt: number; draft: EditorObservation }
- | { kind: 'receipt' }
- | { kind: 'restorePage' }
- | { kind: 'restoreReceipt'; reference: RecoveryRef; cursor: number };
-type Mutation =
- | Extract<
- Request,
- { kind: 'create' | 'send' | 'stop' | 'queue' | 'respond' | 'markRead' | 'uploadAttachment' }
- >
- | { kind: 'restored' };
-interface DisplayPart extends AgentPart {
- invocationId: string | null;
-}
-interface Uncertain {
- reference: RecoveryRef;
- request: Mutation;
-}
-interface QueueEdit {
- hasAttachments: boolean;
- attempt: number;
- target: AgentQueueMessage;
- phase: 'loading' | 'initializing' | 'ready' | 'failed';
- body: string | null;
- command: EditorCommand | null;
-}
-type Page =
- | 'settings'
- | 'connections'
- | 'deployment'
- | 'projects'
- | 'imports'
- | 'apps'
- | 'recall'
- | 'scheduler';
-type Navigation = { pages: State['pages']; category: SettingsCategory };
-function navigationBookmark(state: State): Navigation {
- return { pages: state.pages, category: state.settingsCategory };
-}
-function navigationKey(state: State): string {
- return (
- 'navigation/' +
- (state.embedding.fixed ? (state.embedding.session?.id ?? 'pending') : 'workspace')
- );
-}
-export interface State {
- bookmark: Bookmark<Navigation>;
- embedding: WorkspaceProps;
- externalSelection: WorkspaceProps['session'] | null;
- preferences: AppearanceEditor;
- drafts: DraftRecord[];
- attachments: AttachmentDrafts;
- revision: number;
- serial: number;
- epoch: number;
- catalog: AgentSessionPage | null;
- catalogGeneration: number;
- catalogWatch: { key: string; input: AgentList; loading: boolean } | null;
- catalogFilter: AgentList['filter'];
- catalogFailed: boolean;
- selected: AgentSession | null;
- model: SelectedModel | null;
- executor: AgentExecutorSelection | null;
- hasBackends: boolean;
- modelSetup: ModelSetup['kind'];
- modelOpenRequest: number | null;
- catalogEpoch: number;
- modelRefreshing: boolean;
- searchOpen: boolean;
- browse: number | null;
- pages: Page[];
- openedPages: Page[];
- pageChrome: Partial<Record<Page, PageChrome>>;
- pageBack: Partial<Record<Page, number>>;
- recallAnchor: number | null;
- scheduledTasks: boolean;
- web: boolean;
- jev: boolean;
- scheduleAlerts: AgentScheduleAlerts;
- scheduleWatch: string | null;
- scheduleRequested: { sequence: number; taskId: string } | null;
- settingsCategory: SettingsCategory;
- insightsScope: InsightsProps['scope'];
- sidebar: { open: boolean } | { command: TerminalPaneCommand | null };
- terminals: boolean;
- skills: boolean;
- place: string;
- dialog:
- | 'attachments'
- | 'terminal'
- | 'skills'
- | 'palette'
- | 'directories'
- | 'details'
- | 'help'
- | 'queue'
- | 'question'
- | 'approval'
- | null;
- window: {
- page: Extract<AgentHistoryPage, { kind: 'ready' }>;
- latest: boolean;
- } | null;
- navigation: number;
- image: { sessionId: string; through: number; sequence: number } | null;
- source: {
- through: number;
- chunk: Pick<AgentHistoryChunk, 'sequence' | 'offset' | 'totalBytes' | 'text'>;
- navigation: number;
- landing: { sequence: number; edge: 'start' | 'end' };
- loading: boolean;
- failed: boolean;
- field: SourceField;
- fields: SourceField[];
- format: SourceFormat | null;
- ready: boolean;
- position: SourcePosition;
- } | null;
- landing: { sequence: number; edge: 'start' | 'end' };
- tailReceipt: { landing: number; readingRevision: number } | null;
- fileContext: TimelineFileContext | null;
- copyRequest: number | null;
- messageCopy: MessageCopyObservation | null;
- followAttempt: {
- through: number;
- landing: number;
- readingRevision: number;
- } | null;
- parts: DisplayPart[];
- run: AgentRun | null;
- queue: AgentQueue | null;
- queueEdit: QueueEdit | null;
- queueDraft: EditorObservation | null;
- interactions: AgentPendingInteractions | null;
- interaction: {
- attempt: number;
- sessionId: string;
- requestId: string;
- record: InteractionRecord | null;
- notice: Notice;
- } | null;
- questionSubmission: {
- attempt: number;
- answers: (string | null)[];
- waiting: number[];
- } | null;
- runThrough: number;
- loading: boolean;
- live: {
- sequence: number;
- messageId: string;
- invocationId: string;
- stepId: string;
- text: string;
- closedAt: number | null;
- field: 'body' | 'thinking';
- }[];
- latestKnown: number;
- watch: {
- key: string;
- input: { sessionId: string; after: number };
- last: number;
- } | null;
- pending: Record<string, Request>;
- uncertain: Uncertain | null;
- restoring: boolean;
- restoreAfter: number;
- command: EditorCommand | null;
- commandSequence: number;
- notice: Notice;
-}
-function utf8Bytes(text: string): number {
- let bytes = 0;
- for (const character of text) {
- const point = character.codePointAt(0)!;
- bytes += point <= 0x7f ? 1 : point <= 0x7ff ? 2 : point <= 0xffff ? 3 : 4;
- }
- return bytes;
-}
-// Raw fences can advance while model text is still open. Only an actual step
-// closure covers its live prefix for both atomic and item-accepting providers.
-function appendLive(
- state: State,
- sequence: number,
- part: DisplayPart,
- invocationId: string,
- text: string,
-) {
- const messageId = part.messageId;
- const field = part.textKind === 'thinking' ? 'thinking' : 'body';
- if (
- state.window &&
- sequence <= state.window.page.through &&
- state.window.page.chunks.some(
- (chunk) => chunk.messageId === messageId && (chunk.field ?? 'body') === field,
- )
- )
- return;
- const ids = new Set(state.live.map((fragment) => fragment.messageId));
- let remaining = 16000 - state.live.reduce((total, fragment) => total + fragment.text.length, 0);
- // Thinking shares the existing prefix budget but leaves room for an answer
- // before atomic providers publish either field. Never evict a displayed prefix.
- if (field === 'thinking') {
- const thinking = state.live.filter((fragment) => fragment.field === 'thinking');
- const thinkingIds = new Set(thinking.map((fragment) => fragment.messageId));
- if (thinking.length >= 32 || (!thinkingIds.has(messageId) && thinkingIds.size >= 8)) return;
- remaining = Math.min(
- remaining,
- 8000 - thinking.reduce((total, fragment) => total + fragment.text.length, 0),
- );
- }
- // A truncated surrogate can leave one unused code unit; keep the prefix closed.
- if (remaining <= 1 || state.live.length >= 64 || (!ids.has(messageId) && ids.size >= 16)) return;
- text = text.slice(0, remaining);
- const last = text.charCodeAt(text.length - 1);
- if (last >= 0xd800 && last <= 0xdbff) text = text.slice(0, -1);
- if (text)
- state.live.push({
- sequence,
- messageId,
- invocationId,
- stepId: part.stepId,
- text,
- closedAt: null,
- field,
- });
-}
-function closeLive(state: State, sequence: number, invocationId: string, stepId?: string) {
- for (const fragment of state.live)
- if (
- fragment.closedAt === null &&
- fragment.invocationId === invocationId &&
- (stepId === undefined || fragment.stepId === stepId)
- )
- fragment.closedAt = sequence;
-}
-function messages(state: State): Message[] {
- const canonical: Message[] = (state.window?.page.chunks ?? []).map((chunk) => ({
- id: chunk.field === 'thinking' ? 'thinking-' + chunk.messageId : chunk.messageId,
- contentKey: `canonical-${chunk.sequence}-${chunk.offset}${chunk.field === 'thinking' ? '-thinking' : ''}`,
- role: chunk.field === 'thinking' ? 'thinking' : chunk.role,
- timestamp: chunk.timestamp,
- text: chunk.text,
- tool: chunk.tool,
- continued: chunk.offset !== 0 || utf8Bytes(chunk.text) !== chunk.totalBytes,
- source: chunk.sequence,
- image: chunk.tool?.result?.image ?? false,
- }));
- if (!state.window?.latest) return canonical;
- const live = new Map<string, Message>();
- for (const fragment of state.live) {
- if (
- fragment.sequence <= state.window.page.through &&
- state.window.page.chunks.some(
- (chunk) =>
- chunk.messageId === fragment.messageId && (chunk.field ?? 'body') === fragment.field,
- )
- )
- continue;
- const key = fragment.messageId + (fragment.field === 'thinking' ? '-thinking' : '');
- let message = live.get(key);
- if (!message) {
- message = {
- id: 'live-' + key,
- contentKey: 'live-' + key,
- role: fragment.field === 'thinking' ? 'thinking' : 'assistant',
- timestamp: null,
- text: '',
- tool: null,
- continued: true,
- source: null,
- image: false,
- };
- live.set(key, message);
- }
- message.text += fragment.text;
- }
- return [...canonical, ...live.values()];
-}
-function turnTimings(state: State, displayed: Message[]): TimelineProps['timings'] {
- const turns = new Map<string, TimelineProps['timings'][number]>();
- for (const chunk of state.window?.page.chunks ?? []) {
- if (!chunk.timing) continue;
- if (!chunk.timing.finished && (!state.window?.latest || state.run?.turnId !== chunk.turnId))
- continue;
- turns.set(chunk.turnId, {
- ...chunk.timing,
- turnId: chunk.turnId,
- afterMessageId: chunk.field === 'thinking' ? 'thinking-' + chunk.messageId : chunk.messageId,
- });
- }
- if (state.window?.latest && state.run) {
- const timing = turns.get(state.run.turnId);
- if (timing && !timing.finished) {
- for (const fragment of state.live) {
- const id =
- 'live-' + fragment.messageId + (fragment.field === 'thinking' ? '-thinking' : '');
- if (
- fragment.invocationId === state.run.invocationId &&
- displayed.some((message) => message.id === id)
- )
- timing.afterMessageId = id;
- }
- }
- }
- return [...turns.values()];
-}
-function intent(
- state: State,
- actions: Action[],
- request: Request,
- binding: string,
- method: string,
- input: unknown,
- resources: Action['resources'] = {},
- continuations: Action['continuations'] = [],
-) {
- const key = 'request-' + ++state.serial;
- state.pending[key] = request;
- actions.push({
- key,
- binding,
- member: 0,
- method,
- input,
- resources,
- continuations,
- ...(request.kind === 'send'
- ? {
- recovery: {
- format: 'maka.agent.draft-send@1',
- sessionId: request.sessionId,
- editor: { owner: request.draft.snapshot.text.owner, revision: request.draft.revision },
- attachments: request.attachments,
- },
- }
- : {}),
- });
-}
-function canExecute(state: State) {
- return !!state.executor || (!!state.model && !state.modelRefreshing);
-}
-function selectedBackend(state: State): Backend {
- return state.executor
- ? {
- kind: 'executor',
- selection: { ...state.executor, settings: { ...state.executor.settings } },
- }
- : {
- kind: 'model',
- model: { ...state.model!.choice.selection },
- thinkingLevel: state.model!.thinkingLevel,
- };
-}
-function createSession(state: State, actions: Action[], locale: string) {
- if (mutationPending(state) || catalogPending(state)) return;
- if (!canExecute(state)) {
- openModelSetup(state);
- return;
- }
- const title = copy(locale).new;
- if (state.selected?.archived) state.dialog = 'directories';
- else if (state.selected)
- intent(state, actions, { kind: 'create' }, 'agent', 'create', {
- title,
- labels: [],
- workspace: { kind: 'session', sessionId: state.selected.id },
- });
- else
- intent(state, actions, { kind: 'createDirectory', title }, 'agent', 'directories', {
- kind: 'roots',
- });
-}
-function openModelSetup(state: State) {
- if (state.modelSetup === 'connect' && !state.hasBackends) {
- state.pages = ['connections'];
- } else state.modelOpenRequest = ++state.serial;
-}
-function catalogPending(state: State): boolean {
- return state.catalogWatch?.loading ?? false;
-}
-function catalog(state: State, position?: AgentList['position']) {
- state.catalogFailed = false;
- const anchor = state.catalog?.sessions[0]?.id;
- state.catalogWatch = {
- key: `catalog-${++state.catalogGeneration}`,
- input: {
- filter: state.catalogFilter,
- position: position ?? (anchor ? { kind: 'at', cursor: anchor } : { kind: 'start' }),
- },
- loading: true,
- };
-}
-function observe(state: State, actions: Action[]) {
- if (state.selected)
- intent(state, actions, { kind: 'observe', epoch: state.epoch }, 'agent', 'observe', {
- sessionId: state.selected.id,
- });
-}
-function history(
- state: State,
- actions: Action[],
- position: HistoryPosition,
- through: number,
- latest: boolean,
- implicit = false,
- extend = false,
-) {
- if (!state.selected) return;
- if (!implicit && !extend) state.loading = true;
- const navigation = ++state.navigation;
- intent(
- state,
- actions,
- {
- kind: 'history',
- epoch: state.epoch,
- navigation,
- through,
- latest,
- implicit,
- position,
- extend,
- },
- 'agent',
- 'history',
- { sessionId: state.selected.id, through, position, includeThinking: true },
- {},
- implicit ? [{ child: 'timeline', method: 'tail', input: null }] : [],
- );
-}
-function follow(state: State, actions: Action[]) {
- const receipt = state.tailReceipt;
- if (
- receipt?.landing === state.landing.sequence &&
- !state.source &&
- !state.searchOpen &&
- state.browse === null &&
- !state.image &&
- !state.loading &&
- state.window?.latest &&
- (state.latestKnown > state.window.page.through ||
- state.live.some(
- (fragment) => fragment.closedAt !== null && fragment.closedAt <= state.window!.page.through,
- )) &&
- !Object.values(state.pending).some(
- (request) =>
- request.kind === 'history' &&
- request.epoch === state.epoch &&
- request.navigation === state.navigation,
- ) &&
- !(
- state.followAttempt?.through === state.latestKnown &&
- state.followAttempt.landing === receipt.landing &&
- state.followAttempt.readingRevision === receipt.readingRevision
- )
- ) {
- state.followAttempt = { ...receipt, through: state.latestKnown };
- history(state, actions, { kind: 'tail' }, state.latestKnown, true, true);
- }
-}
-function select(state: State, actions: Action[], session: AgentSession | null) {
- state.searchOpen = false;
- state.browse = null;
- state.recallAnchor = null;
- state.selected = session;
- state.epoch++;
- state.window = null;
- state.source = null;
- state.image = null;
- state.tailReceipt = null;
- state.fileContext = null;
- state.followAttempt = null;
- state.navigation++;
- state.parts = [];
- state.run = null;
- state.queue = null;
- state.interactions = null;
- state.watch = null;
- state.runThrough = 0;
- state.live = [];
- state.latestKnown = 0;
- state.loading = session !== null;
- state.notice = null;
- if (session) observe(state, actions);
-}
-function readHistorySource(state: State, actions: Action[], position: SourcePosition) {
- const source = state.source;
- if (!source || !state.selected) return;
- source.navigation = ++state.navigation;
- source.loading = true;
- source.failed = false;
- source.position = position;
- intent(
- state,
- actions,
- {
- kind: 'source',
- epoch: state.epoch,
- navigation: source.navigation,
- through: source.through,
- sequence: source.chunk.sequence,
- field: source.field,
- position,
- },
- 'agent',
- 'source',
- {
- sessionId: state.selected.id,
- through: source.through,
- source: { kind: 'sequence', sequence: source.chunk.sequence },
- field: source.field,
- position,
- },
- );
-}
-function mutationPending(state: State) {
- return (
- state.restoring ||
- state.uncertain !== null ||
- Object.values(state.pending).some((request) =>
- [
- 'read',
- 'queueRead',
- 'questionRead',
- 'respond',
- 'markRead',
- 'uploadAttachment',
- 'create',
- 'createDirectory',
- 'send',
- 'stop',
- 'queue',
- 'receipt',
- ].includes(request.kind),
- )
- );
-}
-function uploadPastedFile(state: State, actions: Action[]) {
- const paste = state.attachments.paste,
- observed = paste?.observed,
- file = observed?.selection;
- if (
- !paste ||
- !observed ||
- !file ||
- file.phase !== 'ready' ||
- !file.file ||
- paste.command ||
- mutationPending(state) ||
- sameFile(paste.attempted, file)
- )
- return;
- if (
- state.selected?.id === observed.context &&
- sameValue(state.selected.dataSource, observed.dataSource) &&
- state.selected.archived
- )
- return;
- const dataSource = observed.dataSource;
- if (state.selected && !sameValue(state.selected.dataSource, dataSource)) return;
- if (!dataSource || !draftReady(state.drafts, { id: observed.context, dataSource })) return;
- if (!attachmentCapacity(state.attachments, observed.context, dataSource)) {
- state.notice = 'attachmentLimit';
- return;
- }
- paste.attempted = selectedFile(file);
- state.notice = null;
- intent(
- state,
- actions,
- {
- kind: 'uploadAttachment',
- dataSource,
- sessionId: observed.context,
- attempt: ++state.serial,
- pasted: paste.attempted,
- },
- 'agent',
- 'uploadAttachment',
- { sessionId: observed.context, ...file.file },
- {
- file: { child: 'composer', method: 'file', input: paste.attempted },
- },
- );
-}
-function executionPending(state: State) {
- return Object.values(state.pending).some(
- (request) => request.kind === 'observe' && request.epoch === state.epoch,
- );
-}
-
-type RootCommand =
- | 'browse'
- | 'copy-file-path'
- | 'copy-message'
- | 'copy-source'
- | 'search-open'
- | 'help-open'
- | 'interaction-open'
- | 'create'
- | 'appearance-open'
- | 'model-setup'
- | 'terminal-open'
- | 'skills-open'
- | 'details-open'
- | 'usage-open'
- | 'create-workspace'
- | 'navigation-toggle'
- | 'settings'
- | 'projects-open'
- | 'imports-open'
- | 'recall-open'
- | 'scheduler-open'
- | 'apps-open'
- | 'workhub-open'
- | 'deployment-open'
- | 'reload'
- | 'stop'
- | 'check';
-function conversationRoute(state: State) {
- return state.pages.length === 0;
-}
-function reviewTarget(state: State) {
- const retained = state.interaction,
- original = state.uncertain?.request;
- // Keep an unresolved answer attached to its original review even after the
- // canonical pending list advances. Opening never repeats that answer.
- if (
- retained?.record &&
- retained.sessionId === state.selected?.id &&
- original?.kind === 'respond' &&
- original.sessionId === retained.sessionId &&
- original.attempt === retained.attempt &&
- original.requestId === retained.requestId
- )
- return {
- requestId: retained.requestId,
- kind: retained.record.request.kind,
- retained: true,
- };
- const entry = state.interactions?.entries.find(
- (entry) =>
- ['question', 'form', 'service_approval', 'permissions'].includes(entry.kind) &&
- !(
- retained?.sessionId === state.selected?.id &&
- retained?.requestId === entry.requestId &&
- retained.record?.outcome
- ),
- );
- return entry ? { ...entry, retained: false } : null;
-}
-function openInteraction(state: State, actions: Action[], requestId: string, kind: string) {
- if (!state.selected) return;
- if (
- state.interaction?.requestId !== requestId ||
- state.interaction.sessionId !== state.selected.id
- )
- state.interaction = {
- attempt: ++state.serial,
- sessionId: state.selected.id,
- requestId,
- record: null,
- notice: null,
- };
- state.dialog = kind === 'question' || kind === 'form' ? 'question' : 'approval';
- readInteraction(state, actions);
-}
-function rootCommands(
- state: State,
- locale: string,
- target: 'tui' | 'desktop',
-): (PaletteCommand & { kind: RootCommand })[] {
- const text = copy(locale);
- const shellRoute = state.dialog === null || state.dialog === 'palette';
- const route = conversationRoute(state) && shellRoute;
- const review = reviewTarget(state);
- const file = state.fileContext;
- const filePath =
- file &&
- file.sessionId === state.selected?.id &&
- file.through === state.window?.page.through &&
- file.landing === state.landing.sequence &&
- !state.loading &&
- !state.source &&
- !state.image &&
- !state.searchOpen
- ? file.path
- : null;
- const copied = state.messageCopy;
- const readyCopy =
- copied &&
- state.dialog === 'palette' &&
- copied.request === state.copyRequest &&
- copied.sessionId === state.selected?.id &&
- copied.through === state.window?.page.through &&
- copied.landing === state.landing.sequence &&
- !state.loading &&
- !state.source &&
- !state.image &&
- !state.searchOpen
- ? copied
- : null;
- const commands: (PaletteCommand & { kind: RootCommand })[] = [
- ...(target === 'tui'
- ? [
- {
- kind: 'browse' as const,
- id: 'browse',
- label: text.browseMessages,
- hint: '',
- disabled:
- !route ||
- !state.selected ||
- state.loading ||
- !!state.source ||
- !!state.image ||
- !messages(state).length,
- },
- {
- kind: 'search-open' as const,
- id: 'search-open',
- label: text.findConversation,
- hint: 'Ctrl F',
- disabled: !route || !state.selected || state.loading || !!state.source || !!state.image,
- },
- ...(['copy-message', 'copy-source'] as const).map((kind) => {
- const value = kind === 'copy-message' ? readyCopy?.message : readyCopy?.source;
- const result = readyCopy?.field === 'result',
- args = readyCopy?.field === 'arguments';
- return {
- kind,
- id: kind,
- label:
- kind === 'copy-message'
- ? text.copyMessage
- : result
- ? text.copyToolResultSource
- : args
- ? text.copyToolArgumentsSource
- : text.copySource,
- hint: value
- ? ''
- : readyCopy?.status === 'loading'
- ? text.copyLoading
- : readyCopy?.status === 'too_large'
- ? text.copyTooLarge
- : readyCopy?.status === 'empty'
- ? text.copyEmpty
- : readyCopy?.status === 'streaming'
- ? text.copyStreaming
- : text.copyUnavailable,
- disabled: !route || !value,
- ...(value ? { copyText: value } : {}),
- };
- }),
- {
- kind: 'copy-file-path' as const,
- id: 'copy-file-path',
- label: text.toolCopyPath,
- hint: filePath
- ? compactLabel(filePath.split(/[/\\]/u).filter(Boolean).at(-1) || filePath, 32)
- : '',
- disabled: !route || !filePath,
- ...(filePath ? { copyText: filePath } : {}),
- },
- ]
- : []),
- {
- kind: 'help-open',
- id: 'help-open',
- label: text.helpTitle,
- hint: 'F1',
- disabled: state.dialog !== null && state.dialog !== 'palette',
- },
- {
- kind: 'interaction-open',
- id: 'interaction-open',
- label: text.reviewRequest,
- hint: 'Ctrl/⌘ G',
- disabled:
- !route ||
- !state.selected ||
- state.selected.archived ||
- state.loading ||
- !review ||
- (mutationPending(state) && !review.retained),
- },
- {
- kind: 'model-setup',
- id: 'model-setup',
- label: state.hasBackends
- ? text.executionBackend
- : state.modelSetup === 'connect'
- ? text.configureModels
- : text.chooseModel,
- hint: '',
- disabled: !route || mutationPending(state) || executionPending(state),
- },
- {
- kind: 'create',
- id: 'create',
- label: text.new,
- hint: 'Ctrl/⌘ N',
- disabled: !shellRoute || mutationPending(state) || catalogPending(state),
- },
- {
- kind: 'create-workspace',
- id: 'create-workspace',
- label: text.newWorkspace,
- hint: '',
- disabled: !shellRoute || mutationPending(state) || catalogPending(state),
- },
- ...(state.skills
- ? [
- {
- kind: 'skills-open' as const,
- id: 'skills-open',
- label: text.skillsTitle,
- hint: '',
- disabled: !route,
- },
- ]
- : []),
- {
- kind: 'terminal-open',
- id: 'terminal-open',
- label: text.terminalTitle,
- hint: '',
- disabled: !route || !state.selected || state.loading,
- },
- ...(target === 'tui'
- ? [
- {
- kind: 'usage-open' as const,
- id: 'usage-open',
- label: tuiText(locale, 'insights-this-session'),
- hint: '',
- disabled: !route || !state.selected,
- },
- ]
- : []),
- {
- kind: 'details-open',
- id: 'details-open',
- label: text.detailsTitle,
- hint: '',
- disabled:
- !route ||
- !state.selected ||
- state.loading ||
- executionPending(state) ||
- mutationPending(state) ||
- catalogPending(state),
- },
- {
- kind: 'navigation-toggle',
- id: 'navigation-toggle',
- label: sidebarLabel(state, text),
- hint: 'Ctrl/⌘ B',
- disabled: !shellRoute,
- },
- ...(target === 'tui'
- ? [
- {
- kind: 'appearance-open' as const,
- id: 'appearance-open',
- label: tuiText(locale, 'settings-appearance'),
- hint: '',
- disabled: !shellRoute,
- },
- ]
- : []),
- ...(state.embedding.workhub && !state.embedding.fixed
- ? [
- {
- kind: 'workhub-open' as const,
- id: 'workhub-open',
- label: 'WorkHub',
- hint: '',
- disabled: !shellRoute,
- },
- ]
- : []),
- {
- kind: 'apps-open',
- id: 'apps-open',
- label: text.appsTitle,
- hint: '',
- disabled: !shellRoute || !!state.embedding.fixed,
- },
- {
- kind: 'scheduler-open',
- id: 'scheduler-open',
- label: text.scheduleTitle,
- hint: '',
- disabled: !shellRoute || !!state.embedding.fixed || !state.scheduledTasks,
- },
- {
- kind: 'recall-open',
- id: 'recall-open',
- label: text.recallTitle,
- hint: '',
- disabled: !shellRoute || !!state.embedding.fixed,
- },
- {
- kind: 'imports-open',
- id: 'imports-open',
- label: text.importsTitle,
- hint: '',
- disabled: !shellRoute || !!state.embedding.fixed,
- },
- {
- kind: 'projects-open',
- id: 'projects-open',
- label: text.projectsTitle,
- hint: '',
- disabled: !shellRoute || !!state.embedding.fixed,
- },
- {
- kind: 'settings',
- id: 'settings',
- label: target === 'tui' ? tuiText(locale, 'route-settings') : text.settings,
- hint: '',
- disabled: !shellRoute,
- },
- {
- kind: 'deployment-open',
- id: 'deployment-open',
- label: text.deploymentTitle,
- hint: '',
- disabled: !shellRoute,
- },
- {
- kind: 'reload',
- id: `reload:${state.epoch}`,
- label: text.refreshSessions,
- hint: '',
- disabled: !shellRoute || state.loading,
- },
- ];
- if (state.run)
- commands.push({
- kind: 'stop',
- id: `stop:${state.epoch}:${state.run.invocationId}`,
- label: text.stop,
- hint: '',
- disabled: !route || executionPending(state) || mutationPending(state) || !state.selected,
- });
- if (state.uncertain)
- commands.push({
- kind: 'check',
- id: `check:${state.uncertain.reference.operation}:${state.uncertain.reference.fingerprint}`,
- label: text.check,
- hint: '',
- disabled:
- !route || Object.values(state.pending).some((request) => request.kind === 'receipt'),
- });
- return state.embedding.fixed
- ? commands.filter(
- (command) => !['create', 'create-workspace', 'navigation-toggle'].includes(command.kind),
- )
- : commands;
-}
-function openUsage(state: State, session: AgentSession) {
- state.insightsScope = { sequence: ++state.serial, sessionId: session.id, label: session.title };
- state.settingsCategory = 'insights';
- state.pages = ['settings'];
- controlSidebar(state, 'dismiss_transient');
-}
-function sidebarLabel(state: State, text: ReturnType<typeof copy>) {
- return 'open' in state.sidebar
- ? state.sidebar.open
- ? text.hideSidebar
- : text.showSidebar
- : text.toggleSidebar;
-}
-function controlSidebar(state: State, op: TerminalPaneCommand['op']) {
- if ('open' in state.sidebar) {
- if (op === 'toggle') state.sidebar.open = !state.sidebar.open;
- } else {
- state.sidebar.command = { sequence: (state.sidebar.command?.sequence ?? 0) + 1, op };
- }
-}
-function rootCommand(
- state: State,
- actions: Action[],
- locale: string,
- kind: RootCommand,
- target: 'tui' | 'desktop',
-) {
- if (
- !rootCommands(state, locale, target).some(
- (command) => command.kind === kind && !command.disabled,
- )
- )
- return false;
- if (kind === 'browse') {
- state.searchOpen = false;
- state.browse = ++state.serial;
- state.tailReceipt = null;
- } else if (kind === 'search-open') {
- state.searchOpen = true;
- state.browse = null;
- } else if (kind === 'help-open') state.dialog = 'help';
- else if (kind === 'interaction-open') {
- const review = reviewTarget(state)!;
- openInteraction(state, actions, review.requestId, review.kind);
- } else if (kind === 'create') {
- state.pages = [];
- createSession(state, actions, locale);
- } else if (kind === 'appearance-open') state.preferences.open = true;
- else if (kind === 'model-setup') openModelSetup(state);
- else if (kind === 'create-workspace') {
- state.pages = [];
- if (!canExecute(state)) openModelSetup(state);
- else state.dialog = 'directories';
- if (target === 'tui') controlSidebar(state, 'dismiss_transient');
- } else if (kind === 'terminal-open') state.dialog = 'terminal';
- else if (kind === 'skills-open') state.dialog = 'skills';
- else if (kind === 'details-open') state.dialog = 'details';
- else if (kind === 'usage-open' && state.selected) openUsage(state, state.selected);
- else if (kind === 'navigation-toggle') controlSidebar(state, 'toggle');
- else if (kind === 'settings') {
- state.insightsScope = { sequence: ++state.serial, sessionId: null, label: '' };
- state.pages = [target === 'tui' ? 'settings' : 'connections'];
- if (target === 'tui') controlSidebar(state, 'dismiss_transient');
- } else if (kind === 'workhub-open') {
- if (target === 'tui') controlSidebar(state, 'dismiss_transient');
- } else if (kind === 'apps-open') {
- state.pages = ['apps'];
- if (target === 'tui') controlSidebar(state, 'dismiss_transient');
- } else if (kind === 'scheduler-open') {
- state.pages = ['scheduler'];
- if (!state.scheduleWatch) state.scheduleWatch = 'schedule-alerts-' + ++state.serial;
- if (target === 'tui') controlSidebar(state, 'dismiss_transient');
- } else if (kind === 'recall-open') {
- state.pages = ['recall'];
- if (target === 'tui') controlSidebar(state, 'dismiss_transient');
- } else if (kind === 'imports-open') {
- state.pages = ['imports'];
- if (target === 'tui') controlSidebar(state, 'dismiss_transient');
- } else if (kind === 'projects-open') {
- state.pages = ['projects'];
- if (target === 'tui') controlSidebar(state, 'dismiss_transient');
- } else if (kind === 'deployment-open') {
- state.pages = ['deployment'];
- if (target === 'tui') controlSidebar(state, 'dismiss_transient');
- } else if (kind === 'stop' && state.selected && state.run)
- intent(state, actions, { kind: 'stop', sessionId: state.selected.id }, 'agent', 'stop', {
- sessionId: state.selected.id,
- invocationId: state.run.invocationId,
- });
- else if (kind === 'check' && state.uncertain)
- intent(state, actions, { kind: 'receipt' }, 'receipts', 'read', state.uncertain.reference);
- else if (kind === 'reload') {
- if (state.restoring)
- intent(state, actions, { kind: 'restorePage' }, 'history', 'page', {
- after: state.restoreAfter,
- limit: 1,
- });
- else if (!state.selected && state.embedding.session) state.externalSelection = null;
- else if (!state.selected) catalog(state);
- else {
- const window = state.window;
- select(state, actions, state.selected);
- state.window = window;
- }
- }
- return true;
-}
-
-function loadQueueEdit(state: State, actions: Action[], target: AgentQueueMessage) {
- const attempt = ++state.serial;
- state.queueEdit = {
- attempt,
- target,
- phase: 'loading',
- hasAttachments: false,
- body: null,
- command: null,
- };
- state.dialog = 'queue';
- state.notice = null;
- intent(state, actions, { kind: 'queueBody', attempt }, 'agent', 'queueMessage', target);
-}
-function initializeQueueEdit(state: State) {
- const edit = state.queueEdit,
- draft = state.queueDraft;
- if (!edit || edit.body === null || !draft || edit.command) return;
- edit.command = {
- owner: draft.snapshot.text.owner,
- sequence: ++state.serial,
- basedOn: draft.revision,
- edit: { kind: 'reset', text: edit.body },
- };
- edit.body = null;
-}
-function questionChild(state: State) {
- return `question-form-${state.interaction?.attempt ?? 0}`;
-}
-function readInteraction(state: State, actions: Action[]) {
- const question = state.interaction;
- if (
- !question ||
- Object.values(state.pending).some(
- (r) => r.kind === 'interaction' && r.attempt === question.attempt,
- )
- )
- return;
- intent(
- state,
- actions,
- {
- kind: 'interaction',
- attempt: question.attempt,
- sessionId: question.sessionId,
- requestId: question.requestId,
- },
- 'agent',
- 'interaction',
- { sessionId: question.sessionId, requestId: question.requestId },
- );
-}
-function respondQuestion(state: State, actions: Action[]) {
- const submission = state.questionSubmission,
- question = state.interaction;
- if (!submission || submission.waiting.length) return;
- state.questionSubmission = null;
- if (
- !question ||
- submission.attempt !== question.attempt ||
- state.dialog !== 'question' ||
- state.selected?.id !== question.sessionId ||
- state.selected.archived ||
- !question.record ||
- question.record.outcome
- )
- return;
- respondInteraction(state, actions, { kind: 'question', answers: submission.answers });
-}
-function respondInteraction(state: State, actions: Action[], answer: InteractionAnswer) {
- const interaction = state.interaction;
- if (
- !interaction?.record ||
- interaction.record.outcome ||
- interaction.record.request.kind !== answer.kind ||
- state.selected?.id !== interaction.sessionId ||
- state.selected.archived ||
- mutationPending(state)
- )
- return;
- intent(
- state,
- actions,
- {
- kind: 'respond',
- attempt: interaction.attempt,
- sessionId: interaction.sessionId,
- requestId: interaction.requestId,
- },
- 'agent',
- 'respond',
- {
- sessionId: interaction.sessionId,
- requestId: interaction.requestId,
- answer,
- },
- );
-}
-function submitQuestion(
- state: State,
- actions: Action[],
- event: { requestId: string; answers: QuestionDraft[] },
-) {
- const question = state.interaction;
- if (
- !question ||
- !question.record ||
- question.record.request.kind !== 'question' ||
- question.record.outcome ||
- state.dialog !== 'question' ||
- state.selected?.id !== question.sessionId ||
- state.selected.archived ||
- event.requestId !== question.requestId ||
- mutationPending(state)
- )
- return;
- const questions = question.record.request.questions;
- if (event.answers.length !== questions.length) return;
- const answers: (string | null)[] = [],
- waiting: number[] = [];
- for (const [index, answer] of event.answers.entries()) {
- if (answer.kind === 'skip') answers.push(null);
- else if (answer.kind === 'option' && questions[index].options[answer.index])
- answers.push(questions[index].options[answer.index].label);
- else if (
- answer.kind === 'text' &&
- !answer.observation.snapshot.marked &&
- answer.observation.snapshot.text.bytes > 0 &&
- answer.observation.snapshot.text.bytes <= 2048
- ) {
- answers.push(null);
- waiting.push(index);
- } else {
- question.notice = 'questionInvalid';
- return;
- }
- }
- question.notice = null;
- state.questionSubmission = { attempt: question.attempt, answers, waiting };
- for (const index of waiting) {
- const answer = event.answers[index];
- if (answer.kind !== 'text') continue;
- const draft = answer.observation;
- intent(
- state,
- actions,
- { kind: 'questionRead', attempt: question.attempt, index, draft },
- 'reader',
- 'read',
- { start: 0, end: draft.snapshot.text.bytes },
- {
- text: {
- child: questionChild(state),
- method: 'snapshot',
- input: {
- requestId: question.requestId,
- index,
- snapshot: {
- kind: 'at_revision',
- owner: draft.snapshot.text.owner,
- revision: draft.snapshot.text.revision,
- },
- },
- },
- },
- );
- }
- respondQuestion(state, actions);
-}
-function checkInteraction(state: State, actions: Action[]) {
- if (state.uncertain && !Object.values(state.pending).some((r) => r.kind === 'receipt'))
- intent(state, actions, { kind: 'receipt' }, 'receipts', 'read', state.uncertain.reference);
- readInteraction(state, actions);
-}
-function focusAttachments(state: State) {
- const selection = state.attachments.selection;
- if (!selection || selection.browse) return;
- const files = attachmentFiles(state.attachments, selection.sessionId, selection.dataSource);
- const index = files.findIndex((file) => file.id === selection.selected);
- selection.focus = {
- sequence: ++state.serial,
- target: {
- kind: 'focus_item',
- key: index < 0 ? 'attachment-browse' : 'attachment-select-' + index,
- },
- };
-}
-function closeDialog(state: State) {
- // Cancel follows the original queue sheet: discard this edit, not an accepted operation.
- if (state.dialog === 'queue') state.queueEdit = null;
- if (state.dialog === 'question') state.questionSubmission = null;
- state.dialog = null;
-}
-function settled(state: State, actions: Action[], request: Mutation, value: unknown) {
- state.uncertain = null;
- if (request.kind === 'restored') {
- state.restoring = true;
- intent(state, actions, { kind: 'restorePage' }, 'history', 'page', {
- after: state.restoreAfter,
- limit: 1,
- });
- catalog(state);
- } else if (request.kind === 'create') {
- const created = value as AgentCreated;
- if (created.kind === 'rejected') {
- if (state.notice !== 'unavailable') state.notice = 'createRejected';
- return;
- }
- // The live catalog may have selected this first conversation before its
- // original creation receipt arrived. Keep that observation and its owners.
- if (state.selected?.id !== created.session.id) select(state, actions, created.session);
- else if (created.session.revision >= state.selected.revision) state.selected = created.session;
- // Explicit creation reveals its selected row; background catalog changes
- // continue to retain the current directory location.
- if (
- state.catalogFilter !== 'all' ||
- !state.catalog?.sessions.some((session) => session.id === created.session.id)
- ) {
- state.catalogFilter = 'all';
- catalog(state, { kind: 'at', cursor: created.session.id });
- }
- } else if (request.kind === 'markRead') {
- const result = value as AgentMarkReadResult;
- if (result.kind === 'observed' && result.session.id === request.sessionId) {
- if (
- state.selected?.id === request.sessionId &&
- result.session.revision >= state.selected.revision
- )
- state.selected = result.session;
- if (state.catalog)
- state.catalog.sessions = state.catalog.sessions.map((session) =>
- session.id === request.sessionId && result.session.revision >= session.revision
- ? result.session
- : session,
- );
- state.notice = null;
- } else state.notice = 'unavailable';
- } else if (request.kind === 'queue') {
- const result = value as AgentQueueMutationResult;
- state.notice = result.kind === 'committed' ? null : 'queueChanged';
- if (
- result.kind === 'committed' &&
- request.saved &&
- state.queueEdit?.attempt === request.saved.attempt &&
- state.queueDraft?.snapshot.text.owner === request.saved.draft.snapshot.text.owner &&
- state.queueDraft.revision === request.saved.draft.revision
- )
- closeDialog(state);
- } else if (request.kind === 'respond') {
- const result = value as AgentInteractionResult;
- const question = state.interaction;
- if (question?.attempt === request.attempt) {
- if (
- result.kind === 'recorded' &&
- result.record.sessionId === request.sessionId &&
- result.record.requestId === request.requestId &&
- sameValue(result.record.request, question.record?.request)
- ) {
- question.record = result.record;
- question.notice = null;
- } else
- question.notice =
- result.kind === 'rejected' &&
- result.reason === 'invalid_answer' &&
- question.record?.request.kind === 'form'
- ? 'formInvalid'
- : 'interactionChanged';
- }
- if (state.selected?.id === request.sessionId) observe(state, actions);
- } else if (request.kind === 'uploadAttachment') {
- const result = value as AgentAttachmentUploadResult;
- if (result.kind === 'uploaded') {
- if (
- !state.attachments.files.some(
- (file) =>
- file.id === result.attachment.id &&
- file.sessionId === request.sessionId &&
- sameValue(file.dataSource, request.dataSource),
- )
- )
- state.attachments.files.push({ ...result.attachment, dataSource: request.dataSource });
- if (
- request.pasted &&
- state.attachments.paste &&
- sameFile(request.pasted, state.attachments.paste.observed.selection)
- ) {
- state.attachments.paste.command = { selection: request.pasted, action: 'next' };
- }
- if (!request.pasted && state.attachments.selection?.attempt === request.attempt) {
- if (state.dialog === 'attachments') state.dialog = null;
- state.attachments.selection = null;
- }
- state.notice = null;
- } else state.notice = 'attachmentFailed';
- } else if (request.kind === 'send') {
- const started = value as AgentSendResult;
- if (started.sessionId !== request.sessionId) {
- state.notice = 'unknown';
- return;
- }
- if (started.kind === 'rejected') {
- state.notice =
- started.reason === 'input_too_large' ? 'skillInputTooLarge' : 'skillInputUnavailable';
- return;
- }
- state.attachments.files = state.attachments.files.filter(
- (file) =>
- file.sessionId !== request.sessionId ||
- !sameValue(file.dataSource, request.dataSource) ||
- !request.attachments.includes(file.id),
- );
- // Clear only the exact captured draft after a durable returned admission.
- // The native editor rejects this command if a newer draft or IME composition exists.
- state.command = {
- owner: request.draft.snapshot.text.owner,
- sequence: ++state.commandSequence,
- basedOn: request.draft.revision,
- edit: { kind: 'reset', text: '' },
- };
- state.notice = null;
- if (state.selected?.id === started.sessionId) {
- observe(state, actions);
- }
- } else {
- state.notice = 'stopping';
- if (state.selected?.id === request.sessionId) observe(state, actions);
- }
-}
-function receipt(state: State, actions: Action[], request: Mutation, reply: OperationReply) {
- if (reply.receipt.kind === 'recorded' && reply.receipt.outcome.kind === 'returned') {
- const value = reply.receipt.outcome.value;
- const returned = value as AgentAttachmentUploadResult;
- if (
- (request.kind === 'send' && (value as AgentSendResult).sessionId !== request.sessionId) ||
- (request.kind === 'uploadAttachment' &&
- returned.kind === 'uploaded' &&
- returned.attachment.sessionId !== request.sessionId)
- ) {
- state.uncertain = { reference: reply.reference, request };
- state.notice = 'unknown';
- return;
- }
- settled(state, actions, request, value);
- } else {
- state.uncertain = { reference: reply.reference, request };
- state.notice = 'unknown';
- }
-}
-function queryFailed(state: State, request: Request) {
- if (request.kind === 'draftPage' || request.kind === 'draftReceipt') {
- draftFailed(state.drafts, request);
- return;
- }
- if (
- request.kind === 'interaction' ||
- request.kind === 'questionRead' ||
- request.kind === 'respond'
- ) {
- if (state.interaction?.attempt === request.attempt)
- state.interaction.notice =
- request.kind === 'questionRead'
- ? 'questionInvalid'
- : request.kind === 'respond'
- ? 'interactionUnknown'
- : 'interactionUnavailable';
- if (state.questionSubmission?.attempt === request.attempt) state.questionSubmission = null;
- return;
- }
- if (request.kind === 'queueBody' || request.kind === 'queueRead') {
- if (state.queueEdit?.attempt === request.attempt) {
- if (request.kind === 'queueBody') state.queueEdit.phase = 'failed';
- state.notice = 'draftKept';
- }
- return;
- }
- // Optional presentation work does not disable input or change execution facts.
- if (request.kind === 'history' && request.implicit) return;
- if (request.kind === 'source') {
- if (request.epoch === state.epoch && request.navigation === state.source?.navigation) {
- state.source.loading = false;
- state.source.failed = true;
- }
- } else if (request.kind === 'history' || request.kind === 'observe') {
- if (
- request.epoch === state.epoch &&
- (request.kind !== 'history' || request.navigation === state.navigation)
- ) {
- state.loading = false;
- state.notice = 'unavailable';
- }
- } else {
- if (request.kind === 'openSession') {
- if (sameValue(request.selection, state.externalSelection)) {
- state.loading = false;
- state.notice = 'unavailable';
- }
- } else if (request.kind === 'createDirectory') {
- if (state.notice !== 'unavailable') state.notice = 'createRejected';
- } else if (request.kind === 'restoreReceipt') {
- state.uncertain = { reference: request.reference, request: { kind: 'restored' } };
- state.restoring = false;
- state.notice = 'unknown';
- } else
- state.notice =
- request.kind === 'read'
- ? 'draftKept'
- : request.kind === 'uploadAttachment'
- ? 'attachmentFailed'
- : 'unavailable';
- }
-}
-function querySucceeded(state: State, actions: Action[], request: Request, value: unknown) {
- switch (request.kind) {
- case 'openSession': {
- if (!sameValue(request.selection, state.externalSelection)) break;
- const observed = value as AgentObservation;
- if (observed.session.id !== request.selection.id) {
- state.loading = false;
- state.notice = 'unavailable';
- break;
- }
- select(state, actions, observed.session);
- break;
- }
- case 'interaction': {
- const question = state.interaction,
- record = value as InteractionRecord;
- if (!question || question.attempt !== request.attempt) break;
- if (
- record.sessionId !== request.sessionId ||
- record.requestId !== request.requestId ||
- !['question', 'form', 'service_approval', 'permissions'].includes(record.request.kind) ||
- (question.record && !sameValue(question.record.request, record.request))
- ) {
- question.notice = 'interactionUnavailable';
- break;
- }
- if (!question.record?.outcome) question.record = record;
- question.notice = null;
- break;
- }
- case 'questionRead': {
- const submission = state.questionSubmission,
- question = state.interaction;
- if (
- !submission ||
- !question ||
- submission.attempt !== request.attempt ||
- question.attempt !== request.attempt
- )
- break;
- const chunk = value as TextChunk;
- if (
- !sameValue(chunk.metadata, request.draft.snapshot.text) ||
- utf8Bytes(chunk.text) !== request.draft.snapshot.text.bytes ||
- !chunk.text.trim()
- ) {
- state.questionSubmission = null;
- question.notice = 'questionInvalid';
- break;
- }
- submission.answers[request.index] = chunk.text;
- submission.waiting = submission.waiting.filter((index) => index !== request.index);
- respondQuestion(state, actions);
- break;
- }
- case 'createDirectory': {
- const page = value as AgentDirectoryPage;
- if (page.kind !== 'roots' || !page.roots.length) {
- state.notice = 'createRejected';
- break;
- }
- intent(state, actions, { kind: 'create' }, 'agent', 'create', {
- title: request.title,
- labels: [],
- workspace: { kind: 'directory', selection: page.roots[0].selection },
- });
- break;
- }
- case 'observe': {
- if (request.epoch !== state.epoch) break;
- const observation = value as AgentObservation;
- if (observation.session.id !== state.selected?.id) {
- state.loading = false;
- state.notice = 'unavailable';
- break;
- }
- if (observation.session.revision >= state.selected!.revision)
- state.selected = observation.session;
- if (
- observation.through >= state.runThrough &&
- observation.session.revision >= state.selected!.revision
- ) {
- state.run = observation.run ?? null;
- state.runThrough = observation.through;
- }
- if (!state.watch)
- state.parts = observation.parts.map((part) => ({
- ...part,
- invocationId: observation.run?.invocationId ?? null,
- }));
- state.latestKnown = Math.max(state.latestKnown, observation.through);
- // Watch/run facts are independent of the fixed-fence reading window.
- if (!state.watch) {
- const after = Math.min(
- observation.through,
- ...state.parts.map((part) => part.startSequence - 1),
- );
- state.watch = {
- key: 'watch-' + state.epoch,
- input: { sessionId: state.selected!.id, after },
- last: after,
- };
- }
- if (
- !state.window &&
- !Object.values(state.pending).some(
- (pending) => pending.kind === 'history' && pending.epoch === state.epoch,
- )
- ) {
- const anchor = state.recallAnchor;
- state.recallAnchor = null;
- history(
- state,
- actions,
- anchor === null
- ? { kind: 'tail' }
- : { kind: 'after', cursor: { sequence: anchor, offset: 0, field: 'body' } },
- observation.through,
- anchor === null,
- );
- } else if (
- !Object.values(state.pending).some(
- (pending) =>
- pending.kind === 'history' &&
- pending.epoch === state.epoch &&
- pending.navigation === state.navigation,
- )
- )
- state.loading = false;
- follow(state, actions);
- break;
- }
- case 'source': {
- const source = state.source;
- if (
- request.epoch !== state.epoch ||
- !source ||
- request.navigation !== source.navigation ||
- request.field !== source.field
- )
- break;
- const page = value as AgentHistorySourcePage;
- if (page.through !== request.through) {
- queryFailed(state, request);
- break;
- }
- if (page.kind === 'preparing') {
- intent(state, actions, request, 'agent', 'source', {
- sessionId: state.selected!.id,
- through: request.through,
- source: { kind: 'sequence', sequence: request.sequence },
- field: request.field,
- position: request.position,
- });
- break;
- }
- const next = page.source;
- const end = next.offset + utf8Bytes(next.text);
- if (
- next.sequence !== request.sequence ||
- next.path != null ||
- next.field !== request.field ||
- !next.fields.includes(next.field) ||
- (!next.text && next.totalBytes > 0) ||
- end > next.totalBytes ||
- (source.ready &&
- (next.totalBytes !== source.chunk.totalBytes || next.format !== source.format)) ||
- (request.position.kind === 'after' && next.offset !== request.position.offset) ||
- (request.position.kind === 'before' && end !== request.position.offset) ||
- (request.position.kind === 'start' && next.offset !== 0) ||
- (request.position.kind === 'tail' && end !== next.totalBytes)
- ) {
- queryFailed(state, request);
- break;
- }
- source.chunk = {
- ...source.chunk,
- sequence: next.sequence,
- offset: next.offset,
- totalBytes: next.totalBytes,
- text: next.text,
- };
- source.fields = next.fields;
- source.format = next.format;
- source.ready = true;
- source.loading = false;
- source.failed = false;
- source.landing = {
- sequence: request.navigation,
- edge:
- request.position.kind === 'before' || request.position.kind === 'tail' ? 'end' : 'start',
- };
- break;
- }
- case 'history': {
- if (request.epoch !== state.epoch || request.navigation !== state.navigation) break;
- const page = value as AgentHistoryPage;
- if (page.through !== request.through) {
- queryFailed(state, request);
- break;
- }
- if (page.kind === 'preparing') {
- intent(
- state,
- actions,
- request,
- 'agent',
- 'history',
- {
- sessionId: state.selected!.id,
- through: request.through,
- position: request.position,
- includeThinking: true,
- },
- {},
- request.implicit ? [{ child: 'timeline', method: 'tail', input: null }] : [],
- );
- break;
- }
- const old = state.window;
- const splitMessage = old?.page.chunks.some((oldChunk) =>
- page.chunks.some(
- (chunk) => chunk.sequence === oldChunk.sequence && chunk.field === oldChunk.field,
- ),
- );
- if (request.extend && old?.page.through === page.through && !splitMessage) {
- const before = request.position.kind === 'before';
- const chunks = before
- ? [...page.chunks, ...old.page.chunks]
- : [...old.page.chunks, ...page.chunks];
- let bytes = chunks.reduce((total, chunk) => total + utf8Bytes(chunk.text), 0);
- let first = before ? page.before : old.page.before;
- let last = before ? old.page.after : page.after;
- // Adjacent canonical pages share one fence. Retain the displayed edge
- // and its native anchor. Keep two complete pages so loading an adjacent
- // page cannot evict the message that anchors the current viewport.
- while (chunks.length > 16 || bytes > 131072) {
- const removed = before ? chunks.pop()! : chunks.shift()!;
- bytes -= utf8Bytes(removed.text);
- if (before)
- last = { sequence: removed.sequence, offset: removed.offset, field: removed.field };
- else {
- const start = chunks[0]!;
- first = { sequence: start.sequence, offset: start.offset, field: start.field };
- }
- }
- state.window = {
- page: { ...page, chunks, before: first, after: last },
- latest: last === null,
- };
- } else {
- state.window = { page, latest: request.latest };
- state.landing = {
- sequence: state.landing.sequence + 1,
- edge: request.position.kind === 'before' || request.latest ? 'end' : 'start',
- };
- }
- state.loading = false;
- if (!request.implicit) state.notice = null;
- state.live = state.live.filter(
- (fragment) =>
- fragment.sequence > page.through ||
- ((fragment.closedAt === null || fragment.closedAt > page.through) &&
- !page.chunks.some(
- (chunk) =>
- chunk.messageId === fragment.messageId &&
- (chunk.field ?? 'body') === fragment.field,
- )),
- );
- break;
- }
- case 'queueBody': {
- const edit = state.queueEdit;
- if (!edit || edit.attempt !== request.attempt) break;
- const body = value as AgentQueueMessageResult;
- if (body.kind !== 'ready' || body.revision !== edit.target.expectedRevision) {
- edit.phase = 'failed';
- state.notice = 'queueChanged';
- break;
- }
- edit.body = body.message;
- edit.hasAttachments = body.hasAttachments;
- edit.phase = 'initializing';
- initializeQueueEdit(state);
- break;
- }
- case 'queueRead': {
- const edit = state.queueEdit;
- if (!edit || edit.attempt !== request.attempt || state.dialog !== 'queue') break;
- const chunk = value as TextChunk,
- captured = request.draft.snapshot.text;
- if (!sameValue(chunk.metadata, captured) || utf8Bytes(chunk.text) !== captured.bytes) {
- state.notice = 'draftKept';
- break;
- }
- if (!chunk.text.trim() && !edit.hasAttachments) {
- state.notice = 'empty';
- break;
- }
- intent(
- state,
- actions,
- {
- kind: 'queue',
- sessionId: edit.target.sessionId,
- saved: { attempt: edit.attempt, draft: request.draft },
- },
- 'agent',
- 'editQueue',
- {
- sessionId: edit.target.sessionId,
- expectedRevision: edit.target.expectedRevision,
- edit: { kind: 'update', messageId: edit.target.messageId, message: chunk.text },
- },
- );
- break;
- }
- case 'read': {
- if (state.selected?.id === request.sessionId && state.selected.archived) {
- state.notice = 'draftKept';
- break;
- }
- const chunk = value as TextChunk;
- const metadata = chunk.metadata as { owner?: string; revision?: number; bytes?: number };
- const captured = request.draft.snapshot.text;
- if (
- !metadata ||
- metadata.owner !== captured.owner ||
- metadata.revision !== captured.revision ||
- metadata.bytes !== captured.bytes
- ) {
- state.notice = 'draftKept';
- break;
- }
- if (!chunk.text.trim() && request.attachments.length === 0) {
- state.notice = 'empty';
- break;
- }
- intent(
- state,
- actions,
- {
- kind: 'send',
- dataSource: request.dataSource,
- attachments: request.attachments,
- draft: request.draft,
- sessionId: request.sessionId,
- backend: request.backend,
- },
- 'agent',
- request.backend.kind === 'executor' ? 'execute' : 'send',
- request.backend.kind === 'executor'
- ? {
- sessionId: request.sessionId,
- message: chunk.text,
- executor: request.backend.selection,
- attachments: request.attachments,
- }
- : {
- sessionId: request.sessionId,
- message: chunk.text,
- attachments: request.attachments,
- model: request.backend.model,
- thinkingLevel: request.backend.thinkingLevel,
- },
- );
- break;
- }
- case 'draftPage': {
- draftPage(state.drafts, request, value as CallerIntent[]);
- break;
- }
- case 'draftReceipt': {
- draftReceipt(state.drafts, request, value as OperationReply);
- break;
- }
- case 'restorePage': {
- const page = value as CallerIntent[];
- if (!page.length) {
- state.restoring = false;
- break;
- }
- const original = page[0];
- state.restoreAfter = original.cursor;
- intent(
- state,
- actions,
- { kind: 'restoreReceipt', reference: original.reference, cursor: original.cursor },
- 'receipts',
- 'read',
- original.reference,
- );
- break;
- }
- case 'restoreReceipt': {
- const reply = value as OperationReply;
- if (!sameValue(request.reference, reply.reference)) {
- state.uncertain = { reference: request.reference, request: { kind: 'restored' } };
- state.notice = 'unknown';
- state.restoring = false;
- break;
- }
- if (reply.receipt.kind === 'recorded' && reply.receipt.outcome.kind === 'returned')
- intent(state, actions, { kind: 'restorePage' }, 'history', 'page', {
- after: request.cursor,
- limit: 1,
- });
- else {
- state.uncertain = { reference: request.reference, request: { kind: 'restored' } };
- state.notice = 'unknown';
- state.restoring = false;
- }
- break;
- }
- case 'receipt': {
- const original = state.uncertain;
- const reply = value as OperationReply;
- if (!original || !sameValue(original.reference, reply.reference)) {
- state.notice = 'unknown';
- break;
- }
- receipt(state, actions, original.request, reply);
- break;
- }
- default:
- state.notice = 'unknown';
- }
-}
-function delivery(state: State, page: AgentDeliveryPage, actions: Action[]) {
- if (page.kind === 'interactions') {
- if (!state.interactions || page.pending.revision > state.interactions.revision) {
- const changed =
- !state.interactions || !sameValue(state.interactions.entries, page.pending.entries);
- state.interactions = page.pending;
- if (changed) observe(state, actions);
- const question = state.interaction;
- if (
- question?.record &&
- !question.record.outcome &&
- question.sessionId === state.selected?.id &&
- !page.pending.entries.some((entry) => entry.requestId === question.requestId)
- )
- readInteraction(state, actions);
- }
- return;
- }
- if (page.kind === 'queue') {
- if (!state.queue || page.queue.revision > state.queue.revision) state.queue = page.queue;
- return;
- }
- const watch = state.watch;
- if (!watch) return;
- state.latestKnown = Math.max(state.latestKnown, page.through);
- let canonicalChanged = false;
- for (const event of page.events) {
- if (event.sequence <= watch.last) continue;
- const fact = event.fact;
- if (
- [
- 'opened',
- 'executor_started',
- 'transcript_changed',
- 'part_finished',
- 'step_ended',
- 'ended',
- ].includes(fact.kind)
- )
- canonicalChanged = true;
- if (fact.kind === 'opened') {
- if (event.sequence > state.runThrough) {
- state.run = { invocationId: event.invocationId, turnId: event.turnId, state: 'running' };
- state.runThrough = event.sequence;
- }
- } else if (fact.kind === 'part_started' || fact.kind === 'executor_started') {
- const started: DisplayPart[] =
- fact.kind === 'executor_started'
- ? (['text', 'thinking'] as const).map((textKind) => ({
- startSequence: event.sequence,
- messageId: fact.messageId,
- stepId: event.invocationId,
- partId: textKind,
- textKind,
- invocationId: event.invocationId,
- }))
- : [
- {
- startSequence: event.sequence,
- messageId: fact.messageId,
- stepId: fact.stepId,
- partId: fact.partId,
- textKind: fact.textKind,
- invocationId: event.invocationId,
- },
- ];
- state.parts = state.parts.filter(
- (part) =>
- !started.some(
- (next) =>
- part.invocationId === next.invocationId &&
- part.stepId === next.stepId &&
- part.partId === next.partId,
- ),
- );
- if (state.parts.length + started.length > 128) {
- state.watch = null;
- state.notice = 'unavailable';
- return;
- }
- state.parts.push(...started);
- } else if (fact.kind === 'delta') {
- const part = state.parts.find(
- (part) =>
- part.invocationId === event.invocationId &&
- part.stepId === fact.stepId &&
- part.partId === fact.partId,
- );
- if (part) {
- appendLive(state, event.sequence, part, event.invocationId, fact.text);
- }
- } else if (fact.kind === 'part_finished') {
- state.parts = state.parts.filter(
- (part) =>
- part.invocationId !== event.invocationId ||
- part.stepId !== fact.stepId ||
- part.partId !== fact.partId,
- );
- } else if (fact.kind === 'step_ended') {
- closeLive(state, event.sequence, event.invocationId, fact.stepId);
- state.parts = state.parts.filter(
- (part) => part.invocationId !== event.invocationId || part.stepId !== fact.stepId,
- );
- } else if (fact.kind === 'ended') {
- closeLive(state, event.sequence, event.invocationId);
- state.parts = state.parts.filter((part) => part.invocationId !== event.invocationId);
- if (event.sequence > state.runThrough && state.run?.invocationId === event.invocationId) {
- state.run = null;
- state.runThrough = event.sequence;
- state.notice = fact.failed ? 'failed' : 'completed';
- }
- }
- }
- watch.last = Math.max(watch.last, page.nextAfter);
- if (canonicalChanged) follow(state, actions);
-}
-function frame<N extends Node>(
- presentation: Presentation<N>,
- locale: string,
- state: State,
- values: ComponentValues,
- actions: Action[] = [],
- events: Emission[] = [],
-) {
- const target = presentation.target;
- const currentPage = state.pages.at(-1);
- if (currentPage && !state.openedPages.includes(currentPage)) state.openedPages.push(currentPage);
- const pageShell = (page: Page) =>
- target === 'tui' ? { shell: { back: state.pageBack[page] ?? 0 } } : {};
- const requested = state.embedding.session;
- if (requested && !sameValue(requested, state.externalSelection) && !mutationPending(state)) {
- state.externalSelection = requested;
- select(state, actions, null);
- state.loading = true;
- closeDialog(state);
- intent(state, actions, { kind: 'openSession', selection: requested }, 'agent', 'observe', {
- sessionId: requested.id,
- });
- }
- if (state.dialog !== 'palette') {
- state.copyRequest = null;
- state.messageCopy = null;
- }
- retainSelectedDraft(state.drafts, state.selected);
- if (
- !Object.values(state.pending).some(
- (request) => request.kind === 'draftPage' || request.kind === 'draftReceipt',
- )
- ) {
- const step = nextDraftStep(state.drafts, state.attachments);
- if (step)
- intent(
- state,
- actions,
- step,
- step.kind === 'draftPage' ? 'history' : 'receipts',
- step.kind === 'draftPage' ? 'page' : 'read',
- step.kind === 'draftPage' ? { after: step.after, limit: 1 } : step.original.reference,
- );
- }
- uploadPastedFile(state, actions);
- if (!conversationRoute(state) || state.source || state.image || !state.selected) {
- state.searchOpen = false;
- state.browse = null;
- }
- const baseTheme = values.appearance?.[0] as Theme;
- if (target === 'tui' && state.preferences.open && !state.preferences.draft)
- appearanceEvent(
- state.preferences,
- { kind: 'raw', key: 'appearance-open', event: {} },
- baseTheme,
- locale,
- actions,
- );
- locale =
- target === 'tui'
- ? effectiveLocale(state.preferences, locale, values.locale?.[0])
- : resolveLocale(locale, values.locale?.[0]);
- const theme = target === 'tui' ? effectiveAppearance(state.preferences, baseTheme) : baseTheme;
- const text = copy(locale),
- ui = presentation.paint(theme),
- busy = mutationPending(state);
- const catalogBusy = busy || catalogPending(state);
- const commands = rootCommands(state, locale, target);
- const createButton = ui.button(
- 'create',
- target === 'tui' ? '+ ' + tuiText(locale, 'sidebar-new-session') : text.new,
- !commands.some((command) => command.kind === 'create' && !command.disabled),
- target === 'tui',
- target === 'tui' ? { background: 0, align: 'start' } : {},
- );
- const createEntry = ui.contextMenu
- ? ui.contextMenu('navigation-create-menu', text.new, createButton, [
- ui.button(
- 'create-workspace',
- text.newWorkspace,
- !commands.some((command) => command.kind === 'create-workspace' && !command.disabled),
- ),
- ])
- : createButton;
- const navigationLink = (
- key: string,
- label: string,
- glyph: string,
- ascii: string,
- current = false,
- ) =>
- ui.button(key, `${theme.ascii ? ascii : glyph} ${label}`, false, false, {
- background: current ? theme.selected : 0,
- align: 'start',
- });
- const navigationApps =
- target === 'tui' && !state.embedding.fixed
- ? [
- ui.box('navigation-apps-gap', [], { terminal: { basis: 1 } }),
- navigationLink('apps-open', text.appsTitle, '', '', state.pages.at(-1) === 'apps'),
- ...(state.embedding.workhub ? [navigationLink('workhub-open', 'WorkHub', '◈', '*')] : []),
- ...(state.scheduledTasks
- ? [
- navigationLink(
- 'scheduler-open',
- text.scheduleTitle +
- (state.scheduleAlerts.pending ? ` (${state.scheduleAlerts.pending})` : ''),
- '⏲',
- 'T',
- state.pages.at(-1) === 'scheduler',
- ),
- ]
- : []),
- navigationLink(
- 'recall-open',
- text.recallTitle,
- '⌕',
- '/',
- state.pages.at(-1) === 'recall',
- ),
- navigationLink(
- 'imports-open',
- text.importsTitle,
- '⇥',
- '>',
- state.pages.at(-1) === 'imports',
- ),
- ]
- : [];
- const navigation = ui.box(
- 'navigation-panel',
- [
- target === 'tui'
- ? ui.box(
- 'navigation-heading',
- [
- ui.box('navigation-create-cell', [createEntry], { terminal: { grow: 1 } }),
- ui.button(
- 'navigation-back',
- theme.ascii ? 'x' : '×',
- false,
- false,
- { background: 0, align: 'start' },
- undefined,
- text.hideSidebar,
- ),
- ],
- { row: true, gap: 0 },
- )
- : ui.text('navigation-brand', 'Maka', 24, theme.text, 650),
- ...(target === 'desktop' ? [createEntry] : []),
- ...(target === 'desktop'
- ? [ui.text('navigation-heading', text.conversations, 12, theme.muted, 550)]
- : []),
- ui.box(
- 'catalog-filters',
- [
- ...(['all', 'needs_you'] as const).map((filter) => {
- const label =
- filter === 'all'
- ? target === 'tui'
- ? tuiText(locale, 'sidebar-all')
- : text.allSessions
- : target === 'tui'
- ? tuiText(locale, 'sidebar-needs-you') +
- (state.catalog?.needsYouCount ? ` · ${state.catalog.needsYouCount}` : '')
- : state.catalog
- ? `${text.needsYou} (${state.catalog.needsYouCount})`
- : text.needsYou;
- const selected = state.catalogFilter === filter;
- return ui.item
- ? ui.item(
- `catalog-filter-${filter}`,
- ui.text(`catalog-filter-label-${filter}`, label, 12, theme.text),
- selected,
- catalogBusy,
- )
- : ui.button(
- `catalog-filter-${filter}`,
- label,
- catalogBusy,
- false,
- selected ? { background: theme.selected } : {},
- );
- }),
- ...(target === 'tui'
- ? [
- ui.button(
- 'catalog-search',
- `${theme.ascii ? '/' : '⌕'} ${tuiText(locale, 'sidebar-search')}`,
- false,
- false,
- { background: 0, align: 'start' },
- undefined,
- tuiText(locale, 'sidebar-search-sessions'),
- ),
- ]
- : []),
- ],
- { row: true, focusGroup: target === 'tui', gap: target === 'tui' ? 2 : 1 },
- ),
- ...(target === 'tui' ? [ui.box('catalog-gap', [], { terminal: { basis: 1 } })] : []),
- ui.box(
- 'catalog-body',
- [
- ...(!state.catalogFailed && !catalogPending(state) && state.catalog?.sessions.length === 0
- ? [
- ui.box(
- 'catalog-empty-inset',
- [
- ui.text(
- 'catalog-empty',
- (target === 'tui' ? ' ' : '') +
- (state.catalogFilter === 'needs_you'
- ? text.noneNeedsYou
- : tuiText(locale, 'sidebar-empty')),
- 12,
- theme.muted,
- ),
- ],
- { padding: target === 'desktop' ? 1 : 0, terminal: { shrink: true } },
- ),
- ]
- : []),
- ...(state.catalogFailed
- ? [ui.text('catalog-error', text.sessionsUnavailable, 12, theme.muted)]
- : []),
- ui.box(
- 'catalog-controls',
- [
- ...(state.catalog?.before
- ? [
- ui.button(
- 'catalog-previous',
- text.previousSessions,
- catalogBusy || state.catalogFailed,
- ),
- ]
- : []),
- ...(state.catalog?.after
- ? [ui.button('catalog-more', text.moreSessions, catalogBusy || state.catalogFailed)]
- : []),
- ...(target === 'desktop' || state.catalogFailed
- ? [ui.button('catalog-refresh', text.refreshSessions, catalogBusy)]
- : []),
- ],
- { row: true, style: { wrap: true }, terminal: { wrap: true } },
- ),
- presentation.slot(
- 'navigation',
- 'navigation',
- {
- theme,
- locale,
- sessions: state.catalog?.sessions ?? [],
- selected: state.selected?.id ?? null,
- busy: catalogBusy,
- },
- { select: 'select' },
- ),
- ...navigationApps,
- ],
- {
- gap: 0,
- scroll: target === 'tui' ? 'manual' : 'none',
- terminal: { grow: 1 },
- style: { height: pixels(0), grow: 1, minHeight: 0 },
- },
- ),
- ...(target === 'tui'
- ? [
- ui.box(
- 'navigation-footer',
- [
- ...(!state.embedding.fixed
- ? [
- navigationLink(
- 'projects-open',
- text.projectsTitle,
- '▦',
- 'P',
- state.pages.at(-1) === 'projects',
- ),
- ]
- : []),
- navigationLink(
- 'settings',
- tuiText(locale, 'route-settings'),
- '⛭',
- 'S',
- state.pages.at(-1) === 'settings',
- ),
- ],
- { gap: 0 },
- ),
- ]
- : []),
- ],
- {
- surface: target === 'desktop',
- gap: target === 'tui' ? 0 : undefined,
- padding: target === 'desktop' ? 20 : 1,
- style: { height: fill, width: fill },
- terminal: { grow: 1 },
- },
- );
- const timelineMessages = messages(state);
- const timeline = presentation.slot(
- 'timeline',
- 'timeline',
- {
- theme,
- locale,
- messages: timelineMessages,
- timings: turnTimings(state, timelineMessages),
- copyRequest: target === 'tui' && !state.image ? state.copyRequest : null,
- browse: state.browse,
- markReadEnabled:
- !!state.selected?.hasUnread &&
- !state.selected.archived &&
- !busy &&
- conversationRoute(state) &&
- state.dialog === null &&
- !state.source &&
- !state.image,
- search:
- target === 'tui' && state.searchOpen && state.selected
- ? { sessionId: state.selected.id, epoch: state.epoch, through: state.latestKnown }
- : null,
- source: state.source
- ? {
- kind: state.source.field === 'body' ? 'message' : state.source.field,
- fields: state.source.fields,
- format: state.source.format ?? 'text',
- ready: state.source.ready,
- key: `${state.source.field}-${state.source.through}-${state.source.chunk.sequence}-${state.source.chunk.offset}`,
- text: state.source.chunk.text,
- atStart: state.source.chunk.offset === 0,
- atEnd:
- state.source.chunk.offset + utf8Bytes(state.source.chunk.text) ===
- state.source.chunk.totalBytes,
- loading: state.source.loading,
- failed: state.source.failed,
- navigation: state.source.landing.sequence,
- landing: state.source.landing.edge,
- }
- : null,
- loading: state.loading,
- older: !!state.window?.page.before,
- newer: !!state.window?.page.after,
- latest:
- !!state.window &&
- (!state.window.latest ||
- state.latestKnown > state.window.page.through ||
- state.live.some(
- (fragment) =>
- fragment.closedAt !== null && fragment.closedAt <= state.window!.page.through,
- )),
- landing: state.landing,
- toolContext:
- state.selected && state.window && state.window.page.through > 0
- ? { sessionId: state.selected.id, through: state.window.page.through }
- : null,
- liveTools:
- state.window?.latest && state.run
- ? [
- ...new Set(
- state.window.page.chunks.flatMap((chunk) =>
- chunk.turnId === state.run!.turnId && chunk.tool ? [chunk.tool.toolUseId] : [],
- ),
- ),
- ]
- : [],
- waitingTools: state.window?.latest
- ? (state.interactions?.entries ?? [])
- .filter(
- (entry) =>
- !(
- state.interaction?.record?.requestId === entry.requestId &&
- state.interaction.record.outcome
- ),
- )
- .flatMap((entry) => (entry.toolUseId === null ? [] : [entry.toolUseId]))
- : [],
- live: !!state.run && !!state.window?.latest,
- },
- {
- older: 'older',
- newer: 'newer',
- latest: 'latest',
- markRead: 'mark-read',
- source: 'source',
- toolSource: 'tool-source',
- fileContext: 'fileContext',
- messageCopy: 'messageCopy',
- viewImage: 'view-image',
- sourceFirst: 'source-first',
- sourcePrevious: 'source-previous',
- sourceNext: 'source-next',
- sourceLast: 'source-last',
- sourceBack: 'source-back',
- sourceRetry: 'source-retry',
- sourceField: 'source-field',
- tailReady: 'tail-ready',
- searchClosed: 'search-closed',
- browseClosed: 'browse-closed',
- },
- );
- const imageViewer = presentation.slot(
- 'image-viewer',
- 'imageViewer',
- {
- theme,
- locale,
- image: state.image,
- active: !!state.image && conversationRoute(state),
- },
- { closed: 'image-closed' },
- );
- const attached = attachmentFiles(
- state.attachments,
- state.selected?.id,
- state.selected?.dataSource,
- );
- const attachmentsDisabled =
- !state.selected ||
- !draftReady(state.drafts, state.selected) ||
- busy ||
- state.loading ||
- !!state.attachments.paste;
- const attachmentsButton = ui.button(
- 'attachments-open',
- `${text.attachmentTitle}${attached.length ? ` (${attached.length})` : ''}`,
- attachmentsDisabled,
- );
- const composer = presentation.slot(
- 'composer',
- 'composer',
- {
- context: state.selected?.id ?? null,
- dataSource: state.selected?.dataSource ?? null,
- attachments: state.attachments.files,
- drafts: draftStatuses(state.drafts),
- theme,
- locale,
- busy:
- busy ||
- !!state.attachments.paste ||
- executionPending(state) ||
- (target === 'tui' && !!state.run && !state.queue),
- filePaste:
- target === 'tui' &&
- state.selected &&
- !state.selected.archived &&
- conversationRoute(state) &&
- !state.dialog &&
- !state.source &&
- !state.image &&
- !state.loading &&
- (!busy || !!state.attachments.paste) &&
- (!state.attachments.paste ||
- (state.attachments.paste.observed.context === state.selected.id &&
- sameValue(state.attachments.paste.observed.dataSource, state.selected.dataSource)))
- ? {
- context: state.selected.id,
- maxFiles: Math.min(8 - attached.length, 64 - state.attachments.files.length),
- }
- : null,
- fileCommand: state.attachments.paste?.command ?? null,
- running: !!state.run,
- enabled:
- !!state.selected &&
- !state.selected.archived &&
- canExecute(state) &&
- !state.loading &&
- state.notice !== 'unavailable',
- command: state.command,
- autoFocus: !state.searchOpen && state.browse === null,
- hasAttachments: attached.length > 0,
- },
- {
- send: 'send',
- stop: 'stop',
- attachments: 'attachments',
- changed: 'draft',
- files: 'files',
- checkpoint: 'checkpoint',
- draftCheck: 'draft-check',
- },
- );
- const queueProps: RowsProps = {
- terminal: theme.terminal,
- entry: state.queue?.entries.at(-1)?.messageId ?? null,
- revision: state.queue?.revision ?? 0,
- rows: (state.queue?.entries ?? []).map((entry): RowsProps['rows'][number] => {
- const lane = state.queue!.entries.filter((row) => row.kind === entry.kind);
- const index = lane.findIndex((row) => row.messageId === entry.messageId);
- const enabled = !busy && !executionPending(state);
- const up = {
- key: 'up',
- label: theme.ascii ? '^' : '↑',
- enabled: enabled && index > 0,
- };
- const down = {
- key: 'down',
- label: theme.ascii ? 'v' : '↓',
- enabled: enabled && index + 1 < lane.length,
- };
- const edit = { key: 'edit', label: theme.ascii ? 'e' : '✎', enabled };
- const retract = {
- key: 'retract',
- label: theme.ascii ? 'x' : '×',
- enabled,
- };
- return {
- key: entry.messageId,
- text: ` ${entry.kind === 'steering' ? (theme.ascii ? '^' : '↗') : entry.kind === 'in_flight' ? (theme.ascii ? '...' : '…') : theme.ascii ? '+' : '↳'} ${entry.preview}`,
- actions:
- entry.kind === 'in_flight'
- ? []
- : entry.kind === 'followup'
- ? [
- {
- key: 'promote',
- label: theme.ascii ? '^' : '↗',
- enabled: enabled && !!state.run,
- },
- up,
- down,
- edit,
- retract,
- ]
- : [up, down, edit, retract],
- };
- }),
- shortcuts: [
- { key: 'enter', modifiers: [], input: 'edit', enabled: true },
- { key: 'e', modifiers: [], input: 'edit', enabled: true },
- { key: 's', modifiers: [], input: 'promote', enabled: true },
- { key: 'delete', modifiers: [], input: 'retract', enabled: true },
- { key: 'x', modifiers: [], input: 'retract', enabled: true },
- { key: 'up', modifiers: ['alt'], input: 'up', enabled: true },
- { key: 'down', modifiers: ['alt'], input: 'down', enabled: true },
- ],
- maxRows: 3,
- compactBelow: { height: 12, maxRows: 1 },
- tabIndex: 2,
- foreground: theme.text,
- muted: theme.muted,
- selected: theme.selected,
- hovered: theme.surface,
- scrollbar: {
- thumb: theme.ascii ? '#' : '┃',
- track: theme.ascii ? '|' : '│',
- thumbColor: theme.text,
- trackColor: theme.border,
- },
- };
- const status = state.notice
- ? text[state.notice]
- : state.run
- ? state.run.state === 'waiting_for_user'
- ? text.waiting
- : text.working
- : state.selected?.archived
- ? text.archivedConversation
- : '';
- const title = ui.text(
- 'title',
- (currentPage && state.pageChrome[currentPage]?.title) ||
- (state.pages.at(-1) === 'scheduler'
- ? text.scheduleTitle
- : state.pages.at(-1) === 'recall'
- ? text.recallTitle
- : state.pages.at(-1) === 'apps'
- ? text.appsTitle
- : state.pages.at(-1) === 'imports'
- ? text.importsTitle
- : state.pages.at(-1) === 'projects'
- ? text.projectsTitle
- : state.pages.at(-1) === 'settings'
- ? tuiText(locale, 'route-settings')
- : state.pages.at(-1) === 'connections'
- ? text.settings
- : state.pages.at(-1) === 'deployment'
- ? text.deploymentTitle
- : (state.selected?.title ?? tuiText(locale, 'route-workspace'))),
- 17,
- theme.text,
- 650,
- {
- align: target === 'tui' ? 'start' : 'center',
- width: pixels(0),
- grow: 1,
- minWidth: 0,
- truncate: true,
- },
- );
- const models = presentation.slot(
- 'models',
- 'models',
- {
- theme,
- locale,
- selection: state.model,
- executor: state.executor,
- catalogEpoch: state.catalogEpoch,
- openRequest: state.modelOpenRequest,
- busy: busy || executionPending(state) || (target === 'tui' && !!state.run && !state.queue),
- },
- {
- selected: 'model',
- setup: 'model-setup',
- connections: 'model-connections',
- executor: 'executor',
- backends: 'backends',
- },
- );
-
- const header = ui.box(
- 'header',
- [
- ...(target === 'desktop'
- ? [
- ui.box(
- 'context',
- [
- title,
- ...(!state.embedding.fixed
- ? [ui.button('navigation-toggle', sidebarLabel(state, text))]
- : []),
- ],
- {
- row: true,
- gap: 8,
- style: { width: fill, align: 'center' },
- },
- ),
- ui.box(
- 'toolbar',
- [
- ui.box('model-control', [models], {
- style: { width: pixels(0), grow: 1, minWidth: 0 },
- }),
- ui.button('palette-open', text.paletteTitle),
- ui.button(
- 'details-open',
- text.detailsOpen,
- !rootCommands(state, locale, target).some(
- (command) => command.kind === 'details-open' && !command.disabled,
- ),
- ),
- ui.button('terminal-open', text.terminalTitle, !state.selected || state.loading),
- ...(!state.embedding.fixed
- ? [
- ui.button('projects-open', text.projectsTitle),
- ...(state.embedding.workhub ? [ui.button('workhub-open', 'WorkHub')] : []),
- ui.button('apps-open', text.appsTitle),
- ui.button('imports-open', text.importsTitle),
- ...(state.scheduledTasks
- ? [ui.button('scheduler-open', text.scheduleTitle)]
- : []),
- ]
- : []),
- ui.button('settings', text.settings),
- ui.button('deployment-open', text.deploymentTitle),
- ],
- { row: true, gap: 8, style: { width: fill, align: 'center' } },
- ),
- ]
- : [
- ui.box(
- 'header-left',
- [
- ...(!state.embedding.fixed
- ? [
- ui.box(
- 'navigation-cell',
- [
- ui.button(
- 'navigation-toggle',
- theme.ascii ? '=' : '≡',
- false,
- false,
- { background: 0 },
- undefined,
- text.toggleSidebar + ' · Ctrl+B',
- ),
- ],
- { terminal: { basis: 3 } },
- ),
- ]
- : []),
- ...(currentPage
- ? [
- ui.box(
- 'back-cell',
- [
- ui.button(
- 'page-back',
- theme.ascii ? '<' : '‹',
- currentPage !== 'settings' &&
- state.pageChrome[currentPage]?.backEnabled !== true,
- false,
- { background: 0 },
- undefined,
- text.back,
- ),
- ],
- { terminal: { basis: 3 } },
- ),
- ]
- : []),
- ],
- {
- visible: !state.embedding.fixed || !conversationRoute(state),
- row: true,
- gap: 0,
- terminal: { basis: (state.embedding.fixed ? 0 : 3) + (currentPage ? 3 : 0) },
- },
- ),
- ui.box(
- 'header-task',
- [
- ...(state.selected && conversationRoute(state)
- ? [
- state.run?.state === 'waiting_for_user'
- ? ui.text('task-face', '-_-', 14, theme.warning)
- : ui.activity('task-face', state.run ? 'spring' : 'familiar', theme.accent),
- ]
- : []),
- ui.box('header-title', [title], { terminal: { shrink: true } }),
- ui.box('header-title-trailing', [], { terminal: { grow: 1 } }),
- ],
- { row: true, gap: 1, terminal: { grow: 1, shrink: true } },
- ),
- ui.box(
- 'toolbar',
- [
- ...(['details-open', 'terminal-open', 'palette-open'] as const).map((key, at) =>
- ui.box(
- `${key}-cell`,
- [
- ui.button(
- key,
- [theme.ascii ? 'i' : 'ⓘ', '>_', theme.ascii ? ':' : '⌘'][at]!,
- key !== 'palette-open' && (!conversationRoute(state) || !state.selected),
- false,
- { background: 0 },
- undefined,
- [text.detailsOpen, text.terminalTitle, text.paletteTitle + ' · Ctrl+K'][at],
- ),
- ],
- { terminal: { basis: 3 } },
- ),
- ),
- ],
- { row: true, gap: 0, terminal: { basis: 9 } },
- ),
- ]),
- ...((target === 'desktop' ? state.interactions?.entries : [])
- ?.filter((entry) =>
- ['question', 'form', 'service_approval', 'permissions'].includes(entry.kind),
- )
- .map((entry, index) =>
- ui.button(
- `${entry.kind === 'question' || entry.kind === 'form' ? 'question' : 'approval'}-open-${entry.requestId}`,
- `${entry.kind === 'form' ? text.formTitle : entry.kind === 'question' ? text.questionsTitle : entry.kind === 'permissions' ? text.permissionApprovalTitle : text.approvalTitle} ${index + 1}`,
- !state.selected || state.selected.archived || mutationPending(state),
- ),
- ) ?? []),
- ...(target === 'desktop' && !canExecute(state)
- ? [
- ui.text(
- 'model-setup-hint',
- state.hasBackends
- ? text.executionBackend
- : state.modelSetup === 'loading'
- ? text.loadingModels
- : state.modelSetup === 'unavailable'
- ? text.modelsUnavailable
- : state.modelSetup === 'connect'
- ? text.setupConnectionHint
- : text.setupModelHint,
- 13,
- theme.muted,
- ),
- ...(state.modelSetup === 'connect'
- ? [
- ui.button(
- 'model-setup',
- state.hasBackends ? text.executionBackend : text.configureModels,
- busy,
- ),
- ]
- : []),
- ]
- : []),
- ...(target === 'desktop' && status ? [ui.text('status', status, 13, theme.muted)] : []),
- ...(state.uncertain
- ? [
- ui.button(
- 'check',
- text.check,
- Object.values(state.pending).some((request) => request.kind === 'receipt'),
- ),
- ]
- : []),
- ...(state.notice === 'unavailable' ? [ui.button('reload', text.loading, state.loading)] : []),
- ],
- {
- row: target === 'tui',
- gap: target === 'desktop' ? 8 : 0,
- style: {
- width: fill,
- shrink: 0,
- padding: { top: 16, right: 24, bottom: 16, left: 24 },
- },
- },
- );
- const welcome =
- !!state.selected &&
- !state.selected.archived &&
- !state.loading &&
- !!state.window &&
- !state.window.page.chunks.length &&
- !state.live.length &&
- !state.run &&
- !state.source &&
- !state.image &&
- !state.searchOpen;
- const main = ui.box(
- 'main',
- [
- ...(target === 'desktop' ? [header] : []),
- ...(target === 'tui'
- ? [
- ui.box(
- 'home',
- [
- ui.box('home-top', [], { terminal: { grow: 1 } }),
- ui.box(
- 'home-center',
- [
- ui.box('home-left', [], { terminal: { grow: 1 } }),
- ui.box(
- 'home-content',
- [
- ui.text('home-title', 'Maka', 24, theme.text, 650, {
- align: 'center',
- }),
- ...(state.place
- ? [
- ui.text('home-place', state.place, 14, theme.subtle, 400, {
- align: 'center',
- truncate: true,
- }),
- ]
- : []),
- ui.box('home-gap', [], { terminal: { basis: 1 } }),
- ...(!canExecute(state)
- ? [
- ui.text(
- 'home-setup',
- state.hasBackends
- ? text.executionBackend
- : tuiText(
- locale,
- state.modelSetup === 'loading'
- ? 'connections-loading'
- : state.modelSetup === 'unavailable'
- ? 'home-setup-failed'
- : state.modelSetup === 'connect'
- ? 'home-needs-connection'
- : 'home-needs-default',
- ),
- 14,
- state.modelSetup === 'unavailable' ? theme.warning : theme.subtle,
- 400,
- { align: 'center' },
- ),
- ]
- : []),
- ui.button(
- 'home-new',
- canExecute(state)
- ? '+ ' + tuiText(locale, 'sidebar-new-session') + ' Ctrl+N'
- : state.hasBackends
- ? text.executionBackend
- : tuiText(
- locale,
- state.modelSetup === 'connect'
- ? 'route-connections'
- : 'default-model-title',
- ) + ' Ctrl+N',
- catalogBusy ||
- (!canExecute(state) &&
- !state.hasBackends &&
- state.modelSetup === 'loading'),
- true,
- { background: 0 },
- ),
- ui.button('home-projects', text.projectsTitle, false, false, {
- background: 0,
- }),
- ui.button('home-imports', text.importsTitle, false, false, {
- background: 0,
- }),
- ],
- { gap: 0, terminal: { basis: 52, shrink: true } },
- ),
- ui.box('home-right', [], { terminal: { grow: 1 } }),
- ],
- { row: true, gap: 0 },
- ),
- ui.box('home-bottom', [], { terminal: { grow: 1 } }),
- ],
- {
- gap: 0,
- visible: !state.selected && !state.embedding.fixed,
- viewport: true,
- terminal: { grow: 1 },
- },
- ),
- ]
- : []),
- ...(state.selected?.archived && attached.length && !state.source && !state.image
- ? [ui.box('archived-attachments', [attachmentsButton], { row: true })]
- : []),
- ...(target === 'desktop' && state.attachments.paste
- ? [pasteView(presentation, theme, locale, state.attachments.paste, busy)]
- : []),
- ...(target === 'desktop'
- ? [
- ui.box(
- 'conversation',
- [
- ui.box('retained-timeline', [timeline], {
- visible: !state.image,
- style: { width: fill, height: fill },
- terminal: { grow: 1 },
- }),
- imageViewer,
- ],
- {
- style: {
- width: fill,
- height: pixels(0),
- grow: 1,
- shrink: 1,
- minHeight: 0,
- },
- },
- ),
- ui.box(
- 'input-area',
- [...(!state.selected?.archived ? [attachmentsButton] : []), composer],
- {
- visible: !state.image && !state.selected?.archived,
- padding: 24,
- style: { width: fill, maxWidth: 808, shrink: 0 },
- },
- ),
- ]
- : [
- ui.box(
- 'session-body',
- [
- ui.box(
- 'conversation',
- [
- ui.box(
- 'welcome-mark',
- [
- presentation.slot(
- 'welcome',
- 'welcome',
- {
- background: theme.terminal?.background ?? theme.background >>> 8,
- foreground: theme.terminal?.foreground ?? theme.text >>> 8,
- motion: theme.motion,
- ascii: theme.ascii,
- },
- {},
- ),
- ],
- { visible: welcome, terminal: { grow: 1 } },
- ),
- ui.box('retained-timeline', [timeline], {
- visible: !state.image && !welcome,
- style: { width: fill, height: fill },
- terminal: { grow: 1 },
- }),
- imageViewer,
- ],
- { terminal: { basis: 1, grow: 1 } },
- ),
- ...(state.attachments.paste
- ? [pasteView(presentation, theme, locale, state.attachments.paste, busy)]
- : []),
- ui.box(
- 'retained-composer',
- [
- ...(reviewTarget(state)
- ? [
- ui.button(
- 'interaction-open',
- `Ctrl G · ${text.reviewRequest}`,
- !rootCommands(state, locale, target).some(
- (command) => command.kind === 'interaction-open' && !command.disabled,
- ),
- ),
- ]
- : []),
- presentation.slot('queue', 'rows', queueProps, {
- activated: 'queue-action',
- }),
- ...(queueProps.rows.length && ui.focusButton
- ? [
- ui.box(
- 'queue-status',
- [
- ui.box('queue-status-space', [], {
- terminal: { basis: 0, grow: 1 },
- }),
- ui.focusButton(
- 'queue-focus',
- `${theme.ascii ? '=' : '≡'} ${queueProps.rows.length}`,
- 'queue',
- ),
- ],
- { row: true, gap: 0 },
- ),
- ]
- : []),
- ...(attached.length
- ? [
- attachmentSummary(
- presentation,
- theme,
- locale,
- attached,
- attachmentsDisabled,
- ),
- ]
- : []),
- ui.box(
- 'composer-navigation',
- [
- composer,
- ui.box(
- 'composer-footer',
- [
- presentation.slot(
- 'footer',
- 'footer',
- {
- sessionId: state.selected?.id ?? null,
- dataSource: state.selected?.dataSource ?? null,
- running: !!state.run,
- theme,
- locale,
- },
- {},
- ),
- ],
- { terminal: { basis: 1, shrink: false }, scroll: 'none' },
- ),
- ],
- {
- gap: 0,
- terminal: { shrink: true },
- focusShortcuts: [
- {
- key: 'up',
- modifiers: [],
- child: 'queue',
- enabled: queueProps.rows.length > 0,
- },
- ],
- },
- ),
- ],
- {
- gap: 0,
- visible: !state.source && !state.image && !state.selected?.archived,
- terminal: { shrink: true },
- },
- ),
- ],
- {
- gap: 0,
- visible: !!state.selected,
- viewport: true,
- terminal: { grow: 1 },
- },
- ),
- ui.box(
- 'session-metadata',
- [
- ui.box('model-metadata', [models], {
- terminal: { grow: 1, shrink: true },
- }),
- ...(status
- ? [
- ui.text(
- 'status',
- status,
- 12,
- state.run?.state === 'waiting_for_user' ? theme.warning : theme.muted,
- ),
- ]
- : []),
- ],
- { row: true, gap: 1 },
- ),
- ]),
- ],
- {
- visible: conversationRoute(state),
- gap: target === 'tui' ? 0 : undefined,
- terminal: { grow: 1 },
- style: {
- width: target === 'desktop' ? pixels(0) : fill,
- grow: 1,
- height: fill,
- align: 'center',
- minWidth: 0,
- },
- },
- );
- const pageContent = ui.box(
- 'page-content',
- [
- main,
- ...(state.openedPages.includes('settings')
- ? [
- ui.box(
- 'retained-settings',
- [
- settingsView(
- presentation,
- state.preferences,
- theme,
- locale,
- state.settingsCategory,
- currentPage === 'settings',
- state.insightsScope,
- state.web,
- state.jev,
- state.embedding.workhub === true,
- ),
- ],
- {
- visible: currentPage === 'settings',
- terminal: { grow: 1 },
- style: { width: fill, height: fill },
- },
- ),
- ]
- : []),
- ...(state.openedPages.includes('deployment')
- ? [
- ui.box(
- 'retained-deployment',
- [
- presentation.slot(
- 'deployment',
- 'deployment',
- {
- theme,
- locale,
- ...pageShell('deployment'),
- ...(target === 'tui'
- ? {
- bookmarkKey: navigationKey(state) + '/deployment',
- navigationReady: bookmarkReady(state.bookmark, navigationBookmark(state)),
- }
- : {}),
- },
- { chrome: 'page-chrome', closed: 'deployment-closed' },
- ),
- ],
- {
- visible: currentPage === 'deployment',
- gap: 0,
- terminal: { grow: 1 },
- style: { width: fill, height: fill },
- },
- ),
- ]
- : []),
- ...(state.openedPages.includes('apps')
- ? [
- ui.box(
- 'retained-apps',
- [
- presentation.slot(
- 'apps',
- 'apps',
- {
- theme,
- locale,
- ...pageShell('apps'),
- open: state.pages.at(-1) === 'apps',
- busy: mutationPending(state) || catalogPending(state),
- },
- { chrome: 'page-chrome', closed: 'apps-closed', manage: 'apps-manage' },
- ),
- ],
- {
- visible: currentPage === 'apps',
- gap: 0,
- terminal: { grow: 1 },
- style: { width: fill, height: fill },
- },
- ),
- ]
- : []),
- ...(state.openedPages.includes('scheduler')
- ? [
- ui.box(
- 'retained-scheduler',
- [
- presentation.slot(
- 'scheduler',
- 'scheduler',
- {
- theme,
- locale,
- ...pageShell('scheduler'),
- open: currentPage === 'scheduler',
- busy: mutationPending(state) || catalogPending(state),
- catalogEpoch: state.catalogEpoch,
- requested: state.scheduleRequested,
- },
- {
- chrome: 'page-chrome',
- closed: 'scheduler-closed',
- selected: 'scheduler-selected',
- connections: 'scheduler-connections',
- },
- ),
- ],
- {
- visible: currentPage === 'scheduler',
- gap: 0,
- terminal: { grow: 1 },
- style: { width: fill, height: fill },
- },
- ),
- ]
- : []),
- ...(state.openedPages.includes('recall')
- ? [
- ui.box(
- 'retained-recall',
- [
- presentation.slot(
- 'recall',
- 'recall',
- {
- theme,
- locale,
- ...pageShell('recall'),
- open: state.pages.at(-1) === 'recall',
- busy: mutationPending(state) || catalogPending(state),
- },
- { chrome: 'page-chrome', closed: 'recall-closed', selected: 'recall-selected' },
- ),
- ],
- {
- visible: currentPage === 'recall',
- gap: 0,
- terminal: { grow: 1 },
- style: { width: fill, height: fill },
- },
- ),
- ]
- : []),
- ...(state.openedPages.includes('imports')
- ? [
- ui.box(
- 'retained-imports',
- [
- presentation.slot(
- 'imports',
- 'imports',
- {
- theme,
- locale,
- ...pageShell('imports'),
- open: state.pages.at(-1) === 'imports',
- busy: mutationPending(state) || catalogPending(state),
- },
- {
- chrome: 'page-chrome',
- closed: 'imports-closed',
- selected: 'import-selected',
- changed: 'imports-changed',
- },
- ),
- ],
- {
- visible: currentPage === 'imports',
- gap: 0,
- terminal: { grow: 1 },
- style: { width: fill, height: fill },
- },
- ),
- ]
- : []),
- ...(state.openedPages.includes('projects')
- ? [
- ui.box(
- 'retained-projects',
- [
- presentation.slot(
- 'projects',
- 'projects',
- {
- theme,
- locale,
- ...pageShell('projects'),
- open: state.pages.at(-1) === 'projects',
- busy: mutationPending(state) || catalogPending(state),
- },
- {
- chrome: 'page-chrome',
- closed: 'projects-closed',
- selected: 'project-selected',
- changed: 'projects-changed',
- },
- ),
- ],
- {
- visible: currentPage === 'projects',
- gap: 0,
- terminal: { grow: 1 },
- style: { width: fill, height: fill },
- },
- ),
- ]
- : []),
- ...(state.openedPages.includes('connections')
- ? [
- ui.box(
- 'retained-connections',
- [
- presentation.slot(
- 'connections',
- 'connections',
- { theme, locale, ...pageShell('connections') },
- { chrome: 'page-chrome', closed: 'settings-closed', changed: 'settings-changed' },
- ),
- ],
- {
- visible: currentPage === 'connections',
- gap: 0,
- terminal: { grow: 1 },
- style: { width: fill, height: fill },
- },
- ),
- ]
- : []),
- ],
- { gap: 0, terminal: { grow: 1 }, style: { width: fill, height: fill, minWidth: 0 } },
- );
- const conversation = ui.box(
- 'workspace',
- state.embedding.fixed
- ? [pageContent]
- : target === 'desktop'
- ? [
- ui.box('sidebar', [navigation], {
- visible: 'open' in state.sidebar && state.sidebar.open,
- style: { width: pixels(240), height: fill, shrink: 0 },
- }),
- pageContent,
- ]
- : [
- ui.adaptivePane!(
- 'navigation-pane',
- text.conversations,
- ui.box(
- 'page',
- [
- ui.box('page-inset-start', [], { terminal: { basis: 1 } }),
- pageContent,
- ui.box('page-inset-end', [], { terminal: { basis: 1 } }),
- ],
- { row: true, gap: 0, terminal: { grow: 1 } },
- ),
- ui.box('navigation-content', [navigation], {
- gap: 0,
- terminal: { grow: 1 },
- }),
- 'command' in state.sidebar ? state.sidebar.command : null,
- ),
- ],
- {
- row: target === 'desktop',
- gap: 0,
- terminal: { grow: 1 },
- style: { width: fill, height: fill },
- },
- );
- const background = ui.box(
- 'product',
- [
- ...(target === 'tui' ? [header] : []),
- ...(!state.embedding.fixed && state.scheduleAlerts.reminders.length
- ? [
- ui.button(
- 'schedule-reminder',
- `${text.scheduleReminder}: ${compactLabel(state.scheduleAlerts.reminders[0]!.title, 100)}`,
- !!state.dialog,
- false,
- { background: theme.surface, align: 'start', width: fill },
- ),
- ]
- : []),
- conversation,
- ...(target === 'tui' && (!state.embedding.fixed || state.embedding.session)
- ? [
- bookmarkView(
- presentation,
- state.bookmark,
- navigationKey(state),
- navigationKey(state),
- navigationBookmark(state),
- ),
- ...(state.bookmark.observed?.state.kind === 'failed'
- ? [ui.button('navigation-checkpoint-retry', copy(locale).sourceRetry)]
- : []),
- ]
- : []),
- ],
- {
- gap: 0,
- background: theme.background,
- style: { width: fill, height: fill, align: 'center' },
- terminal: { grow: 1 },
- shortcuts: [
- ...(state.pages.at(-1) === 'settings' && state.dialog === null && !state.preferences.open
- ? ([{ key: 'escape', modifiers: [], input: 'page-back', enabled: true }] as Shortcut[])
- : []),
- {
- key: 'f',
- modifiers: ['primary'],
- input: 'search-open',
- enabled: rootCommands(state, locale, target).some(
- (command) => command.kind === 'search-open' && !command.disabled,
- ),
- },
- { key: 'f1', modifiers: [], input: 'help-open', enabled: true },
- {
- key: 'g',
- modifiers: ['primary'],
- input: 'interaction-open',
- enabled: rootCommands(state, locale, target).some(
- (command) => command.kind === 'interaction-open' && !command.disabled,
- ),
- },
- {
- key: 'k',
- modifiers: ['primary'],
- input: 'palette-open',
- enabled: true,
- },
- {
- key: 'b',
- modifiers: ['primary'],
- input: 'navigation-toggle',
- enabled: !state.embedding.fixed,
- },
- {
- key: 'n',
- modifiers: ['primary'],
- input: 'create',
- enabled: !state.embedding.fixed && !mutationPending(state) && !catalogPending(state),
- },
- ],
- },
- );
- const out = output(
- state,
- ui.dialog(
- 'workspace-dialog',
- state.dialog === 'attachments'
- ? target === 'tui' && state.attachments.selection?.browse
- ? tuiText(locale, 'attachments-local-files')
- : text.attachmentTitle
- : state.dialog === 'skills'
- ? text.skillsTitle
- : state.dialog === 'terminal'
- ? text.terminalTitle
- : state.dialog === 'question'
- ? state.interaction?.record?.request.kind === 'form'
- ? text.formTitle
- : text.questionsTitle
- : state.dialog === 'approval'
- ? state.interaction?.record?.request.kind === 'permissions'
- ? text.permissionApprovalTitle
- : text.approvalTitle
- : state.dialog === 'directories'
- ? text.directoryTitle
- : state.dialog === 'details'
- ? text.detailsTitle
- : state.dialog === 'queue'
- ? text.queueEdit
- : state.dialog === 'help'
- ? text.helpTitle
- : text.paletteTitle,
- background,
- ui.box(
- 'dialog-content',
- [
- ...(state.dialog === 'attachments' && state.attachments.selection
- ? [
- attachmentView(
- presentation,
- theme,
- locale,
- state.attachments,
- busy,
- state.selected?.id === state.attachments.selection.sessionId &&
- sameValue(state.selected.dataSource, state.attachments.selection.dataSource) &&
- !state.selected.archived,
- state.notice
- ? text[state.notice]
- : state.selected?.archived
- ? text.archivedConversation
- : null,
- !!state.uncertain,
- Object.values(state.pending).some((request) => request.kind === 'receipt'),
- ),
- ]
- : []),
- ...(state.interaction?.record?.request.kind === 'service_approval' ||
- state.interaction?.record?.request.kind === 'permissions'
- ? [
- presentation.slot(
- `${state.interaction.record.request.kind === 'permissions' ? 'permission' : 'service'}-approval-${state.interaction.attempt}`,
- state.interaction.record.request.kind === 'permissions'
- ? 'permissionApproval'
- : 'serviceApproval',
- {
- theme,
- locale,
- open: state.dialog === 'approval',
- record: state.interaction.record,
- busy:
- mutationPending(state) ||
- state.selected?.id !== state.interaction.sessionId ||
- !!state.selected?.archived,
- checking: Object.values(state.pending).some(
- (r) =>
- r.kind === 'interaction' || r.kind === 'receipt' || r.kind === 'respond',
- ),
- status: state.uncertain
- ? text.interactionUnknown
- : state.interaction.notice
- ? text[state.interaction.notice]
- : state.selected?.id !== state.interaction.sessionId
- ? text.interactionChanged
- : '',
- } satisfies ServiceApprovalProps,
- { closed: 'closed', submit: 'submit', check: 'check' },
- ),
- ]
- : state.interaction?.record
- ? [
- presentation.slot(
- questionChild(state),
- state.interaction.record.request.kind === 'form'
- ? 'formEditor'
- : 'questionEditor',
- {
- theme,
- locale,
- open: state.dialog === 'question',
- record: state.interaction.record,
- busy:
- mutationPending(state) ||
- state.selected?.id !== state.interaction.sessionId ||
- !!state.selected?.archived,
- checking: Object.values(state.pending).some(
- (r) =>
- r.kind === 'interaction' || r.kind === 'receipt' || r.kind === 'respond',
- ),
- status: state.uncertain
- ? text.interactionUnknown
- : state.interaction.notice
- ? text[state.interaction.notice]
- : state.selected?.id !== state.interaction.sessionId
- ? text.interactionChanged
- : '',
- } satisfies QuestionEditorProps,
- { closed: 'closed', submit: 'submit', check: 'check' },
- ),
- ]
- : [
- ui.box(
- 'question-loading',
- [
- ui.button('question-close', text.closeModels),
- ui.text(
- 'question-loading-text',
- state.interaction?.notice
- ? text[state.interaction.notice]
- : text.questionLoading,
- ),
- ui.button(
- 'question-check',
- text.check,
- Object.values(state.pending).some((r) => r.kind === 'interaction'),
- ),
- ],
- {
- visible: state.dialog === 'question' || state.dialog === 'approval',
- terminal: { grow: 1 },
- },
- ),
- ]),
- presentation.slot(
- 'queue-editor',
- 'queueEditor',
- {
- theme,
- locale,
- open: state.dialog === 'queue',
- ready: state.queueEdit?.phase === 'ready',
- hasAttachments: state.queueEdit?.hasAttachments ?? false,
- busy: mutationPending(state),
- uncertain: !!state.uncertain,
- checking: Object.values(state.pending).some((request) => request.kind === 'receipt'),
- command: state.queueEdit?.command ?? null,
- status: state.notice
- ? text[state.notice]
- : state.queueEdit?.phase === 'loading'
- ? text.queueLoading
- : '',
- } satisfies QueueEditorProps,
- {
- changed: 'changed',
- save: 'save',
- closed: 'closed',
- reload: 'reload',
- check: 'check',
- },
- ),
- ...(state.skills
- ? [
- presentation.slot(
- 'skill-library',
- 'skills',
- {
- theme,
- locale,
- open: state.dialog === 'skills',
- workspace: state.selected?.workspace ?? null,
- },
- { closed: 'closed' },
- ),
- ]
- : []),
- presentation.slot(
- 'help',
- 'help',
- { theme, locale, open: state.dialog === 'help' },
- { closed: 'closed' },
- ),
- presentation.slot(
- 'terminal-panel',
- 'terminals',
- {
- theme,
- locale,
- open: state.dialog === 'terminal',
- enabled: state.terminals,
- busy: mutationPending(state),
- session: state.selected,
- },
- { closed: 'closed' },
- ),
- presentation.slot(
- 'session-details',
- 'details',
- {
- theme,
- locale,
- open: state.dialog === 'details',
- session: state.selected,
- busy: mutationPending(state),
- },
- { changed: 'changed', closed: 'closed' },
- ),
- presentation.slot(
- 'directory-picker',
... 1862 lines suppressed ...
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org