@phenx-inc/ctlsurf 0.15.0 → 0.15.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/headless/index.mjs +89 -71
- package/out/headless/index.mjs.map +3 -3
- package/out/main/index.js +90 -72
- package/out/preload/index.js +1 -1
- package/out/renderer/assets/{cssMode-CJR4iD-G.js → cssMode-CPfLk04b.js} +3 -3
- package/out/renderer/assets/{freemarker2-BRI9ISq9.js → freemarker2-CDHpuFqv.js} +1 -1
- package/out/renderer/assets/{handlebars-3hL6XIZI.js → handlebars-CgX9Jz36.js} +1 -1
- package/out/renderer/assets/{html-Dd5_vBoP.js → html-DdgFccDG.js} +1 -1
- package/out/renderer/assets/{htmlMode-DgNtlKcG.js → htmlMode-CRHIwtvP.js} +3 -3
- package/out/renderer/assets/{index-5azC4yNn.js → index-DVdWgw7p.js} +50 -38
- package/out/renderer/assets/{javascript-D0vPLEJg.js → javascript-CHhxL0s7.js} +2 -2
- package/out/renderer/assets/{jsonMode-DBvR0zMu.js → jsonMode-Cy7-eWai.js} +3 -3
- package/out/renderer/assets/{liquid-CLoc6Tx7.js → liquid-vMkF9pfk.js} +1 -1
- package/out/renderer/assets/{lspLanguageFeatures-CiaMaQoY.js → lspLanguageFeatures-nAygNzuG.js} +1 -1
- package/out/renderer/assets/{mdx-DTsq6tlv.js → mdx-C4UVz5dW.js} +1 -1
- package/out/renderer/assets/{python-D04DpZDr.js → python-BHemzN9g.js} +1 -1
- package/out/renderer/assets/{razor-Di6CJ6s8.js → razor-DH6NPzbo.js} +1 -1
- package/out/renderer/assets/{tsMode-mfv6mHOv.js → tsMode-DxBdlGZB.js} +1 -1
- package/out/renderer/assets/{typescript-CDA0Hhef.js → typescript-BRCOaBQ5.js} +1 -1
- package/out/renderer/assets/{xml-BU0voRuq.js → xml-DVv9gDP1.js} +1 -1
- package/out/renderer/assets/{yaml-qzFWnaZJ.js → yaml-jB-C3CdS.js} +1 -1
- package/out/renderer/index.html +1 -1
- package/package.json +1 -1
- package/src/main/index.ts +2 -2
- package/src/main/orchestrator.ts +2 -2
- package/src/main/ticketStore.ts +100 -66
- package/src/preload/index.ts +3 -3
- package/src/renderer/App.tsx +2 -2
- package/src/renderer/components/FloatingMic.tsx +15 -11
- package/src/renderer/components/TerminalPanel.tsx +11 -2
- package/src/renderer/components/TicketPanel.tsx +16 -5
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { conf as conf$1, language as language$1 } from "./typescript-
|
|
2
|
-
import "./index-
|
|
1
|
+
import { conf as conf$1, language as language$1 } from "./typescript-BRCOaBQ5.js";
|
|
2
|
+
import "./index-DVdWgw7p.js";
|
|
3
3
|
const conf = conf$1;
|
|
4
4
|
const language = {
|
|
5
5
|
// Set defaultToken to invalid to see what you do not tokenize yet
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { c as createWebWorker, l as languages, e as editor } from "./index-
|
|
2
|
-
import { f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider, C as CompletionAdapter, H as HoverAdapter, b as DocumentSymbolAdapter, d as DocumentColorAdapter, F as FoldingRangeAdapter, S as SelectionRangeAdapter, e as DiagnosticsAdapter } from "./lspLanguageFeatures-
|
|
3
|
-
import { a, D, h, R, c, i, j, t, k } from "./lspLanguageFeatures-
|
|
1
|
+
import { c as createWebWorker, l as languages, e as editor } from "./index-DVdWgw7p.js";
|
|
2
|
+
import { f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider, C as CompletionAdapter, H as HoverAdapter, b as DocumentSymbolAdapter, d as DocumentColorAdapter, F as FoldingRangeAdapter, S as SelectionRangeAdapter, e as DiagnosticsAdapter } from "./lspLanguageFeatures-nAygNzuG.js";
|
|
3
|
+
import { a, D, h, R, c, i, j, t, k } from "./lspLanguageFeatures-nAygNzuG.js";
|
|
4
4
|
const STOP_WHEN_IDLE_FOR = 2 * 60 * 1e3;
|
|
5
5
|
class WorkerManager {
|
|
6
6
|
constructor(defaults) {
|
package/out/renderer/assets/{lspLanguageFeatures-CiaMaQoY.js → lspLanguageFeatures-nAygNzuG.js}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { R as Range$1, l as languages, e as editor, U as Uri, M as MarkerSeverity } from "./index-
|
|
1
|
+
import { R as Range$1, l as languages, e as editor, U as Uri, M as MarkerSeverity } from "./index-DVdWgw7p.js";
|
|
2
2
|
var DocumentUri;
|
|
3
3
|
(function(DocumentUri2) {
|
|
4
4
|
function is(value) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as createWebWorker, e as editor, U as Uri, a as MarkerTag, M as MarkerSeverity, l as languages, t as typescriptDefaults, R as Range } from "./index-
|
|
1
|
+
import { c as createWebWorker, e as editor, U as Uri, a as MarkerTag, M as MarkerSeverity, l as languages, t as typescriptDefaults, R as Range } from "./index-DVdWgw7p.js";
|
|
2
2
|
class WorkerManager {
|
|
3
3
|
constructor(_modeId, _defaults) {
|
|
4
4
|
this._modeId = _modeId;
|
package/out/renderer/index.html
CHANGED
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
:root[data-theme='catppuccin-latte'] .boot-splash-title { color: #2f3145; }
|
|
107
107
|
:root[data-theme='catppuccin-latte'] .boot-splash-label { color: #7c7f93; }
|
|
108
108
|
</style>
|
|
109
|
-
<script type="module" crossorigin src="./assets/index-
|
|
109
|
+
<script type="module" crossorigin src="./assets/index-DVdWgw7p.js"></script>
|
|
110
110
|
<link rel="stylesheet" crossorigin href="./assets/index-3sFJEz7i.css">
|
|
111
111
|
</head>
|
|
112
112
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phenx-inc/ctlsurf",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.2",
|
|
4
4
|
"description": "Agent-agnostic terminal and desktop app for ctlsurf — run Claude Code, Codex, or any coding agent with live session logging and remote control",
|
|
5
5
|
"main": "out/main/index.js",
|
|
6
6
|
"bin": {
|
package/src/main/index.ts
CHANGED
|
@@ -710,8 +710,8 @@ ipcMain.handle('tickets:add', async (_event, input: {
|
|
|
710
710
|
})
|
|
711
711
|
ipcMain.handle('tickets:update', async (_event, rowId: string, input: {
|
|
712
712
|
title: string; description?: string; status?: string; priority?: string
|
|
713
|
-
}) => {
|
|
714
|
-
return orchestrator.updateTicketForActiveTab(rowId, input)
|
|
713
|
+
}, blockId?: string) => {
|
|
714
|
+
return orchestrator.updateTicketForActiveTab(rowId, input, blockId)
|
|
715
715
|
})
|
|
716
716
|
ipcMain.handle('tickets:list', async () => {
|
|
717
717
|
return orchestrator.listTicketsForActiveTab()
|
package/src/main/orchestrator.ts
CHANGED
|
@@ -780,13 +780,13 @@ export class Orchestrator {
|
|
|
780
780
|
return this.ticketStore.addTicket(cwd, input)
|
|
781
781
|
}
|
|
782
782
|
|
|
783
|
-
async updateTicketForActiveTab(rowId: string, input: TicketInput): Promise<{ ok: boolean; error?: string }> {
|
|
783
|
+
async updateTicketForActiveTab(rowId: string, input: TicketInput, blockId?: string): Promise<{ ok: boolean; error?: string }> {
|
|
784
784
|
const cwd = this.getActiveTabProjectPath()
|
|
785
785
|
if (!cwd) return { ok: false, error: 'No active terminal tab' }
|
|
786
786
|
if (!this.ctlsurfApi.getApiKey()) {
|
|
787
787
|
return { ok: false, error: 'ctlsurf API key not configured' }
|
|
788
788
|
}
|
|
789
|
-
return this.ticketStore.updateTicket(cwd, rowId, input)
|
|
789
|
+
return this.ticketStore.updateTicket(cwd, rowId, input, blockId)
|
|
790
790
|
}
|
|
791
791
|
|
|
792
792
|
async listTicketsForActiveTab(): Promise<{ ok: boolean; tickets: import('./ticketStore').Ticket[]; blockId?: string; pageId?: string; error?: string }> {
|
package/src/main/ticketStore.ts
CHANGED
|
@@ -45,23 +45,31 @@ export interface Ticket {
|
|
|
45
45
|
status: string
|
|
46
46
|
priority: string
|
|
47
47
|
created: string | null
|
|
48
|
+
// Owning datastore, so edits and agent dispatches target the store this row
|
|
49
|
+
// actually lives in — a folder can accumulate more than one Tickets store.
|
|
50
|
+
blockId: string
|
|
51
|
+
pageId: string
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
function log(...args: unknown[]): void {
|
|
51
55
|
baseLog('[ticket-store]', ...args)
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
|
|
58
|
+
/** Collects every page with the given title, in tree order. A folder can end
|
|
59
|
+
* up with duplicate "Agent Datastore" pages (e.g. an agent creating its own
|
|
60
|
+
* under an unrelated page), with tickets split across them. */
|
|
61
|
+
function findPagesByTitle(pages: any[], title: string): any[] {
|
|
62
|
+
const found: any[] = []
|
|
55
63
|
for (const p of pages) {
|
|
56
|
-
if (p?.title === title)
|
|
57
|
-
if (p?.children?.length)
|
|
58
|
-
const c = findPageByTitle(p.children, title)
|
|
59
|
-
if (c) return c
|
|
60
|
-
}
|
|
64
|
+
if (p?.title === title) found.push(p)
|
|
65
|
+
if (p?.children?.length) found.push(...findPagesByTitle(p.children, title))
|
|
61
66
|
}
|
|
62
|
-
return
|
|
67
|
+
return found
|
|
63
68
|
}
|
|
64
69
|
|
|
70
|
+
interface DsRef { blockId: string; pageId: string }
|
|
71
|
+
interface ResolvedStores { primary: DsRef; all: DsRef[] }
|
|
72
|
+
|
|
65
73
|
/**
|
|
66
74
|
* Per-project `Tickets` datastore manager. Mirrors TimeTracker's datastore
|
|
67
75
|
* resolution (find folder → Agent Datastore page → find/create block by
|
|
@@ -70,10 +78,10 @@ function findPageByTitle(pages: any[], title: string): any | null {
|
|
|
70
78
|
*/
|
|
71
79
|
export class TicketStore {
|
|
72
80
|
private api: CtlsurfApi
|
|
73
|
-
// Per-cwd cache of
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
private dsCache = new Map<string,
|
|
81
|
+
// Per-cwd cache of every resolved Tickets datastore. `primary` is where new
|
|
82
|
+
// tickets are written and what gets handed to the coding agent; `all` covers
|
|
83
|
+
// duplicate Agent Datastore pages so listing misses nothing.
|
|
84
|
+
private dsCache = new Map<string, ResolvedStores>()
|
|
77
85
|
|
|
78
86
|
constructor(api: CtlsurfApi) {
|
|
79
87
|
this.api = api
|
|
@@ -84,11 +92,11 @@ export class TicketStore {
|
|
|
84
92
|
const title = input.title?.trim()
|
|
85
93
|
if (!title) return { ok: false, error: 'Title is required' }
|
|
86
94
|
try {
|
|
87
|
-
const
|
|
88
|
-
if (!
|
|
95
|
+
const stores = await this.resolveStores(cwd, true)
|
|
96
|
+
if (!stores) {
|
|
89
97
|
return { ok: false, error: 'No ctlsurf project found for this folder' }
|
|
90
98
|
}
|
|
91
|
-
await this.api.addRow(
|
|
99
|
+
await this.api.addRow(stores.primary.blockId, {
|
|
92
100
|
Title: title,
|
|
93
101
|
Description: input.description?.trim() || '',
|
|
94
102
|
Status: input.status || 'Open',
|
|
@@ -103,14 +111,15 @@ export class TicketStore {
|
|
|
103
111
|
}
|
|
104
112
|
}
|
|
105
113
|
|
|
106
|
-
/** Updates an existing ticket row
|
|
107
|
-
|
|
114
|
+
/** Updates an existing ticket row. `blockId` names the store the row lives
|
|
115
|
+
* in (from listTickets); without it the primary store is assumed. */
|
|
116
|
+
async updateTicket(cwd: string, rowId: string, input: TicketInput, blockId?: string): Promise<{ ok: boolean; error?: string }> {
|
|
108
117
|
const title = input.title?.trim()
|
|
109
118
|
if (!title) return { ok: false, error: 'Title is required' }
|
|
110
119
|
try {
|
|
111
|
-
const
|
|
112
|
-
if (!
|
|
113
|
-
await this.api.updateRow(
|
|
120
|
+
const stores = await this.resolveStores(cwd, false)
|
|
121
|
+
if (!stores) return { ok: false, error: 'No Tickets datastore for this project' }
|
|
122
|
+
await this.api.updateRow(blockId || stores.primary.blockId, rowId, {
|
|
114
123
|
Title: title,
|
|
115
124
|
Description: input.description?.trim() || '',
|
|
116
125
|
Status: input.status || 'Open',
|
|
@@ -124,29 +133,42 @@ export class TicketStore {
|
|
|
124
133
|
}
|
|
125
134
|
}
|
|
126
135
|
|
|
127
|
-
/** Lists existing tickets
|
|
128
|
-
*
|
|
136
|
+
/** Lists existing tickets across every Tickets store in the project's
|
|
137
|
+
* folder, newest first. Does not create the datastore — an unconfigured
|
|
138
|
+
* project simply has no tickets yet. */
|
|
129
139
|
async listTickets(cwd: string): Promise<{ ok: boolean; tickets: Ticket[]; blockId?: string; pageId?: string; error?: string }> {
|
|
130
140
|
try {
|
|
131
|
-
const
|
|
132
|
-
if (!
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
const stores = await this.resolveStores(cwd, false)
|
|
142
|
+
if (!stores) return { ok: true, tickets: [] }
|
|
143
|
+
const tickets: Ticket[] = []
|
|
144
|
+
for (const ds of stores.all) {
|
|
145
|
+
const res = await this.api.queryRows(ds.blockId, { orderBy: 'Created', order: 'desc', limit: 200 })
|
|
146
|
+
for (const r of res?.rows || []) {
|
|
147
|
+
tickets.push({
|
|
148
|
+
id: r.id,
|
|
149
|
+
title: String(r.data?.Title ?? ''),
|
|
150
|
+
description: String(r.data?.Description ?? ''),
|
|
151
|
+
status: String(r.data?.Status ?? 'Open'),
|
|
152
|
+
priority: String(r.data?.Priority ?? 'Med'),
|
|
153
|
+
created: (r.data?.Created as string) ?? r.created_at ?? null,
|
|
154
|
+
blockId: ds.blockId,
|
|
155
|
+
pageId: ds.pageId,
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Each store is queried newest-first; re-sort the merged list so rows
|
|
160
|
+
// from different stores interleave correctly (ISO strings compare fine).
|
|
161
|
+
if (stores.all.length > 1) {
|
|
162
|
+
tickets.sort((a, b) => (b.created || '').localeCompare(a.created || ''))
|
|
163
|
+
}
|
|
164
|
+
return { ok: true, tickets, blockId: stores.primary.blockId, pageId: stores.primary.pageId }
|
|
143
165
|
} catch (err: any) {
|
|
144
166
|
log(`listTickets failed for ${cwd}: ${err?.message || err}`)
|
|
145
167
|
return { ok: false, tickets: [], error: err?.message || String(err) }
|
|
146
168
|
}
|
|
147
169
|
}
|
|
148
170
|
|
|
149
|
-
private async
|
|
171
|
+
private async resolveStores(cwd: string, create: boolean): Promise<ResolvedStores | null> {
|
|
150
172
|
const cached = this.dsCache.get(cwd)
|
|
151
173
|
if (cached) return cached
|
|
152
174
|
|
|
@@ -159,20 +181,34 @@ export class TicketStore {
|
|
|
159
181
|
if (!folder?.id) return null
|
|
160
182
|
|
|
161
183
|
const folderDetail = await this.api.getFolder(folder.id)
|
|
162
|
-
const
|
|
163
|
-
if (
|
|
164
|
-
const pageId: string = agentPage.id
|
|
184
|
+
const agentPages = findPagesByTitle(folderDetail?.pages || [], AGENT_DATASTORE_PAGE_TITLE)
|
|
185
|
+
if (agentPages.length === 0) return null
|
|
165
186
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
187
|
+
// The page bound to the codebase root is the canonical one; prefer it as
|
|
188
|
+
// the write target. Stable sort keeps tree order among the rest.
|
|
189
|
+
const rootPath = folder.root_path || cwd
|
|
190
|
+
agentPages.sort((a, b) => Number(b?.root_path === rootPath) - Number(a?.root_path === rootPath))
|
|
191
|
+
if (agentPages.length > 1) {
|
|
192
|
+
log(`Found ${agentPages.length} "Agent Datastore" pages for ${cwd}; aggregating tickets across all of them`)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const all: DsRef[] = []
|
|
196
|
+
for (const page of agentPages) {
|
|
197
|
+
for (const blockId of await this.findTicketBlocks(page.id)) {
|
|
198
|
+
all.push({ blockId, pageId: page.id })
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (all.length > 0) {
|
|
203
|
+
for (const ds of all) await this.ensureColumns(ds.blockId)
|
|
204
|
+
const stores = { primary: all[0], all }
|
|
205
|
+
this.dsCache.set(cwd, stores)
|
|
206
|
+
return stores
|
|
172
207
|
}
|
|
173
208
|
|
|
174
209
|
if (!create) return null
|
|
175
210
|
|
|
211
|
+
const pageId: string = agentPages[0].id
|
|
176
212
|
const columns = COLUMNS.map((c, i) => ({
|
|
177
213
|
id: `col_${i}`,
|
|
178
214
|
name: c.name,
|
|
@@ -187,50 +223,48 @@ export class TicketStore {
|
|
|
187
223
|
if (created?.id) {
|
|
188
224
|
log(`Created "${DATASTORE_TITLE}" datastore on Agent Datastore page for ${cwd}`)
|
|
189
225
|
const ds = { blockId: created.id, pageId }
|
|
190
|
-
|
|
191
|
-
|
|
226
|
+
const stores = { primary: ds, all: [ds] }
|
|
227
|
+
this.dsCache.set(cwd, stores)
|
|
228
|
+
return stores
|
|
192
229
|
}
|
|
193
230
|
return null
|
|
194
231
|
}
|
|
195
232
|
|
|
196
|
-
/** Finds
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
|
|
233
|
+
/** Finds every Tickets block on the page: system_key matches first, then
|
|
234
|
+
* title matches for legacy blocks. When no system_key block exists, the
|
|
235
|
+
* first legacy block is adopted (system_key backfilled so it survives a
|
|
236
|
+
* later rename); extra title matches are still listed but left unstamped so
|
|
237
|
+
* the primary can't flip between sessions. */
|
|
238
|
+
private async findTicketBlocks(pageId: string): Promise<string[]> {
|
|
200
239
|
const summaries = (await this.api.getPageBlockSummaries(pageId)) || []
|
|
201
240
|
const datastoreSummaries = summaries.filter((b: any) => b?.type === 'datastore' && b?.id)
|
|
202
|
-
if (datastoreSummaries.length === 0) return
|
|
241
|
+
if (datastoreSummaries.length === 0) return []
|
|
203
242
|
|
|
204
|
-
|
|
205
|
-
|
|
243
|
+
const keyed: string[] = []
|
|
244
|
+
const legacy: Array<{ id: string; props: Record<string, unknown> }> = []
|
|
206
245
|
for (const s of datastoreSummaries) {
|
|
207
246
|
try {
|
|
208
247
|
const block = await this.api.getBlock(s.id)
|
|
209
248
|
const props = (block?.props || {}) as Record<string, unknown>
|
|
210
|
-
if (props.system_key === SYSTEM_KEY)
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
if (s.title === DATASTORE_TITLE && titleFallbackId === null) {
|
|
214
|
-
titleFallbackId = s.id
|
|
215
|
-
titleFallbackProps = props
|
|
216
|
-
}
|
|
249
|
+
if (props.system_key === SYSTEM_KEY) keyed.push(s.id)
|
|
250
|
+
else if (s.title === DATASTORE_TITLE) legacy.push({ id: s.id, props })
|
|
217
251
|
} catch (err: any) {
|
|
218
252
|
log(`getBlock(${s.id}) failed during lookup: ${err?.message || err}`)
|
|
219
253
|
}
|
|
220
254
|
}
|
|
221
255
|
|
|
222
|
-
if (
|
|
256
|
+
if (keyed.length === 0 && legacy.length > 0) {
|
|
257
|
+
const adopt = legacy[0]
|
|
223
258
|
try {
|
|
224
|
-
await this.api.updateBlock(
|
|
225
|
-
props: { ...
|
|
259
|
+
await this.api.updateBlock(adopt.id, {
|
|
260
|
+
props: { ...adopt.props, system_key: SYSTEM_KEY },
|
|
226
261
|
})
|
|
227
|
-
log(`Backfilled system_key on legacy Tickets block ${
|
|
262
|
+
log(`Backfilled system_key on legacy Tickets block ${adopt.id}`)
|
|
228
263
|
} catch (err: any) {
|
|
229
|
-
log(`backfill system_key failed on ${
|
|
264
|
+
log(`backfill system_key failed on ${adopt.id}: ${err?.message || err}`)
|
|
230
265
|
}
|
|
231
|
-
return titleFallbackId
|
|
232
266
|
}
|
|
233
|
-
return
|
|
267
|
+
return [...keyed, ...legacy.map(l => l.id)]
|
|
234
268
|
}
|
|
235
269
|
|
|
236
270
|
private async ensureColumns(blockId: string): Promise<void> {
|
package/src/preload/index.ts
CHANGED
|
@@ -138,9 +138,9 @@ const api = {
|
|
|
138
138
|
ipcRenderer.invoke('tickets:project'),
|
|
139
139
|
addTicket: (input: { title: string; description?: string; status?: string; priority?: string }): Promise<{ ok: boolean; error?: string }> =>
|
|
140
140
|
ipcRenderer.invoke('tickets:add', input),
|
|
141
|
-
updateTicket: (rowId: string, input: { title: string; description?: string; status?: string; priority?: string }): Promise<{ ok: boolean; error?: string }> =>
|
|
142
|
-
ipcRenderer.invoke('tickets:update', rowId, input),
|
|
143
|
-
listTickets: (): Promise<{ ok: boolean; tickets: Array<{ id: string; title: string; description: string; status: string; priority: string; created: string | null }>; error?: string }> =>
|
|
141
|
+
updateTicket: (rowId: string, input: { title: string; description?: string; status?: string; priority?: string }, blockId?: string): Promise<{ ok: boolean; error?: string }> =>
|
|
142
|
+
ipcRenderer.invoke('tickets:update', rowId, input, blockId),
|
|
143
|
+
listTickets: (): Promise<{ ok: boolean; tickets: Array<{ id: string; title: string; description: string; status: string; priority: string; created: string | null; blockId?: string; pageId?: string }>; blockId?: string; pageId?: string; error?: string }> =>
|
|
144
144
|
ipcRenderer.invoke('tickets:list'),
|
|
145
145
|
|
|
146
146
|
// Chat logging (global)
|
package/src/renderer/App.tsx
CHANGED
|
@@ -62,8 +62,8 @@ declare global {
|
|
|
62
62
|
deleteProfile: (profileId: string) => Promise<{ ok: boolean }>
|
|
63
63
|
getTicketProject: () => Promise<{ cwd: string | null; name: string | null }>
|
|
64
64
|
addTicket: (input: { title: string; description?: string; status?: string; priority?: string }) => Promise<{ ok: boolean; error?: string }>
|
|
65
|
-
updateTicket: (rowId: string, input: { title: string; description?: string; status?: string; priority?: string }) => Promise<{ ok: boolean; error?: string }>
|
|
66
|
-
listTickets: () => Promise<{ ok: boolean; tickets: Array<{ id: string; title: string; description: string; status: string; priority: string; created: string | null }>; blockId?: string; pageId?: string; error?: string }>
|
|
65
|
+
updateTicket: (rowId: string, input: { title: string; description?: string; status?: string; priority?: string }, blockId?: string) => Promise<{ ok: boolean; error?: string }>
|
|
66
|
+
listTickets: () => Promise<{ ok: boolean; tickets: Array<{ id: string; title: string; description: string; status: string; priority: string; created: string | null; blockId?: string; pageId?: string }>; blockId?: string; pageId?: string; error?: string }>
|
|
67
67
|
getTracking: () => Promise<{ active: boolean }>
|
|
68
68
|
setTracking: (enabled: boolean) => Promise<{ active: boolean }>
|
|
69
69
|
getLogChat: () => Promise<{ enabled: boolean }>
|
|
@@ -15,10 +15,11 @@ interface FloatingMicProps {
|
|
|
15
15
|
onHide: () => void
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
// Keep the
|
|
18
|
+
// Keep the toolbar clear of the 38px titlebar, 24px status bar, and the
|
|
19
|
+
// terminal's bottom input line.
|
|
19
20
|
const EDGE = 20
|
|
20
21
|
const TOP_MIN = 46
|
|
21
|
-
const BOTTOM_GAP =
|
|
22
|
+
const BOTTOM_GAP = 72
|
|
22
23
|
|
|
23
24
|
function loadPos(): Pos | null {
|
|
24
25
|
try {
|
|
@@ -71,17 +72,20 @@ export function FloatingMic({ onTranscript, onHide }: FloatingMicProps) {
|
|
|
71
72
|
}
|
|
72
73
|
}, [])
|
|
73
74
|
|
|
74
|
-
//
|
|
75
|
+
// Default to bottom-right, and clamp saved positions on first mount too so
|
|
76
|
+
// increased chrome/input clearance applies to existing installations.
|
|
75
77
|
useEffect(() => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
setPos((current) => {
|
|
79
|
+
if (current) return clamp(current.x, current.y)
|
|
80
|
+
const el = elRef.current
|
|
81
|
+
const w = el?.offsetWidth ?? 64
|
|
82
|
+
const h = el?.offsetHeight ?? 90
|
|
83
|
+
return clamp(
|
|
84
|
+
window.innerWidth - w - EDGE,
|
|
85
|
+
window.innerHeight - h - BOTTOM_GAP,
|
|
86
|
+
)
|
|
83
87
|
})
|
|
84
|
-
}, [
|
|
88
|
+
}, [clamp])
|
|
85
89
|
|
|
86
90
|
// Keep it reachable if the window shrinks.
|
|
87
91
|
useEffect(() => {
|
|
@@ -197,6 +197,11 @@ function getOrCreateTerminal(tabId: string, onExit: (tabId: string) => void): {
|
|
|
197
197
|
|
|
198
198
|
const terminal = new Terminal({
|
|
199
199
|
cursorBlink: true,
|
|
200
|
+
// Codex and other modern CLIs often render links as OSC 8 hyperlinks
|
|
201
|
+
// instead of plain URL text. OSC 8 links are handled by xterm core (not
|
|
202
|
+
// WebLinksAddon), whose default activation calls window.open(). Route them
|
|
203
|
+
// through the same embedded-panel handler as detected plain-text URLs.
|
|
204
|
+
linkHandler: { activate: handleTerminalLink },
|
|
200
205
|
// Off by default on macOS; without this a right-click would reselect the
|
|
201
206
|
// word under the cursor and clobber a multi-line table selection.
|
|
202
207
|
rightClickSelectsWord: false,
|
|
@@ -469,8 +474,12 @@ export function TerminalPanel({ tabId, agent, onSpawn, onExit, isActive }: Termi
|
|
|
469
474
|
|
|
470
475
|
// Wheel over the terminal is an explicit user scroll — stamp it so onScroll
|
|
471
476
|
// treats the resulting move as intentional (not an involuntary redraw jump).
|
|
477
|
+
// Capture is important: xterm handles wheel events on a descendant and
|
|
478
|
+
// emits onScroll synchronously. A bubbling listener runs too late, causing
|
|
479
|
+
// streamed Codex output to mistake the wheel scroll for a redraw and snap
|
|
480
|
+
// the viewport straight back to the bottom.
|
|
472
481
|
const onWheel = () => markGesture(tabId)
|
|
473
|
-
terminal.element?.addEventListener('wheel', onWheel, { passive: true })
|
|
482
|
+
terminal.element?.addEventListener('wheel', onWheel, { capture: true, passive: true })
|
|
474
483
|
|
|
475
484
|
// Resize handling — skip when the container is hidden (display:none → 0x0),
|
|
476
485
|
// otherwise fit() reports a tiny size and the PTY gets stuck at ~4 cols
|
|
@@ -499,7 +508,7 @@ export function TerminalPanel({ tabId, agent, onSpawn, onExit, isActive }: Termi
|
|
|
499
508
|
clearTimeout(resizeTimeout)
|
|
500
509
|
window.removeEventListener('resize', handleResize)
|
|
501
510
|
observer.disconnect()
|
|
502
|
-
terminal.element?.removeEventListener('wheel', onWheel)
|
|
511
|
+
terminal.element?.removeEventListener('wheel', onWheel, true)
|
|
503
512
|
// Do NOT dispose terminal or unsub PTY listeners — they persist across remounts
|
|
504
513
|
}
|
|
505
514
|
}, [tabId, onExit])
|
|
@@ -31,6 +31,10 @@ interface Ticket {
|
|
|
31
31
|
status: string
|
|
32
32
|
priority: string
|
|
33
33
|
created: string | null
|
|
34
|
+
// Owning datastore — tickets can live in more than one store per project,
|
|
35
|
+
// so edits and dispatch prompts must reference the row's own store.
|
|
36
|
+
blockId?: string
|
|
37
|
+
pageId?: string
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
type SaveState = { kind: 'idle' } | { kind: 'saving' } | { kind: 'error'; message: string }
|
|
@@ -82,10 +86,13 @@ function ticketPrompt(t: Ticket, refs: TicketRefs | null): string {
|
|
|
82
86
|
'ctlsurf references (use the ctlsurf MCP tools to read details or update status):',
|
|
83
87
|
`- Ticket row ID: ${t.id}`,
|
|
84
88
|
)
|
|
85
|
-
|
|
89
|
+
// Prefer the ticket's own store — it may not be the project's primary one.
|
|
90
|
+
const blockId = t.blockId ?? refs?.blockId
|
|
91
|
+
const pageId = t.pageId ?? refs?.pageId
|
|
92
|
+
if (blockId && pageId) {
|
|
86
93
|
lines.push(
|
|
87
|
-
`- Tickets datastore block ID: ${
|
|
88
|
-
`- Agent Datastore page ID: ${
|
|
94
|
+
`- Tickets datastore block ID: ${blockId}`,
|
|
95
|
+
`- Agent Datastore page ID: ${pageId}`,
|
|
89
96
|
)
|
|
90
97
|
}
|
|
91
98
|
return lines.join('\n')
|
|
@@ -177,6 +184,8 @@ export function TicketPane({ onSendToTerminal }: { onSendToTerminal: (text: stri
|
|
|
177
184
|
|
|
178
185
|
// form state
|
|
179
186
|
const [editingId, setEditingId] = useState<string | null>(null)
|
|
187
|
+
// Store the edited ticket came from, so the save lands in the right one.
|
|
188
|
+
const [editingBlockId, setEditingBlockId] = useState<string | null>(null)
|
|
180
189
|
const [title, setTitle] = useState('')
|
|
181
190
|
const [description, setDescription] = useState('')
|
|
182
191
|
const [status, setStatus] = useState(STATUS_OPTIONS[0].value)
|
|
@@ -216,6 +225,7 @@ export function TicketPane({ onSendToTerminal }: { onSendToTerminal: (text: stri
|
|
|
216
225
|
|
|
217
226
|
const openNewForm = useCallback(() => {
|
|
218
227
|
setEditingId(null)
|
|
228
|
+
setEditingBlockId(null)
|
|
219
229
|
setTitle('')
|
|
220
230
|
setDescription('')
|
|
221
231
|
setStatus(STATUS_OPTIONS[0].value)
|
|
@@ -226,6 +236,7 @@ export function TicketPane({ onSendToTerminal }: { onSendToTerminal: (text: stri
|
|
|
226
236
|
|
|
227
237
|
const openEditForm = useCallback((t: Ticket) => {
|
|
228
238
|
setEditingId(t.id)
|
|
239
|
+
setEditingBlockId(t.blockId ?? null)
|
|
229
240
|
setTitle(t.title)
|
|
230
241
|
setDescription(t.description)
|
|
231
242
|
setStatus(STATUS_OPTIONS.some(o => o.value === t.status) ? t.status : STATUS_OPTIONS[0].value)
|
|
@@ -240,7 +251,7 @@ export function TicketPane({ onSendToTerminal }: { onSendToTerminal: (text: stri
|
|
|
240
251
|
const payload = { title: title.trim(), description: description.trim(), status, priority }
|
|
241
252
|
try {
|
|
242
253
|
const r = editingId
|
|
243
|
-
? await window.worker.updateTicket(editingId, payload)
|
|
254
|
+
? await window.worker.updateTicket(editingId, payload, editingBlockId ?? undefined)
|
|
244
255
|
: await window.worker.addTicket(payload)
|
|
245
256
|
if (r?.ok) {
|
|
246
257
|
setView('list')
|
|
@@ -251,7 +262,7 @@ export function TicketPane({ onSendToTerminal }: { onSendToTerminal: (text: stri
|
|
|
251
262
|
} catch (err: any) {
|
|
252
263
|
setSave({ kind: 'error', message: err?.message || 'Failed to save ticket' })
|
|
253
264
|
}
|
|
254
|
-
}, [title, description, status, priority, editingId, save.kind, refreshList])
|
|
265
|
+
}, [title, description, status, priority, editingId, editingBlockId, save.kind, refreshList])
|
|
255
266
|
|
|
256
267
|
// Only capture keys while editing a ticket, so list-view typing in the terminal
|
|
257
268
|
// isn't hijacked. Esc returns to the list; Cmd/Ctrl+Enter saves.
|