@phenx-inc/ctlsurf 0.3.13 → 0.3.14
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/bin/ctlsurf-worker.js +38 -22
- package/out/headless/index.mjs +247 -1
- package/out/headless/index.mjs.map +4 -4
- package/out/main/index.js +303 -46
- package/out/preload/index.js +5 -0
- package/out/renderer/assets/{cssMode-CYoo4t9f.js → cssMode-G_SDogBL.js} +3 -3
- package/out/renderer/assets/{freemarker2--UQnPZsn.js → freemarker2-BzEus0h2.js} +1 -1
- package/out/renderer/assets/{handlebars-DVDrmX0C.js → handlebars-Et995f6O.js} +1 -1
- package/out/renderer/assets/{html-D1-cXoLy.js → html-D4wgKxPD.js} +1 -1
- package/out/renderer/assets/{htmlMode-f5nBuprq.js → htmlMode-DSxpefzL.js} +3 -3
- package/out/renderer/assets/{index-65hyKM_8.css → index-AQ346NMi.css} +386 -0
- package/out/renderer/assets/{index-D23nru43.js → index-ByJTqkiQ.js} +318 -22
- package/out/renderer/assets/{javascript-CcarFzBL.js → javascript-CzLoo8aq.js} +2 -2
- package/out/renderer/assets/{jsonMode-BvF-xK9U.js → jsonMode-BrwPy7fY.js} +3 -3
- package/out/renderer/assets/{liquid-CHLtUKl2.js → liquid-BsfPf6YG.js} +1 -1
- package/out/renderer/assets/{lspLanguageFeatures-B9aNeatS.js → lspLanguageFeatures-CxLZ421s.js} +1 -1
- package/out/renderer/assets/{mdx-HGDrkifZ.js → mdx-CPvHIsAR.js} +1 -1
- package/out/renderer/assets/{python-B_dPzjJ6.js → python-Dr7dCUjG.js} +1 -1
- package/out/renderer/assets/{razor-CHheM4ot.js → razor-a7zjD7Y3.js} +1 -1
- package/out/renderer/assets/{tsMode-CdC3i1gG.js → tsMode-B7KLV2X6.js} +1 -1
- package/out/renderer/assets/{typescript-BX6guVRK.js → typescript-Cjuzf37q.js} +1 -1
- package/out/renderer/assets/{xml-CpS-pOPE.js → xml-Yz9xINtk.js} +1 -1
- package/out/renderer/assets/{yaml-Du0AjOHW.js → yaml-DtKnp5J0.js} +1 -1
- package/out/renderer/index.html +2 -2
- package/package.json +1 -1
- package/src/main/ctlsurfApi.ts +11 -0
- package/src/main/index.ts +20 -0
- package/src/main/orchestrator.ts +37 -0
- package/src/main/ticketStore.ts +252 -0
- package/src/preload/index.ts +10 -0
- package/src/renderer/App.tsx +21 -0
- package/src/renderer/components/TicketPanel.tsx +308 -0
- package/src/renderer/styles.css +386 -0
package/src/main/index.ts
CHANGED
|
@@ -366,6 +366,26 @@ ipcMain.handle('tracking:set', async (_event, enabled: boolean) => {
|
|
|
366
366
|
return { active: orchestrator.isActiveTabTracking() }
|
|
367
367
|
})
|
|
368
368
|
|
|
369
|
+
// ─── Tickets IPC ──────────────────────────────────
|
|
370
|
+
|
|
371
|
+
ipcMain.handle('tickets:project', () => {
|
|
372
|
+
const cwd = orchestrator.getActiveTabCwd()
|
|
373
|
+
return { cwd, name: cwd ? cwd.split('/').filter(Boolean).pop() || cwd : null }
|
|
374
|
+
})
|
|
375
|
+
ipcMain.handle('tickets:add', async (_event, input: {
|
|
376
|
+
title: string; description?: string; status?: string; priority?: string
|
|
377
|
+
}) => {
|
|
378
|
+
return orchestrator.addTicketForActiveTab(input)
|
|
379
|
+
})
|
|
380
|
+
ipcMain.handle('tickets:update', async (_event, rowId: string, input: {
|
|
381
|
+
title: string; description?: string; status?: string; priority?: string
|
|
382
|
+
}) => {
|
|
383
|
+
return orchestrator.updateTicketForActiveTab(rowId, input)
|
|
384
|
+
})
|
|
385
|
+
ipcMain.handle('tickets:list', async () => {
|
|
386
|
+
return orchestrator.listTicketsForActiveTab()
|
|
387
|
+
})
|
|
388
|
+
|
|
369
389
|
// ─── Legacy Settings IPC ──────────────────────────
|
|
370
390
|
|
|
371
391
|
ipcMain.handle('settings:get', (_event, key: string) => {
|
package/src/main/orchestrator.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { CtlsurfApi } from './ctlsurfApi'
|
|
|
8
8
|
import { ConversationBridge } from './bridge'
|
|
9
9
|
import { WorkerWsClient, type WorkerWsStatus, type IncomingMessage } from './workerWs'
|
|
10
10
|
import { TimeTracker } from './timeTracker'
|
|
11
|
+
import { TicketStore, type TicketInput } from './ticketStore'
|
|
11
12
|
import { log } from './logger'
|
|
12
13
|
|
|
13
14
|
// ─── Types ────────────────────────────────────────
|
|
@@ -74,6 +75,7 @@ export class Orchestrator {
|
|
|
74
75
|
readonly bridge = new ConversationBridge()
|
|
75
76
|
readonly workerWs: WorkerWsClient
|
|
76
77
|
readonly timeTracker = new TimeTracker(this.ctlsurfApi)
|
|
78
|
+
readonly ticketStore = new TicketStore(this.ctlsurfApi)
|
|
77
79
|
|
|
78
80
|
// State
|
|
79
81
|
private tabs = new Map<string, TabState>()
|
|
@@ -480,6 +482,41 @@ export class Orchestrator {
|
|
|
480
482
|
}
|
|
481
483
|
}
|
|
482
484
|
|
|
485
|
+
// ─── Tickets (active tab) ───────────────────────
|
|
486
|
+
|
|
487
|
+
/** cwd of the focused terminal tab, or null if no tab is active. */
|
|
488
|
+
getActiveTabCwd(): string | null {
|
|
489
|
+
if (!this.activeTabId) return null
|
|
490
|
+
return this.tabs.get(this.activeTabId)?.cwd ?? null
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async addTicketForActiveTab(input: TicketInput): Promise<{ ok: boolean; error?: string }> {
|
|
494
|
+
const cwd = this.getActiveTabCwd()
|
|
495
|
+
if (!cwd) return { ok: false, error: 'No active terminal tab' }
|
|
496
|
+
if (!this.ctlsurfApi.getApiKey()) {
|
|
497
|
+
return { ok: false, error: 'ctlsurf API key not configured' }
|
|
498
|
+
}
|
|
499
|
+
return this.ticketStore.addTicket(cwd, input)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async updateTicketForActiveTab(rowId: string, input: TicketInput): Promise<{ ok: boolean; error?: string }> {
|
|
503
|
+
const cwd = this.getActiveTabCwd()
|
|
504
|
+
if (!cwd) return { ok: false, error: 'No active terminal tab' }
|
|
505
|
+
if (!this.ctlsurfApi.getApiKey()) {
|
|
506
|
+
return { ok: false, error: 'ctlsurf API key not configured' }
|
|
507
|
+
}
|
|
508
|
+
return this.ticketStore.updateTicket(cwd, rowId, input)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async listTicketsForActiveTab(): Promise<{ ok: boolean; tickets: import('./ticketStore').Ticket[]; error?: string }> {
|
|
512
|
+
const cwd = this.getActiveTabCwd()
|
|
513
|
+
if (!cwd) return { ok: false, tickets: [], error: 'No active terminal tab' }
|
|
514
|
+
if (!this.ctlsurfApi.getApiKey()) {
|
|
515
|
+
return { ok: false, tickets: [], error: 'ctlsurf API key not configured' }
|
|
516
|
+
}
|
|
517
|
+
return this.ticketStore.listTickets(cwd)
|
|
518
|
+
}
|
|
519
|
+
|
|
483
520
|
// ─── Worker WebSocket ───────────────────────────
|
|
484
521
|
|
|
485
522
|
connectWorkerWs(agent: AgentConfig, cwd: string): void {
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import type { CtlsurfApi } from './ctlsurfApi'
|
|
2
|
+
|
|
3
|
+
const DATASTORE_TITLE = 'Tickets'
|
|
4
|
+
const AGENT_DATASTORE_PAGE_TITLE = 'Agent Datastore'
|
|
5
|
+
const SYSTEM_KEY = 'tickets' // stable identifier in props.system_key; lookup survives title rename
|
|
6
|
+
|
|
7
|
+
const STATUS_OPTIONS = [
|
|
8
|
+
{ value: 'Open', color: 'blue' },
|
|
9
|
+
{ value: 'In Progress', color: 'yellow' },
|
|
10
|
+
{ value: 'Blocked', color: 'red' },
|
|
11
|
+
{ value: 'Done', color: 'green' },
|
|
12
|
+
]
|
|
13
|
+
const PRIORITY_OPTIONS = [
|
|
14
|
+
{ value: 'Low', color: 'gray' },
|
|
15
|
+
{ value: 'Med', color: 'yellow' },
|
|
16
|
+
{ value: 'High', color: 'red' },
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
interface ColumnSpec {
|
|
20
|
+
name: string
|
|
21
|
+
type: string
|
|
22
|
+
options?: Array<{ value: string; color?: string }>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const COLUMNS: ColumnSpec[] = [
|
|
26
|
+
{ name: 'Title', type: 'text' },
|
|
27
|
+
{ name: 'Description', type: 'text' },
|
|
28
|
+
{ name: 'Status', type: 'select', options: STATUS_OPTIONS },
|
|
29
|
+
{ name: 'Priority', type: 'select', options: PRIORITY_OPTIONS },
|
|
30
|
+
{ name: 'Created', type: 'date' },
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
export interface TicketInput {
|
|
34
|
+
title: string
|
|
35
|
+
description?: string
|
|
36
|
+
status?: string
|
|
37
|
+
priority?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface Ticket {
|
|
41
|
+
id: string
|
|
42
|
+
title: string
|
|
43
|
+
description: string
|
|
44
|
+
status: string
|
|
45
|
+
priority: string
|
|
46
|
+
created: string | null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function log(...args: unknown[]): void {
|
|
50
|
+
try { console.log('[ticket-store]', ...args) } catch { /* EPIPE safe */ }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function findPageByTitle(pages: any[], title: string): any | null {
|
|
54
|
+
for (const p of pages) {
|
|
55
|
+
if (p?.title === title) return p
|
|
56
|
+
if (p?.children?.length) {
|
|
57
|
+
const c = findPageByTitle(p.children, title)
|
|
58
|
+
if (c) return c
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Per-project `Tickets` datastore manager. Mirrors TimeTracker's datastore
|
|
66
|
+
* resolution (find folder → Agent Datastore page → find/create block by
|
|
67
|
+
* system_key), minus all the session/checkpoint machinery — a ticket is a
|
|
68
|
+
* single manual capture, not a tracked session.
|
|
69
|
+
*/
|
|
70
|
+
export class TicketStore {
|
|
71
|
+
private api: CtlsurfApi
|
|
72
|
+
private blockCache = new Map<string, string>()
|
|
73
|
+
|
|
74
|
+
constructor(api: CtlsurfApi) {
|
|
75
|
+
this.api = api
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Resolves (or creates) the project's Tickets datastore and appends a row. */
|
|
79
|
+
async addTicket(cwd: string, input: TicketInput): Promise<{ ok: boolean; error?: string }> {
|
|
80
|
+
const title = input.title?.trim()
|
|
81
|
+
if (!title) return { ok: false, error: 'Title is required' }
|
|
82
|
+
try {
|
|
83
|
+
const blockId = await this.ensureDatastore(cwd, true)
|
|
84
|
+
if (!blockId) {
|
|
85
|
+
return { ok: false, error: 'No ctlsurf project found for this folder' }
|
|
86
|
+
}
|
|
87
|
+
await this.api.addRow(blockId, {
|
|
88
|
+
Title: title,
|
|
89
|
+
Description: input.description?.trim() || '',
|
|
90
|
+
Status: input.status || 'Open',
|
|
91
|
+
Priority: input.priority || 'Med',
|
|
92
|
+
Created: new Date().toISOString(),
|
|
93
|
+
})
|
|
94
|
+
log(`Added ticket "${title}" for ${cwd}`)
|
|
95
|
+
return { ok: true }
|
|
96
|
+
} catch (err: any) {
|
|
97
|
+
log(`addTicket failed for ${cwd}: ${err?.message || err}`)
|
|
98
|
+
return { ok: false, error: err?.message || String(err) }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Updates an existing ticket row in the project's Tickets datastore. */
|
|
103
|
+
async updateTicket(cwd: string, rowId: string, input: TicketInput): Promise<{ ok: boolean; error?: string }> {
|
|
104
|
+
const title = input.title?.trim()
|
|
105
|
+
if (!title) return { ok: false, error: 'Title is required' }
|
|
106
|
+
try {
|
|
107
|
+
const blockId = await this.ensureDatastore(cwd, false)
|
|
108
|
+
if (!blockId) return { ok: false, error: 'No Tickets datastore for this project' }
|
|
109
|
+
await this.api.updateRow(blockId, rowId, {
|
|
110
|
+
Title: title,
|
|
111
|
+
Description: input.description?.trim() || '',
|
|
112
|
+
Status: input.status || 'Open',
|
|
113
|
+
Priority: input.priority || 'Med',
|
|
114
|
+
})
|
|
115
|
+
log(`Updated ticket ${rowId} for ${cwd}`)
|
|
116
|
+
return { ok: true }
|
|
117
|
+
} catch (err: any) {
|
|
118
|
+
log(`updateTicket failed for ${cwd}: ${err?.message || err}`)
|
|
119
|
+
return { ok: false, error: err?.message || String(err) }
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Lists existing tickets for the project, newest first. Does not create the
|
|
124
|
+
* datastore — an unconfigured project simply has no tickets yet. */
|
|
125
|
+
async listTickets(cwd: string): Promise<{ ok: boolean; tickets: Ticket[]; error?: string }> {
|
|
126
|
+
try {
|
|
127
|
+
const blockId = await this.ensureDatastore(cwd, false)
|
|
128
|
+
if (!blockId) return { ok: true, tickets: [] }
|
|
129
|
+
const res = await this.api.queryRows(blockId, { orderBy: 'Created', order: 'desc', limit: 200 })
|
|
130
|
+
const tickets: Ticket[] = (res?.rows || []).map(r => ({
|
|
131
|
+
id: r.id,
|
|
132
|
+
title: String(r.data?.Title ?? ''),
|
|
133
|
+
description: String(r.data?.Description ?? ''),
|
|
134
|
+
status: String(r.data?.Status ?? 'Open'),
|
|
135
|
+
priority: String(r.data?.Priority ?? 'Med'),
|
|
136
|
+
created: (r.data?.Created as string) ?? r.created_at ?? null,
|
|
137
|
+
}))
|
|
138
|
+
return { ok: true, tickets }
|
|
139
|
+
} catch (err: any) {
|
|
140
|
+
log(`listTickets failed for ${cwd}: ${err?.message || err}`)
|
|
141
|
+
return { ok: false, tickets: [], error: err?.message || String(err) }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private async ensureDatastore(cwd: string, create: boolean): Promise<string | null> {
|
|
146
|
+
const cached = this.blockCache.get(cwd)
|
|
147
|
+
if (cached) return cached
|
|
148
|
+
|
|
149
|
+
let folder: any = null
|
|
150
|
+
try {
|
|
151
|
+
folder = await this.api.findFolderByPath(cwd)
|
|
152
|
+
} catch {
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
if (!folder?.id) return null
|
|
156
|
+
|
|
157
|
+
const folderDetail = await this.api.getFolder(folder.id)
|
|
158
|
+
const agentPage = findPageByTitle(folderDetail?.pages || [], AGENT_DATASTORE_PAGE_TITLE)
|
|
159
|
+
if (!agentPage?.id) return null
|
|
160
|
+
|
|
161
|
+
const blockId = await this.findOrAdoptBlock(agentPage.id)
|
|
162
|
+
if (blockId) {
|
|
163
|
+
await this.ensureColumns(blockId)
|
|
164
|
+
this.blockCache.set(cwd, blockId)
|
|
165
|
+
return blockId
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!create) return null
|
|
169
|
+
|
|
170
|
+
const columns = COLUMNS.map((c, i) => ({
|
|
171
|
+
id: `col_${i}`,
|
|
172
|
+
name: c.name,
|
|
173
|
+
type: c.type,
|
|
174
|
+
...(c.options ? { options: c.options } : {}),
|
|
175
|
+
}))
|
|
176
|
+
const created = await this.api.createBlock(agentPage.id, {
|
|
177
|
+
type: 'datastore',
|
|
178
|
+
title: DATASTORE_TITLE,
|
|
179
|
+
props: { columns, system_key: SYSTEM_KEY },
|
|
180
|
+
})
|
|
181
|
+
if (created?.id) {
|
|
182
|
+
log(`Created "${DATASTORE_TITLE}" datastore on Agent Datastore page for ${cwd}`)
|
|
183
|
+
this.blockCache.set(cwd, created.id)
|
|
184
|
+
return created.id
|
|
185
|
+
}
|
|
186
|
+
return null
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Finds an existing Tickets block on the page. Prefers a system_key match
|
|
190
|
+
* (which survives title renames). Falls back to title match for legacy blocks
|
|
191
|
+
* and backfills system_key on the way out. */
|
|
192
|
+
private async findOrAdoptBlock(pageId: string): Promise<string | null> {
|
|
193
|
+
const summaries = (await this.api.getPageBlockSummaries(pageId)) || []
|
|
194
|
+
const datastoreSummaries = summaries.filter((b: any) => b?.type === 'datastore' && b?.id)
|
|
195
|
+
if (datastoreSummaries.length === 0) return null
|
|
196
|
+
|
|
197
|
+
let titleFallbackId: string | null = null
|
|
198
|
+
let titleFallbackProps: Record<string, unknown> | null = null
|
|
199
|
+
for (const s of datastoreSummaries) {
|
|
200
|
+
try {
|
|
201
|
+
const block = await this.api.getBlock(s.id)
|
|
202
|
+
const props = (block?.props || {}) as Record<string, unknown>
|
|
203
|
+
if (props.system_key === SYSTEM_KEY) {
|
|
204
|
+
return s.id
|
|
205
|
+
}
|
|
206
|
+
if (s.title === DATASTORE_TITLE && titleFallbackId === null) {
|
|
207
|
+
titleFallbackId = s.id
|
|
208
|
+
titleFallbackProps = props
|
|
209
|
+
}
|
|
210
|
+
} catch (err: any) {
|
|
211
|
+
log(`getBlock(${s.id}) failed during lookup: ${err?.message || err}`)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (titleFallbackId) {
|
|
216
|
+
try {
|
|
217
|
+
await this.api.updateBlock(titleFallbackId, {
|
|
218
|
+
props: { ...(titleFallbackProps || {}), system_key: SYSTEM_KEY },
|
|
219
|
+
})
|
|
220
|
+
log(`Backfilled system_key on legacy Tickets block ${titleFallbackId}`)
|
|
221
|
+
} catch (err: any) {
|
|
222
|
+
log(`backfill system_key failed on ${titleFallbackId}: ${err?.message || err}`)
|
|
223
|
+
}
|
|
224
|
+
return titleFallbackId
|
|
225
|
+
}
|
|
226
|
+
return null
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private async ensureColumns(blockId: string): Promise<void> {
|
|
230
|
+
try {
|
|
231
|
+
const schema = await this.api.getDatastoreSchema(blockId)
|
|
232
|
+
const existingCols = schema.columns || []
|
|
233
|
+
const existingNames = new Set(existingCols.map(c => c.name))
|
|
234
|
+
const missing = COLUMNS.filter(c => !existingNames.has(c.name))
|
|
235
|
+
if (missing.length === 0) return
|
|
236
|
+
|
|
237
|
+
const usedIds = new Set(existingCols.map(c => c.id))
|
|
238
|
+
let nextIdx = existingCols.length
|
|
239
|
+
const appended = missing.map(c => {
|
|
240
|
+
let id = `col_${nextIdx++}`
|
|
241
|
+
while (usedIds.has(id)) id = `col_${nextIdx++}`
|
|
242
|
+
usedIds.add(id)
|
|
243
|
+
return { id, name: c.name, type: c.type, ...(c.options ? { options: c.options } : {}) }
|
|
244
|
+
})
|
|
245
|
+
const merged = [...existingCols, ...appended]
|
|
246
|
+
await this.api.updateDatastoreSchema(blockId, merged)
|
|
247
|
+
log(`Added ${missing.length} missing column(s) to existing Tickets datastore: ${missing.map(c => c.name).join(', ')}`)
|
|
248
|
+
} catch (err: any) {
|
|
249
|
+
log(`ensureColumns failed: ${err?.message || err}`)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
package/src/preload/index.ts
CHANGED
|
@@ -82,6 +82,16 @@ const api = {
|
|
|
82
82
|
setTracking: (enabled: boolean): Promise<{ active: boolean }> =>
|
|
83
83
|
ipcRenderer.invoke('tracking:set', enabled),
|
|
84
84
|
|
|
85
|
+
// Tickets (active tab)
|
|
86
|
+
getTicketProject: (): Promise<{ cwd: string | null; name: string | null }> =>
|
|
87
|
+
ipcRenderer.invoke('tickets:project'),
|
|
88
|
+
addTicket: (input: { title: string; description?: string; status?: string; priority?: string }): Promise<{ ok: boolean; error?: string }> =>
|
|
89
|
+
ipcRenderer.invoke('tickets:add', input),
|
|
90
|
+
updateTicket: (rowId: string, input: { title: string; description?: string; status?: string; priority?: string }): Promise<{ ok: boolean; error?: string }> =>
|
|
91
|
+
ipcRenderer.invoke('tickets:update', rowId, input),
|
|
92
|
+
listTickets: (): Promise<{ ok: boolean; tickets: Array<{ id: string; title: string; description: string; status: string; priority: string; created: string | null }>; error?: string }> =>
|
|
93
|
+
ipcRenderer.invoke('tickets:list'),
|
|
94
|
+
|
|
85
95
|
// Chat logging (global)
|
|
86
96
|
getLogChat: (): Promise<{ enabled: boolean }> =>
|
|
87
97
|
ipcRenderer.invoke('logchat:get'),
|
package/src/renderer/App.tsx
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from './components/PaneLayout'
|
|
13
13
|
import { StatusBar } from './components/StatusBar'
|
|
14
14
|
import { SettingsDialog } from './components/SettingsDialog'
|
|
15
|
+
import { TicketPanel } from './components/TicketPanel'
|
|
15
16
|
|
|
16
17
|
interface AgentConfig {
|
|
17
18
|
id: string
|
|
@@ -44,6 +45,10 @@ declare global {
|
|
|
44
45
|
saveProfile: (profileId: string, data: { name: string; apiKey?: string; baseUrl: string; dataspacePageId: string }) => Promise<{ ok: boolean }>
|
|
45
46
|
switchProfile: (profileId: string) => Promise<{ ok: boolean }>
|
|
46
47
|
deleteProfile: (profileId: string) => Promise<{ ok: boolean }>
|
|
48
|
+
getTicketProject: () => Promise<{ cwd: string | null; name: string | null }>
|
|
49
|
+
addTicket: (input: { title: string; description?: string; status?: string; priority?: string }) => Promise<{ ok: boolean; error?: string }>
|
|
50
|
+
updateTicket: (rowId: string, input: { title: string; description?: string; status?: string; priority?: string }) => Promise<{ ok: boolean; error?: string }>
|
|
51
|
+
listTickets: () => Promise<{ ok: boolean; tickets: Array<{ id: string; title: string; description: string; status: string; priority: string; created: string | null }>; error?: string }>
|
|
47
52
|
getTracking: () => Promise<{ active: boolean }>
|
|
48
53
|
setTracking: (enabled: boolean) => Promise<{ active: boolean }>
|
|
49
54
|
getLogChat: () => Promise<{ enabled: boolean }>
|
|
@@ -116,6 +121,7 @@ export default function App() {
|
|
|
116
121
|
})
|
|
117
122
|
const [activeTabId, setActiveTabId] = useState<string>(tabs[0].id)
|
|
118
123
|
const [trackingActive, setTrackingActive] = useState(false)
|
|
124
|
+
const [showTicketPanel, setShowTicketPanel] = useState(false)
|
|
119
125
|
|
|
120
126
|
// Agent picker state: which tab is being configured (null = initial picker for first tab)
|
|
121
127
|
const [pickerTargetTabId, setPickerTargetTabId] = useState<string | null>(tabs[0].id)
|
|
@@ -412,6 +418,20 @@ export default function App() {
|
|
|
412
418
|
<span className="tracking-icon">⏱</span>
|
|
413
419
|
<span className={`tracking-dot ${trackingActive ? 'on' : 'off'}`} />
|
|
414
420
|
</button>
|
|
421
|
+
<button
|
|
422
|
+
className={`titlebar-btn titlebar-icon-btn ${showTicketPanel ? 'active' : ''}`}
|
|
423
|
+
onClick={() => setShowTicketPanel(v => !v)}
|
|
424
|
+
title="Tickets for the active project"
|
|
425
|
+
aria-label="Tickets"
|
|
426
|
+
>
|
|
427
|
+
<svg className="ticket-tag-icon" viewBox="0 0 24 24" width="13" height="13"
|
|
428
|
+
fill="none" stroke="currentColor" strokeWidth="2"
|
|
429
|
+
strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
430
|
+
<path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z" />
|
|
431
|
+
<line x1="7" y1="7" x2="7.01" y2="7" />
|
|
432
|
+
</svg>
|
|
433
|
+
<span>Tickets</span>
|
|
434
|
+
</button>
|
|
415
435
|
<span className="titlebar-separator" />
|
|
416
436
|
{agents.map(a => {
|
|
417
437
|
const activeTab = tabs.find(t => t.id === activeTabId)
|
|
@@ -457,6 +477,7 @@ export default function App() {
|
|
|
457
477
|
|
|
458
478
|
<StatusBar wsStatus={wsStatus} cwd={cwd} onChangeCwd={handleChangeCwd} updateInfo={updateInfo} />
|
|
459
479
|
<SettingsDialog open={showSettings} onClose={() => setShowSettings(false)} />
|
|
480
|
+
<TicketPanel open={showTicketPanel} onClose={() => setShowTicketPanel(false)} />
|
|
460
481
|
|
|
461
482
|
{showAgentPicker && agents.length > 0 && (
|
|
462
483
|
<AgentPicker
|